/* * macho-postlink — post-link fixups for Mach-O executables produced by * ld64.lld when cross-linking macOS binaries on a non-darwin host. * * macho-postlink [--stack-size=0xNNN] [--entitlements=] * [--identifier=] * * Two jobs, both done in place: * * 1. `--stack-size`: set `LC_MAIN.stacksize`. Apple's ld64 implements * `-stack_size`; lld's Mach-O port parses it and prints "not yet * implemented" (LLVM 21), leaving the field 0 (= the 8 MB default). * JSC's interpreter recurses deeply enough that bun ships with an * 18 MB main-thread stack on every platform — patch the load command * after the fact. * * 2. Re-generate the ad-hoc code signature, embedding the entitlements * plist. arm64 macOS refuses to exec unsigned binaries, and any header * edit (including #1) invalidates the hash of page 0 in the existing * linker-generated CodeDirectory — so a re-sign is mandatory whenever * the stack size is patched. While re-signing, the entitlements plist * is embedded as both the XML blob (CSSLOT_ENTITLEMENTS) and the * DER blob (CSSLOT_DER_ENTITLEMENTS — required by AMFI for binaries * whose deployment target is macOS 12+). The signature replaces the * one already reserved by `-adhoc_codesign`; the file is grown or * shrunk as needed and __LINKEDIT / LC_CODE_SIGNATURE are updated to * match. * * Standalone by design: no dependency beyond libc, compiles as C11 on any * host (`cc -O2 macho-postlink.c -o macho-postlink`), so it can also be * run by hand on a Mac to inspect/repair a binary. All Mach-O and code- * signature structures are defined locally rather than pulled from * so the Linux build doesn't need Apple headers. * * The signature layout intentionally mirrors what `codesign --sign -` * produces: SuperBlob{ CodeDirectory, Requirements(empty), Entitlements, * DER Entitlements }, SHA-256 page hashes over [0, dataoff), special * slots -1..-7 with the entitlement/requirement blob hashes filled in. */ /* ftruncate/fileno are POSIX, hidden under -std=c11 without this. The tool * only ever runs on POSIX hosts (a Linux build box or a Mac). */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include /* ──────────────────────────────────────────────────────────────────────── * SHA-256 (FIPS 180-4). Self-contained so the tool has no library * dependency on either the build host or a Mac. * ──────────────────────────────────────────────────────────────────────── */ typedef struct { uint32_t state[8]; uint64_t total_len; uint8_t buf[64]; size_t buf_len; } sha256_ctx; static const uint32_t SHA256_K[64] = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, }; static uint32_t rotr32(uint32_t x, unsigned n) { return (x >> n) | (x << (32 - n)); } static void sha256_init(sha256_ctx *c) { static const uint32_t iv[8] = { 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, }; memcpy(c->state, iv, sizeof(iv)); c->total_len = 0; c->buf_len = 0; } static void sha256_block(sha256_ctx *c, const uint8_t *p) { uint32_t w[64]; for (int i = 0; i < 16; i++) { w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | ((uint32_t)p[i * 4 + 2] << 8) | (uint32_t)p[i * 4 + 3]; } for (int i = 16; i < 64; i++) { uint32_t s0 = rotr32(w[i - 15], 7) ^ rotr32(w[i - 15], 18) ^ (w[i - 15] >> 3); uint32_t s1 = rotr32(w[i - 2], 17) ^ rotr32(w[i - 2], 19) ^ (w[i - 2] >> 10); w[i] = w[i - 16] + s0 + w[i - 7] + s1; } uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3]; uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7]; for (int i = 0; i < 64; i++) { uint32_t s1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25); uint32_t ch = (e & f) ^ (~e & g); uint32_t t1 = h + s1 + ch + SHA256_K[i] + w[i]; uint32_t s0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22); uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc); uint32_t t2 = s0 + maj; h = g; g = f; f = e; e = d + t1; d = cc; cc = b; b = a; a = t1 + t2; } c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d; c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h; } static void sha256_update(sha256_ctx *c, const void *data, size_t len) { const uint8_t *p = (const uint8_t *)data; c->total_len += len; if (c->buf_len > 0) { size_t take = 64 - c->buf_len; if (take > len) take = len; memcpy(c->buf + c->buf_len, p, take); c->buf_len += take; p += take; len -= take; if (c->buf_len == 64) { sha256_block(c, c->buf); c->buf_len = 0; } } while (len >= 64) { sha256_block(c, p); p += 64; len -= 64; } if (len > 0) { memcpy(c->buf, p, len); c->buf_len = len; } } static void sha256_final(sha256_ctx *c, uint8_t out[32]) { /* Capture the message length before padding mutates total_len. */ uint64_t bit_len = c->total_len * 8; uint8_t pad = 0x80; sha256_update(c, &pad, 1); uint8_t zero = 0; while (c->buf_len != 56) { sha256_update(c, &zero, 1); } uint8_t lenbuf[8]; for (int i = 0; i < 8; i++) lenbuf[i] = (uint8_t)(bit_len >> (56 - i * 8)); sha256_update(c, lenbuf, 8); for (int i = 0; i < 8; i++) { out[i * 4] = (uint8_t)(c->state[i] >> 24); out[i * 4 + 1] = (uint8_t)(c->state[i] >> 16); out[i * 4 + 2] = (uint8_t)(c->state[i] >> 8); out[i * 4 + 3] = (uint8_t)(c->state[i]); } } static void sha256(const void *data, size_t len, uint8_t out[32]) { sha256_ctx c; sha256_init(&c); sha256_update(&c, data, len); sha256_final(&c, out); } /* ──────────────────────────────────────────────────────────────────────── * Mach-O structures (subset of , defined locally so this * compiles on non-darwin hosts). All header fields are little-endian on * the architectures we target (x86_64, arm64). * ──────────────────────────────────────────────────────────────────────── */ #define MH_MAGIC_64 0xfeedfacfu #define CPU_TYPE_ARM64 0x0100000cu #define CPU_TYPE_X86_64 0x01000007u #define LC_REQ_DYLD 0x80000000u #define LC_SEGMENT_64 0x19u #define LC_CODE_SIGNATURE 0x1du #define LC_MAIN (0x28u | LC_REQ_DYLD) typedef struct { uint32_t magic; uint32_t cputype; uint32_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; uint32_t reserved; } mach_header_64; typedef struct { uint32_t cmd; uint32_t cmdsize; } load_command; typedef struct { uint32_t cmd; uint32_t cmdsize; uint64_t entryoff; uint64_t stacksize; } entry_point_command; typedef struct { uint32_t cmd; uint32_t cmdsize; char segname[16]; uint64_t vmaddr; uint64_t vmsize; uint64_t fileoff; uint64_t filesize; uint32_t maxprot; uint32_t initprot; uint32_t nsects; uint32_t flags; } segment_command_64; typedef struct { uint32_t cmd; uint32_t cmdsize; uint32_t dataoff; uint32_t datasize; } linkedit_data_command; /* ──────────────────────────────────────────────────────────────────────── * Code signature structures (subset of / * osfmk/kern/cs_blobs.h). All fields are BIG-endian on disk. * ──────────────────────────────────────────────────────────────────────── */ #define CSMAGIC_REQUIREMENTS 0xfade0c01u #define CSMAGIC_CODEDIRECTORY 0xfade0c02u #define CSMAGIC_EMBEDDED_SIGNATURE 0xfade0cc0u #define CSMAGIC_EMBEDDED_ENTITLEMENTS 0xfade7171u #define CSMAGIC_EMBEDDED_DER_ENTITLEMENTS 0xfade7172u #define CSSLOT_CODEDIRECTORY 0u #define CSSLOT_REQUIREMENTS 2u #define CSSLOT_ENTITLEMENTS 5u #define CSSLOT_DER_ENTITLEMENTS 7u #define CS_ADHOC 0x00000002u #define CS_HASHTYPE_SHA256 2u #define CS_EXECSEG_MAIN_BINARY 0x1u #define CS_PAGE_SIZE 4096u /* CodeDirectory page granule — always 4 KB, even on 16 KB-page arm64. */ #define CS_HASH_SIZE 32u /* SHA-256 */ /* CodeDirectory version 0x20400 — earliest version with the execSeg * fields the kernel uses to decide whether MAP_JIT is allowed. */ #define CS_CODEDIRECTORY_VERSION 0x20400u typedef struct { uint32_t magic; uint32_t length; uint32_t count; } cs_superblob; typedef struct { uint32_t type; uint32_t offset; } cs_blob_index; typedef struct { uint32_t magic; uint32_t length; uint32_t version; uint32_t flags; uint32_t hashOffset; uint32_t identOffset; uint32_t nSpecialSlots; uint32_t nCodeSlots; uint32_t codeLimit; uint8_t hashSize; uint8_t hashType; uint8_t platform; uint8_t pageSize; /* log2 */ uint32_t spare2; uint32_t scatterOffset; /* v0x20100 */ uint32_t teamOffset; /* v0x20200 */ uint32_t spare3; /* v0x20300 */ uint64_t codeLimit64; /* v0x20300 */ uint64_t execSegBase; /* v0x20400 */ uint64_t execSegLimit; /* v0x20400 */ uint64_t execSegFlags; /* v0x20400 */ } cs_code_directory; /* ──────────────────────────────────────────────────────────────────────── * Helpers * ──────────────────────────────────────────────────────────────────────── */ static const char *g_file = ""; static void die(const char *msg) { fprintf(stderr, "macho-postlink: %s: %s\n", g_file, msg); exit(1); } static void die2(const char *msg, const char *detail) { fprintf(stderr, "macho-postlink: %s: %s: %s\n", g_file, msg, detail); exit(1); } static uint32_t be32(uint32_t v) { return ((v & 0xffu) << 24) | ((v & 0xff00u) << 8) | ((v >> 8) & 0xff00u) | (v >> 24); } static uint64_t be64(uint64_t v) { return ((uint64_t)be32((uint32_t)v) << 32) | be32((uint32_t)(v >> 32)); } /* Growable byte buffer for assembling the signature superblob. */ typedef struct { uint8_t *data; size_t len; size_t cap; } buf_t; static void buf_reserve(buf_t *b, size_t extra) { if (b->len + extra <= b->cap) return; size_t cap = b->cap ? b->cap : 256; while (cap < b->len + extra) cap *= 2; uint8_t *p = (uint8_t *)realloc(b->data, cap); if (!p) die("out of memory"); b->data = p; b->cap = cap; } static void buf_push(buf_t *b, const void *data, size_t len) { buf_reserve(b, len); memcpy(b->data + b->len, data, len); b->len += len; } static void buf_push_byte(buf_t *b, uint8_t v) { buf_push(b, &v, 1); } /* ──────────────────────────────────────────────────────────────────────── * Entitlements: minimal plist reader + DER encoder * * Only the subset of plist that bun's entitlements files use is accepted: * a single of NAME followed by or . * Anything else (strings, arrays, nested dicts) is rejected so a future * edit to entitlements.plist can't silently produce a DER blob that * doesn't match the XML — AMFI compares the two. * ──────────────────────────────────────────────────────────────────────── */ typedef struct { char *key; int value; /* 0 or 1 */ } entitlement_t; typedef struct { entitlement_t *items; size_t count; } entitlements_t; static const char *skip_ws(const char *p) { while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; return p; } /* Parse `...` / `` / `` pairs out of the plist. * Returns the number of pairs found. Dies on anything it doesn't understand * between a and its value. */ static entitlements_t parse_entitlements(const char *xml) { entitlements_t out = {NULL, 0}; size_t cap = 0; const char *p = xml; for (;;) { const char *k = strstr(p, ""); if (!k) break; k += 5; const char *kend = strstr(k, ""); if (!kend) die("entitlements plist: unterminated "); const char *v = skip_ws(kend + 6); int value; if (strncmp(v, "", 7) == 0) { value = 1; p = v + 7; } else if (strncmp(v, "", 8) == 0) { value = 0; p = v + 8; } else { die("entitlements plist: only boolean values are supported " "(update the DER encoder in macho-postlink.c if a non-boolean entitlement is added)"); return out; /* unreachable */ } if (out.count == cap) { cap = cap ? cap * 2 : 8; out.items = (entitlement_t *)realloc(out.items, cap * sizeof(entitlement_t)); if (!out.items) die("out of memory"); } size_t klen = (size_t)(kend - k); char *key = (char *)malloc(klen + 1); if (!key) die("out of memory"); memcpy(key, k, klen); key[klen] = 0; if (strchr(key, '&') || strchr(key, '<')) die("entitlements plist: XML escapes in keys are not supported"); out.items[out.count].key = key; out.items[out.count].value = value; out.count++; } if (out.count == 0) die("entitlements plist: no / pairs found"); /* DER dictionaries are sorted by key. Insertion sort — the list is tiny. */ for (size_t i = 1; i < out.count; i++) { entitlement_t tmp = out.items[i]; size_t j = i; while (j > 0 && strcmp(out.items[j - 1].key, tmp.key) > 0) { out.items[j] = out.items[j - 1]; j--; } out.items[j] = tmp; } return out; } /* DER definite-length encoding. */ static void der_push_len(buf_t *b, size_t len) { if (len < 0x80) { buf_push_byte(b, (uint8_t)len); } else if (len <= 0xff) { buf_push_byte(b, 0x81); buf_push_byte(b, (uint8_t)len); } else if (len <= 0xffff) { buf_push_byte(b, 0x82); buf_push_byte(b, (uint8_t)(len >> 8)); buf_push_byte(b, (uint8_t)len); } else { die("entitlements DER: value too large"); } } static void der_push_tlv(buf_t *b, uint8_t tag, const void *data, size_t len) { buf_push_byte(b, tag); der_push_len(b, len); buf_push(b, data, len); } /* * DER entitlements payload (the bytes after the 8-byte blob header): * * APPLICATION [16] constructed (0x70) { * INTEGER 1 * CONTEXT [16] constructed (0xB0) { * SEQUENCE { UTF8String key, BOOLEAN value } (sorted by key) * ... * } * } * * Matches the encoding `codesign` has emitted since macOS 12 and the one * rcodesign/apple-codesign produces. */ static buf_t der_encode_entitlements(const entitlements_t *ents) { /* Inner dict: concatenated SEQUENCEs. */ buf_t dict = {0}; for (size_t i = 0; i < ents->count; i++) { buf_t seq = {0}; der_push_tlv(&seq, 0x0c /* UTF8String */, ents->items[i].key, strlen(ents->items[i].key)); uint8_t boolval = ents->items[i].value ? 0xff : 0x00; der_push_tlv(&seq, 0x01 /* BOOLEAN */, &boolval, 1); buf_push_byte(&dict, 0x30 /* SEQUENCE, constructed */); der_push_len(&dict, seq.len); buf_push(&dict, seq.data, seq.len); free(seq.data); } /* version INTEGER 1 + CONTEXT[16]{dict} */ buf_t body = {0}; uint8_t one = 1; der_push_tlv(&body, 0x02 /* INTEGER */, &one, 1); buf_push_byte(&body, 0xb0 /* CONTEXT [16], constructed */); der_push_len(&body, dict.len); buf_push(&body, dict.data, dict.len); free(dict.data); /* APPLICATION[16]{body} */ buf_t out = {0}; buf_push_byte(&out, 0x70 /* APPLICATION [16], constructed */); der_push_len(&out, body.len); buf_push(&out, body.data, body.len); free(body.data); return out; } /* ──────────────────────────────────────────────────────────────────────── * Signature assembly * ──────────────────────────────────────────────────────────────────────── */ /* Append a generic blob (4-byte magic, 4-byte total length, payload), * recording its offset within `sb` into *out_off. */ static void push_blob(buf_t *sb, uint32_t magic, const void *payload, size_t payload_len, uint32_t *out_off) { *out_off = (uint32_t)sb->len; uint32_t hdr[2] = {be32(magic), be32((uint32_t)(payload_len + 8))}; buf_push(sb, hdr, sizeof(hdr)); if (payload_len > 0) buf_push(sb, payload, payload_len); } /* * Build the embedded-signature superblob. * * code: the file contents from offset 0 to code_limit (= dataoff) * identifier: signing identifier (NUL-terminated in the blob) * ents_xml: raw plist bytes, or NULL to skip the entitlement blobs * exec_*: __TEXT segment file extent for the execSeg fields */ static buf_t build_signature(const uint8_t *code, uint64_t code_limit, const char *identifier, const char *ents_xml, size_t ents_xml_len, uint64_t exec_seg_base, uint64_t exec_seg_limit) { uint32_t n_special = 0; uint32_t n_blobs = 1; /* CodeDirectory */ /* Empty Requirements superblob — what `codesign --sign -` embeds. */ const uint8_t reqs_payload[4] = {0, 0, 0, 0}; /* count = 0 */ n_blobs += 1; if (n_special < CSSLOT_REQUIREMENTS) n_special = CSSLOT_REQUIREMENTS; buf_t der = {0}; if (ents_xml != NULL) { entitlements_t ents = parse_entitlements(ents_xml); der = der_encode_entitlements(&ents); for (size_t i = 0; i < ents.count; i++) free(ents.items[i].key); free(ents.items); n_blobs += 2; if (n_special < CSSLOT_DER_ENTITLEMENTS) n_special = CSSLOT_DER_ENTITLEMENTS; } /* CodeDirectory layout: header, identifier, special-slot hashes * (stored in *reverse* slot order, ending right at hashOffset), then * code-slot hashes. */ size_t ident_len = strlen(identifier) + 1; uint64_t n_code_slots = (code_limit + CS_PAGE_SIZE - 1) / CS_PAGE_SIZE; size_t cd_header = sizeof(cs_code_directory); size_t ident_off = cd_header; size_t hash_off = ident_off + ident_len + (size_t)n_special * CS_HASH_SIZE; size_t cd_len = hash_off + (size_t)n_code_slots * CS_HASH_SIZE; uint8_t *cd = (uint8_t *)calloc(1, cd_len); if (!cd) die("out of memory"); cs_code_directory *cdh = (cs_code_directory *)cd; cdh->magic = be32(CSMAGIC_CODEDIRECTORY); cdh->length = be32((uint32_t)cd_len); cdh->version = be32(CS_CODEDIRECTORY_VERSION); cdh->flags = be32(CS_ADHOC); cdh->hashOffset = be32((uint32_t)hash_off); cdh->identOffset = be32((uint32_t)ident_off); cdh->nSpecialSlots = be32(n_special); cdh->nCodeSlots = be32((uint32_t)n_code_slots); /* codeLimit is 32-bit; a >4 GB executable would need codeLimit64. The * linker would have failed long before that, but check anyway. */ if (code_limit > 0xffffffffull) die("binary too large for a 32-bit codeLimit"); cdh->codeLimit = be32((uint32_t)code_limit); cdh->hashSize = CS_HASH_SIZE; cdh->hashType = CS_HASHTYPE_SHA256; cdh->platform = 0; cdh->pageSize = 12; /* log2(4096) */ cdh->execSegBase = be64(exec_seg_base); cdh->execSegLimit = be64(exec_seg_limit); cdh->execSegFlags = be64(CS_EXECSEG_MAIN_BINARY); memcpy(cd + ident_off, identifier, ident_len); /* Code-slot hashes: SHA-256 of each 4 KB page of [0, code_limit). The * final partial page is hashed over only the bytes present (matching * the kernel's validation in cs_validate_page). */ for (uint64_t i = 0; i < n_code_slots; i++) { uint64_t off = i * CS_PAGE_SIZE; uint64_t len = code_limit - off; if (len > CS_PAGE_SIZE) len = CS_PAGE_SIZE; sha256(code + off, (size_t)len, cd + hash_off + (size_t)i * CS_HASH_SIZE); } /* Assemble the superblob: header + index entries + blobs. */ size_t index_bytes = sizeof(cs_superblob) + (size_t)n_blobs * sizeof(cs_blob_index); buf_t sb = {0}; buf_reserve(&sb, index_bytes); memset(sb.data, 0, index_bytes); sb.len = index_bytes; uint32_t cd_off, reqs_off, ents_off = 0, der_off = 0; /* The CodeDirectory already begins with its own magic+length header, so * it's appended verbatim rather than via push_blob(). */ cd_off = (uint32_t)sb.len; buf_push(&sb, cd, cd_len); free(cd); push_blob(&sb, CSMAGIC_REQUIREMENTS, reqs_payload, sizeof(reqs_payload), &reqs_off); if (ents_xml != NULL) { push_blob(&sb, CSMAGIC_EMBEDDED_ENTITLEMENTS, ents_xml, ents_xml_len, &ents_off); push_blob(&sb, CSMAGIC_EMBEDDED_DER_ENTITLEMENTS, der.data, der.len, &der_off); } free(der.data); /* Special-slot hashes are SHA-256 of each blob (header included), * stored at hashOffset - slot*hashSize. Slots without a blob stay * all-zero (calloc above). Blob offsets within a superblob are not * 4-byte aligned (codesign doesn't align them either), so the lengths * are recomputed from the inputs rather than read back from the blob * headers. */ uint8_t *special = sb.data + cd_off + hash_off; sha256(sb.data + reqs_off, sizeof(reqs_payload) + 8, special - (size_t)CSSLOT_REQUIREMENTS * CS_HASH_SIZE); if (ents_xml != NULL) { sha256(sb.data + ents_off, ents_xml_len + 8, special - (size_t)CSSLOT_ENTITLEMENTS * CS_HASH_SIZE); sha256(sb.data + der_off, der.len + 8, special - (size_t)CSSLOT_DER_ENTITLEMENTS * CS_HASH_SIZE); } /* Fill in the superblob header + index now that offsets are known. */ cs_superblob *hdr = (cs_superblob *)sb.data; hdr->magic = be32(CSMAGIC_EMBEDDED_SIGNATURE); hdr->length = be32((uint32_t)sb.len); hdr->count = be32(n_blobs); cs_blob_index *idx = (cs_blob_index *)(sb.data + sizeof(cs_superblob)); uint32_t slot = 0; idx[slot].type = be32(CSSLOT_CODEDIRECTORY); idx[slot].offset = be32(cd_off); slot++; idx[slot].type = be32(CSSLOT_REQUIREMENTS); idx[slot].offset = be32(reqs_off); slot++; if (ents_xml != NULL) { idx[slot].type = be32(CSSLOT_ENTITLEMENTS); idx[slot].offset = be32(ents_off); slot++; idx[slot].type = be32(CSSLOT_DER_ENTITLEMENTS); idx[slot].offset = be32(der_off); slot++; } return sb; } /* ──────────────────────────────────────────────────────────────────────── * main * ──────────────────────────────────────────────────────────────────────── */ static char *read_file(const char *path, size_t *out_len) { FILE *f = fopen(path, "rb"); if (!f) die2("cannot open", path); if (fseek(f, 0, SEEK_END) != 0) die2("cannot seek", path); long len = ftell(f); if (len < 0) die2("cannot tell", path); if (fseek(f, 0, SEEK_SET) != 0) die2("cannot seek", path); char *data = (char *)malloc((size_t)len + 1); if (!data) die("out of memory"); if (len > 0 && fread(data, 1, (size_t)len, f) != (size_t)len) die2("cannot read", path); data[len] = 0; fclose(f); *out_len = (size_t)len; return data; } static void usage(void) { fprintf(stderr, "usage: macho-postlink [--stack-size=0xNNN] [--entitlements=] [--identifier=]\n" "\n" "Patches LC_MAIN.stacksize and regenerates the ad-hoc code signature\n" "(embedding the entitlements plist) of a Mach-O executable, in place.\n"); exit(2); } int main(int argc, char **argv) { const char *file = NULL; const char *entitlements_path = NULL; const char *identifier = NULL; uint64_t stack_size = 0; int have_stack_size = 0; for (int i = 1; i < argc; i++) { const char *a = argv[i]; if (strncmp(a, "--stack-size=", 13) == 0) { char *end = NULL; stack_size = strtoull(a + 13, &end, 0); if (end == NULL || *end != 0 || stack_size == 0) { fprintf(stderr, "macho-postlink: invalid --stack-size value: %s\n", a + 13); return 2; } have_stack_size = 1; } else if (strncmp(a, "--entitlements=", 15) == 0) { entitlements_path = a + 15; } else if (strncmp(a, "--identifier=", 13) == 0) { identifier = a + 13; } else if (strcmp(a, "--help") == 0 || strcmp(a, "-h") == 0) { usage(); } else if (a[0] == '-') { fprintf(stderr, "macho-postlink: unknown option: %s\n", a); usage(); } else if (file == NULL) { file = a; } else { usage(); } } if (file == NULL) usage(); g_file = file; size_t size = 0; uint8_t *data = (uint8_t *)read_file(file, &size); if (size < sizeof(mach_header_64)) die("not a Mach-O file (too small)"); mach_header_64 *mh = (mach_header_64 *)data; if (mh->magic != MH_MAGIC_64) { /* A FAT/universal binary or a 32-bit slice would land here. The build * only ever links thin 64-bit executables. */ die("not a thin 64-bit little-endian Mach-O (bad magic)"); } if ((uint64_t)sizeof(mach_header_64) + mh->sizeofcmds > size) die("load commands extend past end of file"); /* Walk the load commands. */ entry_point_command *lc_main = NULL; linkedit_data_command *lc_codesig = NULL; segment_command_64 *seg_linkedit = NULL; segment_command_64 *seg_text = NULL; uint8_t *p = data + sizeof(mach_header_64); uint8_t *cmds_end = p + mh->sizeofcmds; for (uint32_t i = 0; i < mh->ncmds; i++) { if (p + sizeof(load_command) > cmds_end) die("truncated load command"); load_command *lc = (load_command *)p; if (lc->cmdsize < sizeof(load_command) || p + lc->cmdsize > cmds_end) die("bad load command size"); if (lc->cmd == LC_MAIN && lc->cmdsize >= sizeof(entry_point_command)) { lc_main = (entry_point_command *)p; } else if (lc->cmd == LC_CODE_SIGNATURE && lc->cmdsize >= sizeof(linkedit_data_command)) { lc_codesig = (linkedit_data_command *)p; } else if (lc->cmd == LC_SEGMENT_64 && lc->cmdsize >= sizeof(segment_command_64)) { segment_command_64 *seg = (segment_command_64 *)p; if (strncmp(seg->segname, "__LINKEDIT", 16) == 0) seg_linkedit = seg; if (strncmp(seg->segname, "__TEXT", 16) == 0) seg_text = seg; } p += lc->cmdsize; } /* 1. Stack size. */ if (have_stack_size) { if (lc_main == NULL) die("no LC_MAIN load command (is this an executable?)"); lc_main->stacksize = stack_size; } /* 2. Re-sign. Always done when a signature exists — even a stack-size-only * run invalidates the page-0 hash of the old signature. */ if (lc_codesig != NULL) { if (seg_linkedit == NULL) die("LC_CODE_SIGNATURE present but no __LINKEDIT segment"); if (seg_text == NULL) die("no __TEXT segment"); uint64_t dataoff = lc_codesig->dataoff; if (dataoff > size || (uint64_t)lc_codesig->datasize > size - dataoff) die("LC_CODE_SIGNATURE points past end of file"); if (dataoff + lc_codesig->datasize != size) die("code signature is not the last thing in the file — refusing to re-sign"); if ((dataoff & 0xf) != 0) die("code signature offset is not 16-byte aligned"); char *ents_xml = NULL; size_t ents_xml_len = 0; if (entitlements_path != NULL) ents_xml = read_file(entitlements_path, &ents_xml_len); /* Default identifier: the basename of the file, like codesign. */ char ident_buf[256]; if (identifier == NULL) { const char *base = strrchr(file, '/'); base = base ? base + 1 : file; snprintf(ident_buf, sizeof(ident_buf), "%s", base); identifier = ident_buf; } /* The header edits below (LC_CODE_SIGNATURE.datasize, __LINKEDIT sizes) * land in page 0, which is hashed into the CodeDirectory — so the new * sizes must be computed and written *before* hashing. The signature * size depends only on its inputs, not on the header contents, so do a * dry-run layout first to learn the size, patch the header, then build * the real thing. */ buf_t probe = build_signature(data, dataoff, identifier, ents_xml, ents_xml_len, seg_text->fileoff, seg_text->filesize); uint32_t new_datasize = (uint32_t)probe.len; free(probe.data); lc_codesig->datasize = new_datasize; uint64_t new_filesize = dataoff + new_datasize - seg_linkedit->fileoff; seg_linkedit->filesize = new_filesize; /* vmsize: page-aligned to the *VM* page size (16 KB on arm64, 4 KB on * x86_64) — distinct from the 4 KB CodeDirectory page granule. */ uint64_t vm_page = (mh->cputype == CPU_TYPE_ARM64) ? 0x4000u : 0x1000u; seg_linkedit->vmsize = (new_filesize + vm_page - 1) & ~(vm_page - 1); buf_t sig = build_signature(data, dataoff, identifier, ents_xml, ents_xml_len, seg_text->fileoff, seg_text->filesize); if (sig.len != new_datasize) die("internal error: signature size changed between layout and build"); /* Rewrite the file: [0, dataoff) unchanged (but with the patched * header), then the new signature. */ FILE *f = fopen(file, "r+b"); if (!f) die("cannot reopen for writing"); if (fwrite(data, 1, (size_t)dataoff, f) != dataoff) die("write failed"); if (fwrite(sig.data, 1, sig.len, f) != sig.len) die("write failed"); fflush(f); /* Shrink the file if the new signature is smaller than the old one * (e.g. the linker reserved more space than the final blob needs). */ if (ftruncate(fileno(f), (off_t)(dataoff + sig.len)) != 0) die("ftruncate failed"); if (fclose(f) != 0) die("close failed"); free(sig.data); free(ents_xml); } else if (have_stack_size) { if (entitlements_path != NULL) die("no LC_CODE_SIGNATURE to embed entitlements into — link with -Wl,-adhoc_codesign"); /* No signature to maintain — just write the patched header back. */ FILE *f = fopen(file, "r+b"); if (!f) die("cannot reopen for writing"); if (fwrite(data, 1, sizeof(mach_header_64) + mh->sizeofcmds, f) != sizeof(mach_header_64) + mh->sizeofcmds) die("write failed"); if (fclose(f) != 0) die("close failed"); } else if (entitlements_path != NULL) { die("no LC_CODE_SIGNATURE to embed entitlements into — link with -Wl,-adhoc_codesign"); } free(data); return 0; }