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:
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");