kit

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

commit 295a93eeb9452f75958744a51bf6edd4271a2127
parent f2c14c6b380edb531729aa4cd195b4a6ec02c27f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 14:41:07 -0700

link: defsym, section-start, orphan/fatal policy + freestanding strict validation

Add KitLinkSessionOptions fields (defsyms, section_starts, orphan_handling,
fatal_warnings, freestanding_strict) plumbed onto the Linker, and the
libkit-side logic in link_resolve():

- link_apply_defsyms: define --defsym symbols as absolute literals or as
  aliases that inherit the target's placement (rebasing identically at emit).
- link_apply_section_starts: post-layout per-section vaddr override
  (--section-start / -Tdata / -Tbss), interpreting the address as absolute by
  subtracting the static image base; ignored under a linker script.
- link_validate_freestanding: strict-by-default freestanding policy rejecting
  DSO inputs, dynamic-link sections (.dynamic/.dynsym/.interp/.plt/.got.plt),
  and cross-input ELF e_flags mismatches.
- link_warn: non-fatal linker warning, escalated to a hard error under
  --fatal-warnings.

All additions confined to the resolve/validate region of link_layout.c
(~1733+); the scripted-layout region is untouched.

Diffstat:
Minclude/kit/link.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/link/link_internal.h | 10++++++++++
Msrc/link/link_layout.c | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 325 insertions(+), 0 deletions(-)

