Files
bun-src/scripts/build/shims/macho-postlink.c
T
2026-08-27 21:09:14 +00:00

827 lines
31 KiB
C

/*
* 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 <file> [--stack-size=0xNNN] [--entitlements=<plist>]
* [--identifier=<name>]
*
* 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
* <mach-o/loader.h> 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
/* ────────────────────────────────────────────────────────────────────────
* 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 <mach-o/loader.h>, 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 <Security/CSCommon.h> /
* 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 = "<unknown>";
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 <dict> of <key>NAME</key> followed by <true/> or <false/>.
* 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 `<key>...</key>` / `<true/>` / `<false/>` pairs out of the plist.
* Returns the number of pairs found. Dies on anything it doesn't understand
* between a <key> 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, "<key>");
if (!k) break;
k += 5;
const char *kend = strstr(k, "</key>");
if (!kend) die("entitlements plist: unterminated <key>");
const char *v = skip_ws(kend + 6);
int value;
if (strncmp(v, "<true/>", 7) == 0) {
value = 1;
p = v + 7;
} else if (strncmp(v, "<false/>", 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 <key>/<true|false/> 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 <file> [--stack-size=0xNNN] [--entitlements=<plist>] [--identifier=<name>]\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;
}