commit f73b0ab8419d3e2781fc654d0e107a2206e719d8
parent f962942019b876ed343d1ba705c0c6fa96bd22bf
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 16 Jul 2026 11:24:06 -0700
link: validate legacy TAPI targets
Diffstat:
3 files changed, 152 insertions(+), 32 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`: 107 pass, 0 fail; release/dist gates reject the test signer ID after copy, comment changes, or valid reserialization |
| KIT-P1-012 | **Complete** | authenticated updater result cases included in `test-selfdist`: 107 pass, 0 fail |
| KIT-P1-001 | **Complete** | `target_test`: 177 checks, 0 failures; `test-driver-targets`: 61 pass, 0 fail |
-| KIT-P0-004 | **Complete** | public ELF/Mach-O/COFF matrix 106/0, including dylib/TBD device/simulator/arm64e, import-library, pointer-width/endian boundaries; driver authority/inference/archive suite 15/0 |
+| KIT-P0-004 | **Complete** | public ELF/Mach-O/COFF matrix 114/0, including TAPI v2-v4 device/simulator/arm64e, 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; independent `llvm-readelf` confirms AArch64 `ET_DYN` plus exported `ANativeActivity_onCreate`: 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/obj/macho/tbd_read.c b/src/obj/macho/tbd_read.c
@@ -54,18 +54,65 @@ 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_line_has_token(const u8* data, size_t start, size_t end,
+ const char* token, size_t token_len) {
+ size_t i;
+ for (i = start; i + token_len <= end; ++i) {
+ size_t after = i + token_len;
+ if ((i == start || !is_target_cont(data[i - 1u])) &&
+ memcmp(data + i, token, token_len) == 0 &&
+ (after == end || !is_target_cont(data[after])))
+ return 1;
+ }
+ return 0;
+}
+
+static int tbd_line_has_target(const u8* data, size_t start, size_t end,
+ const char* arch, size_t arch_len,
+ const char* platform, size_t platform_len,
+ int allow_arm64e) {
+ size_t i;
+ for (i = start; i + arch_len + platform_len <= end; ++i) {
+ size_t after = i + arch_len + platform_len;
+ if ((i == start || !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;
+ /* Current macOS SDK stubs advertise arm64e for system dylibs even when an
+ * ordinary arm64 consumer is linked. ld64 accepts that ABI-compatible
+ * provider, so Kit must accept the SDK's only ARM slice as well. */
+ if (allow_arm64e && i + 6u + platform_len <= end &&
+ (i == start || !is_target_cont(data[i - 1u])) &&
+ memcmp(data + i, "arm64e", 6u) == 0 &&
+ memcmp(data + i + 6u, platform, platform_len) == 0 &&
+ (i + 6u + platform_len == end ||
+ !is_target_cont(data[i + 6u + platform_len])))
+ return 1;
+ }
+ return 0;
+}
+
+/* TAPI v4 declares architecture/platform pairs in top-level `targets:`. v2/v3
+ * split the same authority across top-level `archs:` and `platform:` fields.
+ * Validate either schema before treating the text stub as a DSO; only truly
+ * target-less legacy documents retain the permissive scanner behavior. */
static int tbd_supports_target(Compiler* c, const u8* data, size_t len,
int* declared_out) {
- static const char KEY[] = "targets:";
+ static const char TARGETS_KEY[] = "targets:";
+ static const char ARCHS_KEY[] = "archs:";
+ static const char PLATFORM_KEY[] = "platform:";
const char* arch;
const char* platform;
+ const char* legacy_platform;
size_t arch_len;
size_t platform_len;
+ size_t legacy_platform_len;
size_t line = 0;
+ int saw_archs = 0;
+ int saw_platform = 0;
+ int arch_matches = 0;
+ int platform_matches = 0;
*declared_out = 0;
switch (c->target.arch) {
@@ -76,58 +123,65 @@ static int tbd_supports_target(Compiler* c, const u8* data, size_t len,
arch = "x86_64";
break;
default:
+ *declared_out = 1;
return 0;
}
switch (c->target.os) {
case KIT_OS_MACOS:
platform = "-macos";
+ legacy_platform = "macosx";
break;
case KIT_OS_IOS:
platform = "-ios";
+ legacy_platform = "ios";
break;
case KIT_OS_IOS_SIMULATOR:
platform = "-ios-simulator";
+ legacy_platform = "ios-simulator";
break;
default:
+ *declared_out = 1;
return 0;
}
arch_len = strlen(arch);
platform_len = strlen(platform);
+ legacy_platform_len = strlen(legacy_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) {
+ if (end - line >= sizeof(TARGETS_KEY) - 1u &&
+ memcmp(data + line, TARGETS_KEY, sizeof(TARGETS_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;
- /* Current macOS SDK stubs advertise arm64e for system dylibs even
- * when an ordinary arm64 consumer is being linked. ld64 accepts that
- * ABI-compatible provider, so Kit must not reject the SDK's only ARM
- * slice before symbol ingestion. */
- if (c->target.arch == KIT_ARCH_ARM_64 &&
- i + 6u + platform_len <= end &&
- (i == line || !is_target_cont(data[i - 1u])) &&
- memcmp(data + i, "arm64e", 6u) == 0 &&
- memcmp(data + i + 6u, platform, platform_len) == 0 &&
- (i + 6u + platform_len == end ||
- !is_target_cont(data[i + 6u + platform_len])))
- return 1;
- }
- return 0;
+ return tbd_line_has_target(
+ data, line + sizeof(TARGETS_KEY) - 1u, end, arch, arch_len, platform,
+ platform_len, c->target.arch == KIT_ARCH_ARM_64);
+ }
+ if (end - line >= sizeof(ARCHS_KEY) - 1u &&
+ memcmp(data + line, ARCHS_KEY, sizeof(ARCHS_KEY) - 1u) == 0) {
+ size_t value = line + sizeof(ARCHS_KEY) - 1u;
+ saw_archs = 1;
+ arch_matches =
+ tbd_line_has_token(data, value, end, arch, arch_len) ||
+ (c->target.arch == KIT_ARCH_ARM_64 &&
+ tbd_line_has_token(data, value, end, "arm64e", 6u));
+ }
+ if (end - line >= sizeof(PLATFORM_KEY) - 1u &&
+ memcmp(data + line, PLATFORM_KEY, sizeof(PLATFORM_KEY) - 1u) == 0) {
+ size_t value = line + sizeof(PLATFORM_KEY) - 1u;
+ saw_platform = 1;
+ platform_matches = tbd_line_has_token(
+ data, value, end, legacy_platform, legacy_platform_len);
+ if (!platform_matches && c->target.os == KIT_OS_MACOS)
+ platform_matches =
+ tbd_line_has_token(data, value, end, "macos", 5u);
}
while (end < len && (data[end] == '\n' || data[end] == '\r')) ++end;
line = end;
}
- return 1;
+ *declared_out = saw_archs || saw_platform;
+ return (!saw_archs || arch_matches) &&
+ (!saw_platform || platform_matches);
}
/* Extract the install-name from the first document. We look for a
diff --git a/test/link/link_compat_test.c b/test/link/link_compat_test.c
@@ -458,6 +458,27 @@ static void check_macho_dso_boundary(void) {
" - targets: [ arm64e-macos ]\n"
" symbols: [ _audit_import ]\n"
"...\n";
+ static const uint8_t tbd_v3_x64[] =
+ "--- !tapi-tbd-v3\n"
+ "archs: [ x86_64 ]\n"
+ "platform: macosx\n"
+ "install-name: '/usr/lib/libAudit.dylib'\n"
+ "exports:\n"
+ " - archs: [ x86_64 ]\n"
+ " symbols: [ _audit_import ]\n"
+ "...\n";
+ static const uint8_t tbd_v3_arm64[] =
+ "--- !tapi-tbd-v3\n"
+ "archs: [ arm64 ]\n"
+ "platform: macosx\n"
+ "install-name: '/usr/lib/libAudit.dylib'\n"
+ "...\n";
+ static const uint8_t tbd_v3_ios[] =
+ "--- !tapi-tbd-v3\n"
+ "archs: [ x86_64 ]\n"
+ "platform: ios\n"
+ "install-name: '/usr/lib/libAudit.dylib'\n"
+ "...\n";
KitCompiler* expected = NULL;
KitCompiler* elf_expected = NULL;
KitCompiler* simulator_expected = NULL;
@@ -476,6 +497,12 @@ static void check_macho_dso_boundary(void) {
.len = sizeof(tbd_ios_simulator) - 1u};
KitSlice arm64e_tbd = {.data = tbd_arm64e,
.len = sizeof(tbd_arm64e) - 1u};
+ KitSlice v3_native_tbd = {.data = tbd_v3_x64,
+ .len = sizeof(tbd_v3_x64) - 1u};
+ KitSlice v3_arch_tbd = {.data = tbd_v3_arm64,
+ .len = sizeof(tbd_v3_arm64) - 1u};
+ KitSlice v3_platform_tbd = {.data = tbd_v3_ios,
+ .len = sizeof(tbd_v3_ios) - 1u};
EXPECT(kit_unit_compiler_new(
&g_u,
@@ -593,6 +620,45 @@ static void check_macho_dso_boundary(void) {
}
s = new_reloc_session(expected);
+ EXPECT(s != NULL, "matching TAPI v3 session");
+ if (s) {
+ EXPECT(kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libnative-v3.tbd"), &v3_native_tbd) ==
+ KIT_OK,
+ "matching TAPI v3 archs/platform accepted");
+ kit_link_session_free(s);
+ s = NULL;
+ }
+
+ s = new_reloc_session(expected);
+ EXPECT(s != NULL, "architecture-mismatched TAPI v3 session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libforeign-arch-v3.tbd"), &v3_arch_tbd);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign-architecture TAPI v3 rejected");
+ EXPECT(strstr(g_u.last_diag, "libforeign-arch-v3.tbd") != NULL,
+ "TAPI v3 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 TAPI v3 session");
+ if (s) {
+ g_u.suppress_fatal = 1;
+ st = kit_link_session_add_dso_bytes(
+ s, KIT_SLICE_LIT("libforeign-platform-v3.tbd"), &v3_platform_tbd);
+ g_u.suppress_fatal = 0;
+ EXPECT(st == KIT_ERR, "foreign-platform TAPI v3 rejected");
+ EXPECT(strstr(g_u.last_diag, "libforeign-platform-v3.tbd") != NULL,
+ "TAPI v3 platform mismatch is named: %s", g_u.last_diag);
+ 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;