kit

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

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:
Mdriver/cmd/build.c | 35++++++++++++++++++++++-------------
Msrc/link/link_layout.c | 22+++++++---------------
Mtest/buildcmds/run.sh | 6++++--
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