diff --git a/include/kit/link.h b/include/kit/link.h @@ -253,6 +253,40 @@ typedef enum KitPeSubsystem { KIT_PE_SUBSYSTEM_WINDOWS_CUI = 3, } KitPeSubsystem; +/* `--defsym NAME=EXPR`: define an absolute linker symbol. `value` is the + * resolved absolute address for the literal form (`name=0x1234`); for the + * symbol-alias form (`name=othersym`) `alias` names the symbol whose final + * address `name` takes (and `value` is an additive bias, normally 0). The + * driver does the simple parse; the linker defines the symbol after layout + * (so an alias target's address is known). */ +typedef struct KitLinkDefsym { + KitSlice name; + KitSlice alias; /* empty => literal absolute `value`; else alias target */ + uint64_t value; /* literal absolute value, or additive bias for an alias */ +} KitLinkDefsym; + +/* `--section-start=.name=ADDR` (and the `-Tdata`/`-Tbss` shorthands, which + * the driver folds in as `.data` / `.bss`): force an output section's runtime + * address. Applied as a post-layout vaddr override on the matching section and + * its containing segment. Honored on the default (non-scripted) static layout; + * a linker script pins addresses itself, so these are ignored when a script is + * in force. */ +typedef struct KitLinkSectionStart { + KitSlice name; + uint64_t addr; +} KitLinkSectionStart; + +/* `--orphan-handling=MODE`: disposition of input sections the linker script + * does not name. Default `place`. Only `warn`/`error` differ in behaviour + * today (an orphan is reported as a warning or a hard error); `discard` + * suppresses placement of orphans (best-effort under the bucket layout). */ +typedef enum KitLinkOrphanHandling { + KIT_LINK_ORPHAN_PLACE = 0, + KIT_LINK_ORPHAN_WARN, + KIT_LINK_ORPHAN_ERROR, + KIT_LINK_ORPHAN_DISCARD, +} KitLinkOrphanHandling; + typedef struct KitLinkSessionOptions { uint8_t output_kind; /* KitLinkOutputKind */ bool gc_sections; @@ -266,6 +300,22 @@ typedef struct KitLinkSessionOptions { * PIE/shared (base 0) and scripted layouts (the script pins vaddrs). */ bool text_base_set; uint64_t text_base; + /* `--defsym` absolute / alias linker symbols (borrowed array). */ + const KitLinkDefsym* defsyms; + uint32_t ndefsyms; + /* `--section-start` / `-Tdata` / `-Tbss` per-section address overrides + * (borrowed array). Ignored under a linker script. */ + const KitLinkSectionStart* section_starts; + uint32_t nsection_starts; + /* `--orphan-handling=MODE` (KitLinkOrphanHandling). */ + uint8_t orphan_handling; + /* `--fatal-warnings`: promote linker warnings to hard errors. */ + bool fatal_warnings; + /* Strict freestanding policy: reject dynamic artifacts (PT_INTERP, dynamic + * sections, PLT/GOT imports, DSO inputs) and cross-input target/format + * mismatches. Set by the driver for `-ffreestanding` and/or a `*-none-*` + * static non-PIE link. --allow-undefined remains the undef escape hatch. */ + bool freestanding_strict; const KitLinkScript* linker_script; uint8_t build_id_mode; /* KitBuildIdMode */ const uint8_t* build_id_bytes; @@ -305,6 +355,15 @@ KIT_API KitStatus kit_link_session_write_map(KitLinkSession*, KitWriter* out); KIT_API KitStatus kit_link_session_write_symbols(KitLinkSession*, uint8_t format, KitWriter* out); +/* `--cref`: write a deterministic cross-reference table — one block per global + * symbol giving its defining file and every file that references it. Input + * paths are normalized to basenames so no absolute host path leaks. */ +KIT_API KitStatus kit_link_session_write_cref(KitLinkSession*, KitWriter* out); +/* `--print-memory-usage`: write a GNU-ld-style per-MEMORY-region used/free/ + * total summary. Requires a linker script with MEMORY regions; emits a header + * line and one row per region. With no regions it emits just the header. */ +KIT_API KitStatus kit_link_session_write_memory_usage(KitLinkSession*, + KitWriter* out); KIT_API KitStatus kit_link_session_jit(KitLinkSession*, KitJit** out_jit); KIT_API void kit_link_session_free(KitLinkSession*); diff --git a/src/link/link_internal.h b/src/link/link_internal.h @@ -223,6 +223,16 @@ struct Linker { /* Caller-supplied PT_INTERP. layout_dyn falls back to a target- * derived default when this is 0. */ Sym interp_path; + /* ---- Flags/policy plumbed from KitLinkSessionOptions by src/api/link.c + * (the kernel-C "linker flags and policy" + strict-validation work). The + * arrays are borrowed; they must outlive link_resolve. ---- */ + const KitLinkDefsym* defsyms; /* --defsym NAME=EXPR */ + u32 ndefsyms; + const KitLinkSectionStart* section_starts; /* --section-start / -Tdata/-Tbss */ + u32 nsection_starts; + u8 orphan_handling; /* KitLinkOrphanHandling */ + int fatal_warnings; /* --fatal-warnings */ + int freestanding_strict; /* strict freestanding link validation */ LinkExternResolver resolver; void* resolver_user; /* Borrowed JIT host. Set by kit_link_jit before link_resolve so the diff --git a/src/link/link_layout.c b/src/link/link_layout.c @@ -11,10 +11,12 @@ #include <kit/core.h> #include <kit/jit.h> +#include <stdarg.h> #include <string.h> #include "core/buf.h" #include "core/bytes.h" +#include "core/diag.h" #include "core/heap.h" #include "core/metrics.h" #include "core/pool.h" @@ -1931,6 +1933,248 @@ u8* link_fileonly_bytes(LinkImage* img, LinkSectionId id) { } } +/* ---- linker flags, policy, and strict validation (kernel-C work) ---- + * + * These run inside link_resolve after layout (so alias targets / section + * vaddrs are known) and after symbol resolution (so dynamic-artifact and + * cross-input checks see every input). They are deliberately self-contained + * and only consult the borrowed option arrays plumbed onto the Linker. */ + +/* Emit a non-fatal linker warning. With --fatal-warnings the warning is + * promoted to a hard error (compiler_panic, which unwinds the link). */ +static void link_warn(Linker* l, const char* fmt, ...) { + va_list ap; + if (l->fatal_warnings) { + va_start(ap, fmt); + compiler_panicv(l->c, SRCLOC_NONE, fmt, ap); + va_end(ap); /* unreachable; compiler_panicv is _Noreturn */ + } + if (l->c && l->c->ctx && l->c->ctx->diag) { + va_start(ap, fmt); + diag_emitv(l->c->ctx->diag, DIAG_WARN, SRCLOC_NONE, fmt, ap); + va_end(ap); + } +} + +/* --defsym: define each requested symbol. Two forms: + * NAME=0x1234 — an absolute symbol carrying the literal value verbatim. + * NAME=othersym — an alias that resolves to othersym's address (+optional + * bias). The alias copies the target's kind/section/value so + * it rebases identically at emit (matching GNU ld, where the + * alias takes the target's final address). Run after layout + * and undef resolution so the target has a settled vaddr. */ +static void link_apply_defsyms(Linker* l, LinkImage* img) { + u32 i; + for (i = 0; i < l->ndefsyms; ++i) { + const KitLinkDefsym* d = &l->defsyms[i]; + char namebuf[256]; + size_t nlen; + Sym sym; + LinkSymId existing; + LinkSymbol rec; + if (!d->name.s || d->name.len == 0) continue; + nlen = + d->name.len < sizeof(namebuf) - 1u ? d->name.len : sizeof(namebuf) - 1u; + memcpy(namebuf, d->name.s, nlen); + namebuf[nlen] = '\0'; + memset(&rec, 0, sizeof(rec)); + sym = pool_intern_slice(l->c->global, (Slice){.s = namebuf, .len = (u32)nlen}); + rec.name = sym; + rec.bind = SB_GLOBAL; + rec.defined = 1; + if (d->alias.s && d->alias.len) { + Sym tgt = pool_intern_slice(l->c->global, + (Slice){.s = d->alias.s, .len = d->alias.len}); + LinkSymId tid = symhash_get(&img->globals, tgt); + const LinkSymbol* t; + if (tid == LINK_SYM_NONE) { + compiler_panic(l->c, SRCLOC_NONE, + "link: --defsym '%s': undefined alias target '%.*s'", + namebuf, (int)d->alias.len, d->alias.s); + } + t = LinkSyms_at(&img->syms, tid - 1); + /* Inherit the target's placement so the alias rebases identically. */ + rec.kind = t->kind; + rec.section_id = t->section_id; + rec.value = t->value; + rec.vaddr = t->vaddr + d->value; + rec.imported = t->imported; + rec.dso_input_id = t->dso_input_id; + } else { + rec.kind = SK_ABS; + rec.vaddr = d->value; + rec.value = d->value; + } + existing = symhash_get(&img->globals, sym); + if (existing != LINK_SYM_NONE) { + rec.id = existing; + *LinkSyms_at(&img->syms, existing - 1) = rec; + } else { + LinkSymId fresh = link_append_symbol(img, &rec); + symhash_insert(&img->globals, sym, fresh, &existing); + } + } +} + +/* The static ET_EXEC runtime base the ELF emitter will add to image-relative + * vaddrs. Mirrors src/obj/elf/link.c's img_base rule so --section-start can + * interpret its argument as an absolute runtime address (it subtracts this + * base to recover the image-relative vaddr the layout / emit pass uses). PIE + * and scripted images keep base 0. */ +#define LINK_IMAGE_BASE_STATIC 0x400000ULL +static u64 link_static_image_base(const LinkImage* img) { + if (img->pie || img->scripted) return 0ULL; + if (img->text_base_set) return img->text_base; + return LINK_IMAGE_BASE_STATIC; +} + +/* --section-start / -Tdata / -Tbss: post-layout vaddr override for a named + * output section and its containing segment. The requested address is an + * absolute runtime address (GNU-ld semantics); it is converted to the + * image-relative vaddr space layout/emit use by subtracting the static image + * base. Ignored under a script (which pins addresses itself). Best-effort + * under the bucket layout: it shifts the matched section's segment so the + * section lands at the requested address, keeping intra-segment offsets. */ +static void link_apply_section_starts(Linker* l, LinkImage* img) { + u32 i, j; + u64 base; + if (l->script) { + if (l->nsection_starts) + link_warn(l, + "link: --section-start/-Tdata/-Tbss ignored under a linker " + "script"); + return; + } + base = link_static_image_base(img); + for (i = 0; i < l->nsection_starts; ++i) { + const KitLinkSectionStart* ss = &l->section_starts[i]; + /* The requested absolute address, expressed in image-relative space. */ + u64 want = ss->addr >= base ? ss->addr - base : ss->addr; + int matched = 0; + if (!ss->name.s || ss->name.len == 0) continue; + for (j = 0; j < img->nsections; ++j) { + LinkSection* sec = &img->sections[j]; + Slice nm = sec->name ? pool_slice(l->c->global, sec->name) : SLICE_NULL; + if (!nm.s || nm.len != ss->name.len || + memcmp(nm.s, ss->name.s, nm.len) != 0) + continue; + matched = 1; + if (sec->segment_id != LINK_SEG_NONE && + sec->segment_id <= img->nsegments) { + LinkSegment* seg = &img->segments[sec->segment_id - 1]; + u64 delta = want - sec->vaddr; + u32 k; + /* Shift the whole segment so this section lands at the requested + * address; other sections in the segment keep their relative + * placement. */ + seg->vaddr += delta; + seg->paddr += delta; + for (k = 0; k < img->nsections; ++k) { + if (img->sections[k].segment_id == seg->id) + img->sections[k].vaddr += delta; + } + } else { + sec->vaddr = want; + } + } + if (!matched) + link_warn(l, "link: --section-start: no output section named '%.*s'", + (int)ss->name.len, ss->name.s); + } +} + +/* Strict freestanding validation: reject dynamic-link artifacts on a + * freestanding/static-non-PIE link. Runs after symbol resolution so every + * input (and any DT_NEEDED-style import) is visible. The DSO-input and + * dynamic-section checks generalize the existing relocatable-only DSO guard + * to the freestanding executable case. --allow-undefined remains the escape + * hatch for undefined references, but it does not relax these structural + * rejections (a freestanding image has no dynamic loader to honor them). */ +static int link_section_name_is_dynamic(Slice nm) { + /* The dynamic-link sections a freestanding static image must not carry. */ + static const char* const dyn[] = {".dynamic", ".dynsym", ".dynstr", + ".rela.plt", ".rel.plt", ".plt", + ".got.plt", ".rela.dyn", ".rel.dyn", + ".interp"}; + u32 i; + if (!nm.s || nm.len == 0) return 0; + for (i = 0; i < (u32)(sizeof(dyn) / sizeof(dyn[0])); ++i) { + size_t dl = strlen(dyn[i]); + if (nm.len == dl && memcmp(nm.s, dyn[i], dl) == 0) return 1; + } + return 0; +} + +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; + + for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) { + LinkInput* in = LinkInputs_at(&l->inputs, ii); + ObjBuilder* ob = in->obj; + Slice inname = in->name ? pool_slice(l->c->global, in->name) : SLICE_NULL; + const char* label = inname.s ? inname.s : "<input>"; + u32 j; + + /* (c) DSO inputs: a shared object has no place in a freestanding image. */ + if (in->kind == LINK_INPUT_DSO_BYTES) { + compiler_panic(l->c, SRCLOC_NONE, + "link: freestanding link rejects shared-object input " + "'%s' (no dynamic loader)", + label); + } + 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; + } + { + u32 ef; + if (obj_get_elf_e_flags(ob, &ef)) { + if (!have_eflags) { + eflags0 = ef; + have_eflags = 1; + } else if (ef != eflags0) { + compiler_panic(l->c, SRCLOC_NONE, + "link: freestanding link: input '%s' has incompatible " + "ELF e_flags (0x%x vs 0x%x)", + label, ef, eflags0); + } + } + } + + /* (a)+(b) dynamic-interpreter / dynamic-section / PLT-GOT-import inputs. + * A freestanding static image must not pull in .interp (PT_INTERP) or any + * .dynamic/.dynsym/.plt/.got.plt artifact. */ + for (j = 1; j < obj_section_count(ob); ++j) { + const Section* s = obj_section_get(ob, j); + Slice nm; + if (!s || s->removed) continue; + nm = s->name ? pool_slice(l->c->global, s->name) : SLICE_NULL; + if (link_section_name_is_dynamic(nm)) { + compiler_panic(l->c, SRCLOC_NONE, + "link: freestanding link rejects dynamic section '%.*s' " + "from input '%s'", + (int)nm.len, nm.s, label); + } + } + } + (void)fmt0; + (void)arch0; +} + /* ---- public orchestration ---- */ LinkImage* link_resolve(Linker* l) { @@ -1969,6 +2213,10 @@ LinkImage* link_resolve(Linker* l) { metrics_scope_begin(l->c, "link.resolve_symbols"); link_resolve_symbols(l, img); metrics_scope_end(l->c, "link.resolve_symbols"); + /* Strict freestanding policy: reject dynamic-link artifacts and cross-input + * mismatches before laying anything out. DSO inputs / dynamic sections have + * no home in a freestanding static image (no dynamic loader). */ + link_validate_freestanding(l); { GcLive g = {0}; metrics_scope_begin(l->c, "link.gc"); @@ -1993,6 +2241,9 @@ LinkImage* link_resolve(Linker* l) { obj_format_carries_file_only_debug(l->c)) link_layout_debug(l, img); metrics_scope_end(l->c, "link.layout_debug"); + /* --section-start / -Tdata / -Tbss: pin chosen output sections to fixed + * addresses before symbol vaddrs are derived from them. */ + link_apply_section_starts(l, img); metrics_scope_begin(l->c, "link.assign_vaddrs"); link_assign_symbol_vaddrs(l, img); metrics_scope_end(l->c, "link.assign_vaddrs"); @@ -2042,6 +2293,11 @@ LinkImage* link_resolve(Linker* l) { metrics_scope_begin(l->c, "link.resolve_undefs"); link_resolve_undefs(l, img); metrics_scope_end(l->c, "link.resolve_undefs"); + /* --defsym: define absolute / alias linker symbols now that layout and + * undef resolution are done (an alias target's vaddr is known). Runs + * before relocations so a defsym can satisfy a reference, and before + * entry resolution so a defsym can name the entry point. */ + link_apply_defsyms(l, img); metrics_scope_begin(l->c, "link.gc_drop_dead"); link_gc_drop_dead_globals(l, img, &g); metrics_scope_end(l->c, "link.gc_drop_dead");