commit 8dbe9e8841b691663cce45b6a526d2a9906109eb
parent 4a2ca1694db800696de3b45245d3b33f277d715c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 15:21:58 -0700
link: align freestanding arch/format guard with strict trigger (Bug 5)
build.c set freestanding_strict for two triggers (a *-none-* target OR a
hosted -ffreestanding static non-PIE exe) but ran the per-input arch/format
guard only for the *-none-* one, so the hosted -ffreestanding -static -no-pie
path set strict yet silently accepted a foreign-arch object. Hoist the strict
decision to a single freestanding_strict boolean and gate BOTH the per-input
arch/format guard and lopts.freestanding_strict on it. The non-freestanding
-static path is unaffected (no false positive).
link_validate_freestanding documented a cross-input arch/format check it never
performed (it captured fmt0/arch0 then discarded them with (void) casts; only
ELF e_flags equality, which is NOT e_machine, was checked). Remove the dead
capture and correct the comment: the per-input arch/format match is the
driver's job (it has each input's detected target); libkit checks structural
dynamic-artifact rejection + ELF e_flags ABI compatibility.
Tests: fx2-fs-hosted-* assert the hosted -ffreestanding path rejects a
cross-arch input and still accepts a same-arch one.
Diffstat:
3 files changed, 33 insertions(+), 30 deletions(-)
diff --git a/driver/cmd/build.c b/driver/cmd/build.c
@@ -1597,6 +1597,19 @@ static int build_run_link(BuildOptions* o, KitCompiler* compiler,
uint32_t i;
uint32_t norder = 0;
int rc = 1;
+ /* Strict-by-default freestanding policy: a `*-none-*` target, or an explicit
+ * -ffreestanding static non-PIE executable, must reject dynamic-link
+ * artifacts and cross-input target/format mismatches. Computed once here (the
+ * pie value mirrors what driver_link_flags_fill_options derives for lopts.pie)
+ * so the per-input arch/format guard below and lopts.freestanding_strict
+ * agree on the trigger — otherwise the hosted -ffreestanding path would set
+ * strict yet skip the guard. The relocatable partial-link lane never imposes
+ * it. */
+ int freestanding_strict =
+ output_kind == KIT_LINK_OUTPUT_EXE &&
+ (o->target.os == KIT_OS_FREESTANDING ||
+ (o->freestanding && o->static_link &&
+ !driver_link_pie(o->target, o->pie, o->shared, 0)));
if (o->nsources) {
objs = driver_alloc_zeroed(env, o->nsources * sizeof(*objs));
@@ -1638,10 +1651,11 @@ static int build_run_link(BuildOptions* o, KitCompiler* compiler,
obj_names[i] = kit_slice_cstr(o->inputs.object_files[i]);
}
/* Strict freestanding policy: pre-built object inputs must agree with the
- * link target's arch and object format (a `*-none-*` exe link must not
- * silently accept a foreign-arch object). */
- if (output_kind == KIT_LINK_OUTPUT_EXE &&
- o->target.os == KIT_OS_FREESTANDING) {
+ * link target's arch and object format (a freestanding exe link must not
+ * silently accept a foreign-arch object). Gated on the same trigger as
+ * lopts.freestanding_strict so the hosted -ffreestanding -static -no-pie path
+ * runs this guard too (not only the `*-none-*` target). */
+ if (freestanding_strict) {
for (i = 0; i < o->inputs.nobject_files; ++i) {
KitTargetSpec t;
if (kit_detect_target(obj_in[i].data, obj_in[i].len, &t) != KIT_OK)
@@ -1717,15 +1731,10 @@ static int build_run_link(BuildOptions* o, KitCompiler* compiler,
output_kind == KIT_LINK_OUTPUT_RELOCATABLE, output_kind, script,
&lopts, &rpath_slices) != 0)
goto out;
- /* Strict-by-default freestanding policy: a `*-none-*` target, or an
- * explicit -ffreestanding static non-PIE executable, must reject
- * dynamic-link artifacts (PT_INTERP, .dynamic/.dynsym, PLT/GOT imports,
- * DSO inputs) and cross-input target/format mismatches. The relocatable
- * partial-link lane never imposes it. */
- if (output_kind == KIT_LINK_OUTPUT_EXE &&
- (o->target.os == KIT_OS_FREESTANDING ||
- (o->freestanding && !lopts.pie && o->static_link)))
- lopts.freestanding_strict = true;
+ /* Reject dynamic-link artifacts (PT_INTERP, .dynamic/.dynsym, PLT/GOT
+ * imports, DSO inputs) for the strict freestanding link. Same trigger as
+ * the per-input arch/format guard above (freestanding_strict). */
+ if (freestanding_strict) lopts.freestanding_strict = true;
memset(&li, 0, sizeof(li));
li.objs = objs;
li.nobjs = nobjs;
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -2129,9 +2129,6 @@ static int link_section_name_is_dynamic(Slice nm) {
static void link_validate_freestanding(Linker* l) {
u32 ii;
- int have_fmt = 0, have_arch = 0;
- KitObjFmt fmt0 = KIT_OBJ_ELF;
- KitArchKind arch0 = KIT_ARCH_X86_64;
u32 eflags0 = 0;
int have_eflags = 0;
if (!l->freestanding_strict) return;
@@ -2152,16 +2149,13 @@ static void link_validate_freestanding(Linker* l) {
}
if (!ob) continue;
- /* (d) cross-input target / object-format mismatch. The link target is
- * authoritative; reject any input whose format/arch/e_flags disagree. */
- if (!have_fmt) {
- fmt0 = l->c->target.obj;
- have_fmt = 1;
- }
- if (!have_arch) {
- arch0 = l->c->target.arch;
- have_arch = 1;
- }
+ /* (d) cross-input ABI mismatch. The per-input arch / object-format match
+ * against the link target is enforced by the driver (build.c / ld.c), which
+ * has each input's detected KitTargetSpec via kit_detect_target; libkit
+ * sees only the format-neutral ObjBuilders here. What libkit can still check
+ * cheaply is ELF e_flags compatibility across inputs (RISC-V float-ABI /
+ * RVC bits; 0 for x86_64/aarch64) — a mismatch is a genuine ABI conflict
+ * even within a single arch. */
{
u32 ef;
if (obj_get_elf_e_flags(ob, &ef)) {
@@ -2193,8 +2187,6 @@ static void link_validate_freestanding(Linker* l) {
}
}
}
- (void)fmt0;
- (void)arch0;
}
/* ---- public orchestration ---- */
diff --git a/test/buildcmds/run.sh b/test/buildcmds/run.sh
@@ -755,8 +755,10 @@ assert_file_exists fx2-noload-reloc-file fx2_noload.elf
# build.c sets freestanding_strict for BOTH the *-none-* target AND the hosted
# -ffreestanding static-non-PIE trigger; the per-input arch/format guard must
# run for the hosted trigger too (else a silent mis-link of a foreign-arch .o).
+# fsother.c (defined earlier) provides only `other()`, so it can be linked
+# alongside kstart.c's `_start` without a duplicate-symbol clash.
run_ok fx2-fs-hosted-x64 "$KIT" build-obj -target x86_64-linux -ffreestanding \
- -nostdlib kstart.c -o fx2_fs_x64.o
+ -nostdlib fsother.c -o fx2_fs_x64.o
run_ok fx2-fs-hosted-aa64 "$KIT" build-obj -target aarch64-linux \
-ffreestanding -nostdlib fsother.c -o fx2_fs_aa64.o
run_fail fx2-fs-hosted-cross "$KIT" build-exe -target x86_64-linux \
@@ -766,7 +768,7 @@ contains fx2-fs-hosted-cross-diag "$work/fx2-fs-hosted-cross.err" \
"does not match the link target"
# Same-arch hosted freestanding link still succeeds (no false positive).
run_ok fx2-fs-hosted-same "$KIT" build-exe -target x86_64-linux -ffreestanding \
- -nostdlib -static -no-pie kstart.c fx2_fs_x64.o -o fx2_fs_same.elf
+ -nostdlib -static -no-pie -e _start kstart.c fx2_fs_x64.o -o fx2_fs_same.elf
kit_summary build-driver
kit_exit