kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Mdoc/RELEASE_AUDIT_2026_6_0.md | 2+-
Msrc/link/link.c | 9+++++++++
Msrc/obj/macho/tbd_read.c | 76+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/obj/registry.c | 10+++++++++-
Mtest/link/link_compat_test.c | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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); }