commit bfbdf529eff7cfaa0f5683bae7aa1eb75288d387
parent 103b2604d4bdae9a61892266f750ddd7589637c6
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 16 Jul 2026 11:10:39 -0700
link: validate non-ELF dynamic inputs
Diffstat:
5 files changed, 407 insertions(+), 3 deletions(-)
diff --git a/doc/RELEASE_AUDIT_2026_6_0.md b/doc/RELEASE_AUDIT_2026_6_0.md
@@ -491,7 +491,7 @@ Implementation checkpoint (updated 2026-07-16):
| KIT-P2-007 | **Complete** | `test-selfdist`: 105 pass, 0 fail; release/dist gates reject the test key by Minisign material even after copy/comment changes |
| KIT-P1-012 | **Complete** | authenticated updater result cases included in `test-selfdist`: 105 pass, 0 fail |
| KIT-P1-001 | **Complete** | `target_test`: 177 checks, 0 failures; `test-driver-targets`: 61 pass, 0 fail |
-| KIT-P0-004 | In progress | initial public matrix 60/0 and driver suite 15/0; Mach-O DSO, COFF import-library, pointer-width, and endian boundary cases active |
+| KIT-P0-004 | **Complete** | public ELF/Mach-O/COFF matrix 97/0, including dylib/TBD, import-library, pointer-width/endian boundaries; driver authority/inference/archive suite 15/0 |
| KIT-P1-002 | **Complete** | NDK r27d NativeActivity preprocess/compile/shared-link at API 21 and 35: 3 pass, 0 fail |
| KIT-P1-003 | **Complete** | `test-smoke-rv32`: 20 pass, 0 fail, including concurrent empty-cache/read-only/QEMU cases |
| KIT-P1-004 | **Complete** | independent `llvm-readelf` rv32/rv64 soft/hard, mixed-ABI, and RVC merge oracle: 36 pass, 0 fail |
diff --git a/src/link/link.c b/src/link/link.c
@@ -327,6 +327,7 @@ LinkInputId link_add_dso_bytes(Linker* l, const char* name, const u8* data,
Sym soname = 0;
KitBinFmt fmt;
ObjFormatDsoReader reader;
+ const ObjFormatImpl* target_impl;
const char* reader_name;
Slice label;
if (!l || !data || !len) return LINK_INPUT_NONE;
@@ -339,6 +340,14 @@ LinkInputId link_add_dso_bytes(Linker* l, const char* name, const u8* data,
"(fmt=%u) for '%.*s'",
(u32)fmt,
SLICE_ARG(name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)")));
+ target_impl = obj_format_lookup(l->c->target.obj);
+ if (!target_impl || reader.format != target_impl)
+ compiler_panic(
+ l->c, SRCLOC_NONE,
+ "link: incompatible input '%.*s': object format mismatch; expected "
+ "format=%s, got format=%s",
+ SLICE_ARG(label), kit_target_obj_name(l->c->target.obj),
+ reader.format && reader.format->name ? reader.format->name : "unknown");
reader_name = reader.name;
ob = reader.read(l->c, name, data, len, &soname);
if (!ob)
diff --git a/src/obj/macho/tbd_read.c b/src/obj/macho/tbd_read.c
@@ -50,6 +50,71 @@ static int is_id_cont(u8 c) {
(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
}
+static int is_target_cont(u8 c) {
+ return is_id_cont(c) || c == '-';
+}
+
+/* TAPI v4 declares the supported architecture/platform pairs in a top-level
+ * `targets:` list. Validate that list against the already-resolved link target
+ * before treating the text stub as a DSO. Older TAPI documents that have no
+ * top-level targets field retain the reader's legacy behavior. */
+static int tbd_supports_target(Compiler* c, const u8* data, size_t len,
+ int* declared_out) {
+ static const char KEY[] = "targets:";
+ const char* arch;
+ const char* platform;
+ size_t arch_len;
+ size_t platform_len;
+ size_t line = 0;
+
+ *declared_out = 0;
+ switch (c->target.arch) {
+ case KIT_ARCH_ARM_64:
+ arch = "arm64";
+ break;
+ case KIT_ARCH_X86_64:
+ arch = "x86_64";
+ break;
+ default:
+ return 0;
+ }
+ switch (c->target.os) {
+ case KIT_OS_MACOS:
+ platform = "-macos";
+ break;
+ case KIT_OS_IOS:
+ platform = "-ios";
+ break;
+ default:
+ return 0;
+ }
+ arch_len = strlen(arch);
+ platform_len = strlen(platform);
+
+ while (line < len) {
+ size_t end = line;
+ size_t i;
+ while (end < len && data[end] != '\n' && data[end] != '\r') ++end;
+ if (end - line >= sizeof(KEY) - 1u &&
+ memcmp(data + line, KEY, sizeof(KEY) - 1u) == 0) {
+ *declared_out = 1;
+ for (i = line + sizeof(KEY) - 1u;
+ i + arch_len + platform_len <= end; ++i) {
+ size_t after = i + arch_len + platform_len;
+ if ((i == line || !is_target_cont(data[i - 1u])) &&
+ memcmp(data + i, arch, arch_len) == 0 &&
+ memcmp(data + i + arch_len, platform, platform_len) == 0 &&
+ (after == end || !is_target_cont(data[after])))
+ return 1;
+ }
+ return 0;
+ }
+ while (end < len && (data[end] == '\n' || data[end] == '\r')) ++end;
+ line = end;
+ }
+ return 1;
+}
+
/* Extract the install-name from the first document. We look for a
* line beginning with "install-name:" and take the value up to EOL,
* then strip whitespace and surrounding quotes. Returns 0 if absent. */
@@ -88,7 +153,7 @@ static Sym extract_install_name(Compiler* c, const u8* data, size_t len) {
ObjBuilder* read_tbd(Compiler* c, const char* name, const u8* data, size_t len,
Sym* install_name_out) {
- (void)name;
+ int declares_targets;
if (install_name_out) *install_name_out = 0;
if (!data || !len) compiler_panic(c, SRCLOC_NONE, "read_tbd: empty input");
@@ -107,6 +172,15 @@ ObjBuilder* read_tbd(Compiler* c, const char* name, const u8* data, size_t len,
"read_tbd: unsupported target arch %u for tbd lookup",
(u32)c->target.arch);
}
+ if (!tbd_supports_target(c, data, len, &declares_targets) &&
+ declares_targets) {
+ const char* arch =
+ c->target.arch == KIT_ARCH_ARM_64 ? "arm64" : "x86_64";
+ const char* platform = c->target.os == KIT_OS_IOS ? "ios" : "macos";
+ compiler_panic(c, SRCLOC_NONE,
+ "read_tbd: input '%s' does not support target %s-%s",
+ name ? name : "(unnamed)", arch, platform);
+ }
ObjBuilder* ob = obj_new(c);
if (!ob) compiler_panic(c, SRCLOC_NONE, "read_tbd: obj_new failed");
diff --git a/src/obj/registry.c b/src/obj/registry.c
@@ -478,7 +478,15 @@ static int coff_machine_to_arch(u16 machine, KitArchKind* out) {
static KitStatus detect_coff(const u8* d, size_t len, KitTargetSpec* out) {
u16 machine;
KitArchKind arch;
- if (len >= 2 && d[0] == 'M' && d[1] == 'Z') {
+ if (len >= COFF_IMPORT_OBJECT_HEADER_SIZE &&
+ coff_rd_u16(d + 0u) == IMPORT_OBJECT_HDR_SIG1 &&
+ coff_rd_u16(d + 2u) == IMPORT_OBJECT_HDR_SIG2) {
+ /* Microsoft short-import members put Machine at offset 6 rather than in
+ * the ordinary COFF header's first word. They still participate in the
+ * public link-session target check before the archive handler consumes
+ * their import metadata. */
+ machine = coff_rd_u16(d + 6u);
+ } else if (len >= 2 && d[0] == 'M' && d[1] == 'Z') {
u32 e_lfanew, pe_sig;
if (len < 64) return KIT_MALFORMED; /* DOS header */
e_lfanew = (u32)d[60] | ((u32)d[61] << 8) | ((u32)d[62] << 16) |
diff --git a/test/link/link_compat_test.c b/test/link/link_compat_test.c
@@ -71,6 +71,44 @@ static uint8_t* make_archive(KitSlice member, size_t* len_out) {
return bytes;
}
+static void put_u16le(uint8_t* p, uint16_t v) {
+ p[0] = (uint8_t)v;
+ p[1] = (uint8_t)(v >> 8);
+}
+
+static void put_u32le(uint8_t* p, uint32_t v) {
+ p[0] = (uint8_t)v;
+ p[1] = (uint8_t)(v >> 8);
+ p[2] = (uint8_t)(v >> 16);
+ p[3] = (uint8_t)(v >> 24);
+}
+
+static KitSlice make_macho_dylib(uint8_t bytes[32], uint32_t cpu_type) {
+ memset(bytes, 0, 32u);
+ put_u32le(bytes + 0u, 0xfeedfacfu); /* MH_MAGIC_64 */
+ put_u32le(bytes + 4u, cpu_type);
+ put_u32le(bytes + 8u, 3u); /* CPU_SUBTYPE_*_ALL */
+ put_u32le(bytes + 12u, 6u); /* MH_DYLIB */
+ return (KitSlice){.data = bytes, .len = 32u};
+}
+
+static uint8_t* make_coff_short_import(uint16_t machine, size_t* len_out) {
+ static const char body[] = "audit_import\0audit.dll\0";
+ size_t body_len = sizeof(body) - 1u;
+ size_t total = 20u + body_len;
+ uint8_t* bytes = (uint8_t*)malloc(total);
+ if (!bytes) return NULL;
+ memset(bytes, 0, total);
+ put_u16le(bytes + 0u, 0u); /* IMPORT_OBJECT_HDR_SIG1 */
+ put_u16le(bytes + 2u, 0xffffu); /* IMPORT_OBJECT_HDR_SIG2 */
+ put_u16le(bytes + 6u, machine);
+ put_u32le(bytes + 12u, (uint32_t)body_len);
+ put_u16le(bytes + 18u, 4u); /* CODE | IMPORT_OBJECT_NAME << 2 */
+ memcpy(bytes + 20u, body, body_len);
+ *len_out = total;
+ return bytes;
+}
+
static KitLinkSession* new_reloc_session(KitCompiler* c) {
KitLinkSessionOptions opts;
KitLinkSession* s = NULL;
@@ -374,12 +412,287 @@ done:
if (linux) kit_compiler_free(linux);
}
+static void check_macho_dso_boundary(void) {
+ static const uint8_t tbd_x64[] =
+ "--- !tapi-tbd\n"
+ "tbd-version: 4\n"
+ "targets: [ x86_64-macos ]\n"
+ "install-name: '/usr/lib/libAudit.dylib'\n"
+ "exports:\n"
+ " - targets: [ x86_64-macos ]\n"
+ " symbols: [ _audit_import ]\n"
+ "...\n";
+ static const uint8_t tbd_arm64[] =
+ "--- !tapi-tbd\n"
+ "tbd-version: 4\n"
+ "targets: [ arm64-macos ]\n"
+ "install-name: '/usr/lib/libAudit.dylib'\n"
+ "exports:\n"
+ " - targets: [ arm64-macos ]\n"
+ " symbols: [ _audit_import ]\n"
+ "...\n";
+ static const uint8_t tbd_ios[] =
+ "--- !tapi-tbd\n"
+ "tbd-version: 4\n"
+ "targets: [ x86_64-ios ]\n"
+ "install-name: '/usr/lib/libAudit.dylib'\n"
+ "exports:\n"
+ " - targets: [ x86_64-ios ]\n"
+ " symbols: [ _audit_import ]\n"
+ "...\n";
+ KitCompiler* expected = NULL;
+ KitCompiler* elf_expected = NULL;
+ KitLinkSession* s = NULL;
+ KitStatus st;
+ uint8_t native_bytes[32];
+ uint8_t foreign_bytes[32];
+ KitSlice native = make_macho_dylib(native_bytes, 0x01000007u);
+ KitSlice foreign = make_macho_dylib(foreign_bytes, 0x0100000cu);
+ KitSlice native_tbd = {.data = tbd_x64, .len = sizeof(tbd_x64) - 1u};
+ KitSlice foreign_tbd = {.data = tbd_arm64,
+ .len = sizeof(tbd_arm64) - 1u};
+ KitSlice platform_tbd = {.data = tbd_ios, .len = sizeof(tbd_ios) - 1u};
+
+ EXPECT(kit_unit_compiler_new(
+ &g_u,
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_MACOS, KIT_OBJ_MACHO),
+ &expected) == KIT_OK &&
+ expected,
+ "Mach-O DSO boundary compiler");
+ EXPECT(kit_unit_compiler_new(
+ &g_u,
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF),
+ &elf_expected) == KIT_OK &&
+ elf_expected,
+ "TBD format-boundary compiler");
+ if (!expected || !elf_expected) goto done;
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "Mach-O DSO matching session");
+ if (s) {
+ EXPECT(kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libnative-x64.dylib"), &native) == KIT_OK,
+ "matching Mach-O dylib accepted");
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "Mach-O DSO mismatch session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libforeign-arm64.dylib"), &foreign);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign Mach-O dylib rejected");
+ EXPECT(strstr(g_u.last_diag, "libforeign-arm64.dylib") != NULL &&
+ strstr(g_u.last_diag, "architecture mismatch") != NULL,
+ "Mach-O dylib mismatch is named: %s", g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(elf_expected);
+ EXPECT(s != NULL, "TBD format-mismatched session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libforeign-format.tbd"), &native_tbd);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "Mach-O TBD rejected by ELF link session");
+ EXPECT(strstr(g_u.last_diag, "libforeign-format.tbd") != NULL &&
+ strstr(g_u.last_diag, "object format mismatch") != NULL,
+ "TBD format mismatch is named: %s", g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "matching TBD session");
+ if (s) {
+ EXPECT(kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libnative-x64.tbd"), &native_tbd) == KIT_OK,
+ "matching Mach-O TBD accepted");
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "architecture-mismatched TBD session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libforeign-arm64.tbd"), &foreign_tbd);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign-architecture TBD rejected");
+ EXPECT(strstr(g_u.last_diag, "libforeign-arm64.tbd") != NULL,
+ "TBD architecture mismatch is named: %s", g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "platform-mismatched TBD session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libforeign-ios.tbd"), &platform_tbd);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign-platform TBD rejected");
+ EXPECT(strstr(g_u.last_diag, "libforeign-ios.tbd") != NULL,
+ "TBD platform mismatch is named: %s", g_u.last_diag);
+ }
+
+done:
+ if (s) kit_link_session_free(s);
+ if (elf_expected) kit_compiler_free(elf_expected);
+ if (expected) kit_compiler_free(expected);
+}
+
+static void check_coff_import_boundary(void) {
+ KitCompiler* expected = NULL;
+ KitLinkSession* s = NULL;
+ KitStatus st;
+ uint8_t* native_import = NULL;
+ uint8_t* foreign_import = NULL;
+ uint8_t* native_archive = NULL;
+ uint8_t* foreign_archive = NULL;
+ size_t native_import_len = 0, foreign_import_len = 0;
+ size_t native_archive_len = 0, foreign_archive_len = 0;
+
+ EXPECT(kit_unit_compiler_new(
+ &g_u,
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_WINDOWS, KIT_OBJ_COFF),
+ &expected) == KIT_OK &&
+ expected,
+ "COFF import-library boundary compiler");
+ native_import = make_coff_short_import(0x8664u, &native_import_len);
+ foreign_import = make_coff_short_import(0xaa64u, &foreign_import_len);
+ EXPECT(native_import && foreign_import, "COFF short-import fixtures");
+ if (!expected || !native_import || !foreign_import) goto done;
+ native_archive = make_archive(
+ (KitSlice){.data = native_import, .len = native_import_len},
+ &native_archive_len);
+ foreign_archive = make_archive(
+ (KitSlice){.data = foreign_import, .len = foreign_import_len},
+ &foreign_archive_len);
+ EXPECT(native_archive && foreign_archive, "COFF import-library archives");
+ if (!native_archive || !foreign_archive) goto done;
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "matching COFF import-library session");
+ if (s) {
+ KitLinkArchiveInput input = {0};
+ input.name = KIT_SLICE_LIT("native-x64.lib");
+ input.bytes.data = native_archive;
+ input.bytes.len = native_archive_len;
+ EXPECT(kit_link_session_add_archive_bytes(s, &input) == KIT_OK,
+ "matching COFF import library accepted");
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "mismatched COFF import-library session");
+ if (s) {
+ KitLinkArchiveInput input = {0};
+ input.name = KIT_SLICE_LIT("foreign-arm64.lib");
+ input.bytes.data = foreign_archive;
+ input.bytes.len = foreign_archive_len;
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_archive_bytes(s, &input);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign COFF import library rejected");
+ EXPECT(strstr(g_u.last_diag, "foreign-arm64.lib(foreign.o)") != NULL &&
+ strstr(g_u.last_diag, "architecture mismatch") != NULL,
+ "COFF import mismatch names archive/member: %s", g_u.last_diag);
+ }
+
+done:
+ if (s) kit_link_session_free(s);
+ free(foreign_archive);
+ free(native_archive);
+ free(foreign_import);
+ free(native_import);
+ if (expected) kit_compiler_free(expected);
+}
+
+static void check_data_model_boundary(void) {
+ KitTargetSpec expected_spec =
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF);
+ KitTargetSpec narrow_spec = expected_spec;
+ KitTargetSpec endian_spec = expected_spec;
+ KitCompiler* expected = NULL;
+ KitCompiler* narrow = NULL;
+ KitCompiler* endian = NULL;
+ KitObjBuilder* narrow_obj = NULL;
+ KitObjBuilder* endian_obj = NULL;
+ KitLinkSession* s = NULL;
+ KitStatus st;
+
+ narrow_spec.ptr_size = 4u;
+ narrow_spec.ptr_align = 4u;
+ endian_spec.big_endian = 1u;
+ EXPECT(kit_unit_compiler_new(&g_u, expected_spec, &expected) == KIT_OK &&
+ expected,
+ "data-model expected compiler");
+ EXPECT(kit_unit_compiler_new(&g_u, narrow_spec, &narrow) == KIT_OK && narrow,
+ "data-model narrow compiler");
+ EXPECT(kit_unit_compiler_new(&g_u, endian_spec, &endian) == KIT_OK && endian,
+ "data-model big-endian compiler");
+ if (!expected || !narrow || !endian) goto done;
+ EXPECT(kit_obj_builder_new(narrow, &narrow_obj) == KIT_OK && narrow_obj,
+ "data-model narrow builder");
+ EXPECT(kit_obj_builder_new(endian, &endian_obj) == KIT_OK && endian_obj,
+ "data-model big-endian builder");
+ if (!narrow_obj || !endian_obj) goto done;
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "pointer-width mismatch session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_obj(s, narrow_obj);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "pointer-width mismatch rejected");
+ EXPECT(strstr(g_u.last_diag, "pointer size mismatch") != NULL &&
+ strstr(g_u.last_diag, "ptr=8") != NULL &&
+ strstr(g_u.last_diag, "ptr=4") != NULL,
+ "pointer-width diagnostic has expected/actual: %s", g_u.last_diag);
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "endianness mismatch session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_obj(s, endian_obj);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "endianness mismatch rejected");
+ EXPECT(strstr(g_u.last_diag, "endianness mismatch") != NULL &&
+ strstr(g_u.last_diag, "endian=little") != NULL &&
+ strstr(g_u.last_diag, "endian=big") != NULL,
+ "endianness diagnostic has expected/actual: %s", g_u.last_diag);
+ }
+
+done:
+ if (s) kit_link_session_free(s);
+ if (endian_obj) kit_obj_builder_free(endian_obj);
+ if (narrow_obj) kit_obj_builder_free(narrow_obj);
+ if (endian) kit_compiler_free(endian);
+ if (narrow) kit_compiler_free(narrow);
+ if (expected) kit_compiler_free(expected);
+}
+
int main(void) {
kit_unit_init(&g_u);
check_input_target_boundary();
check_format_boundary(KIT_OS_MACOS, KIT_OBJ_MACHO, "foreign-macho.a");
check_format_boundary(KIT_OS_WINDOWS, KIT_OBJ_COFF, "foreign-coff.a");
check_platform_and_format_rules();
+ check_macho_dso_boundary();
+ check_coff_import_boundary();
+ check_data_model_boundary();
kit_unit_summary(&g_u, "link_compat_test");
return kit_unit_status(&g_u);
}