kit

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

commit 8dea968324928d6a77b54be45cb1037097f109d8
parent 4cff1a6f663d2b9cc7efb6e2cf4305b38ca840db
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 16 Jul 2026 10:11:23 -0700

link: enforce target compatibility and canonical symbols

Diffstat:
Msrc/api/link.c | 6++++--
Msrc/link/link.c | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/link/link_internal.h | 32++++++++++++++++++++++++++++++++
Msrc/link/link_layout.c | 26++------------------------
Msrc/link/link_relocatable.c | 13+------------
Msrc/link/link_resolve.c | 46++++++++++++++++++++++++++++++++++------------
Msrc/obj/elf/link.c | 21+++++++++------------
Msrc/obj/macho/link.c | 1+
Mtest/link/harness/link_exe_runner.c | 36+++++++++++++++++++++++++++++++++++-
Atest/link/link_compat_test.c | 385+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/link/macho-symbols.sh | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/link/macho_symbols/aliases.S | 7+++++++
Atest/link/macho_symbols/definition.c | 1+
Atest/link/macho_symbols/reference_a.c | 9+++++++++
Atest/link/macho_symbols/reference_b.c | 3+++
15 files changed, 773 insertions(+), 63 deletions(-)

diff --git a/src/api/link.c b/src/api/link.c @@ -625,9 +625,11 @@ static int link_report_slice_cmp(KitSlice a, KitSlice b) { return 0; } -static int link_report_symbol_include(const LinkSymbol* s) { +static int link_report_symbol_include(const LinkImage* img, + const LinkSymbol* s) { if (!s || !s->name || !s->defined || s->imported) return 0; if (s->kind == SK_FILE || s->kind == SK_SECTION) return 0; + if (s->bind != SB_LOCAL && !link_symbol_is_canonical_global(img, s)) return 0; return 1; } @@ -661,7 +663,7 @@ static KitStatus link_report_collect_symbols(LinkImage* img, if (!out->ids) return KIT_NOMEM; for (i = 0; i < out->cap; ++i) { const LinkSymbol* s = LinkSyms_at(&img->syms, i); - if (link_report_symbol_include(s)) out->ids[out->n++] = s->id; + if (link_report_symbol_include(img, s)) out->ids[out->n++] = s->id; } for (i = 1; i < out->n; ++i) { LinkSymId key = out->ids[i]; diff --git a/src/link/link.c b/src/link/link.c @@ -15,6 +15,7 @@ #include <kit/archive.h> #include <kit/core.h> #include <kit/object.h> +#include <kit/target.h> #include <string.h> #include "core/heap.h" @@ -22,8 +23,162 @@ #include "core/slice.h" #include "core/vec.h" #include "link/link_internal.h" +#include "obj/elf/elf.h" #include "obj/format.h" +static const char* link_float_abi_name(KitFloatAbi abi) { + switch (abi) { + case KIT_FLOAT_ABI_DEFAULT: + return "default"; + case KIT_FLOAT_ABI_SOFT: + return "soft"; + case KIT_FLOAT_ABI_SINGLE: + return "single"; + case KIT_FLOAT_ABI_DOUBLE: + return "double"; + } + return "unknown"; +} + +/* OS compatibility is deliberately directional: an ELF object carrying the + * generic/Linux OSABI may be consumed by Linux, Android, or a freestanding + * image, because that header value is what assemblers use for otherwise + * platform-neutral relocatables. A FreeBSD object or a Mach-O platform stamp + * is never borrowed across operating systems. */ +static int link_target_os_compatible(KitTargetSpec expected, + KitTargetSpec actual) { + if (expected.os == actual.os) return 1; + if (expected.obj != KIT_OBJ_ELF || actual.os != KIT_OS_LINUX) return 0; + return expected.os == KIT_OS_ANDROID || + expected.os == KIT_OS_FREESTANDING; +} + +static const char* link_target_mismatch(KitTargetSpec expected, + KitTargetSpec actual) { + if (expected.arch != actual.arch) return "architecture"; + if (expected.obj != actual.obj) return "object format"; + if (expected.ptr_size != actual.ptr_size) return "pointer size"; + if (expected.big_endian != actual.big_endian) return "endianness"; + if (!link_target_os_compatible(expected, actual)) return "operating system"; + if ((expected.arch == KIT_ARCH_RV32 || expected.arch == KIT_ARCH_RV64) && + expected.float_abi != KIT_FLOAT_ABI_DEFAULT && + actual.float_abi != KIT_FLOAT_ABI_DEFAULT && + expected.float_abi != actual.float_abi) + return "float ABI"; + return NULL; +} + +static void link_target_panic(Linker* l, Slice name, Slice container, + KitTargetSpec actual, const char* reason) { + KitTargetSpec expected = l->c->target; + if (container.s) { + compiler_panic( + l->c, SRCLOC_NONE, + "link: incompatible input '%.*s(%.*s)': %s mismatch; expected " + "arch=%s os=%s format=%s ptr=%u endian=%s float=%s, got " + "arch=%s os=%s format=%s ptr=%u endian=%s float=%s", + SLICE_ARG(container), SLICE_ARG(name), reason, + kit_target_arch_name(expected.arch), kit_target_os_name(expected.os), + kit_target_obj_name(expected.obj), (u32)expected.ptr_size, + expected.big_endian ? "big" : "little", + link_float_abi_name((KitFloatAbi)expected.float_abi), + kit_target_arch_name(actual.arch), kit_target_os_name(actual.os), + kit_target_obj_name(actual.obj), (u32)actual.ptr_size, + actual.big_endian ? "big" : "little", + link_float_abi_name((KitFloatAbi)actual.float_abi)); + } + compiler_panic( + l->c, SRCLOC_NONE, + "link: incompatible input '%.*s': %s mismatch; expected " + "arch=%s os=%s format=%s ptr=%u endian=%s float=%s, got " + "arch=%s os=%s format=%s ptr=%u endian=%s float=%s", + SLICE_ARG(name), reason, kit_target_arch_name(expected.arch), + kit_target_os_name(expected.os), kit_target_obj_name(expected.obj), + (u32)expected.ptr_size, expected.big_endian ? "big" : "little", + link_float_abi_name((KitFloatAbi)expected.float_abi), + kit_target_arch_name(actual.arch), kit_target_os_name(actual.os), + kit_target_obj_name(actual.obj), (u32)actual.ptr_size, + actual.big_endian ? "big" : "little", + link_float_abi_name((KitFloatAbi)actual.float_abi)); +} + +static void link_validate_target(Linker* l, Slice name, Slice container, + KitTargetSpec actual) { + const char* reason = link_target_mismatch(l->c->target, actual); + if (reason) link_target_panic(l, name, container, actual, reason); +} + +static void link_validate_obj_target(Linker* l, Slice name, ObjBuilder* ob) { + Compiler* source = obj_compiler(ob); + if (!source) + compiler_panic(l->c, SRCLOC_NONE, + "link: input '%.*s' has no target-owning compiler", + SLICE_ARG(name)); + link_validate_target(l, name, SLICE_NULL, source->target); +} + +static void link_validate_bytes_target(Linker* l, Slice name, Slice container, + const u8* data, size_t len, + int allow_text_dso) { + KitTargetSpec actual; + KitStatus st = kit_detect_target(data, len, &actual); + if (st == KIT_OK) { + link_validate_target(l, name, container, actual); + return; + } + if (allow_text_dso && kit_detect_fmt(data, len) == KIT_BIN_UNKNOWN) return; + if (container.s) + compiler_panic(l->c, SRCLOC_NONE, + "link: cannot determine target of input '%.*s(%.*s)'", + SLICE_ARG(container), SLICE_ARG(name)); + compiler_panic(l->c, SRCLOC_NONE, + "link: cannot determine target of input '%.*s'", + SLICE_ARG(name)); +} + +void link_merge_elf_e_flags(Linker* l, ObjBuilder* ob, KitSlice label) { + u32 incoming; + u32 known = EF_RISCV_RVC | EF_RISCV_FLOAT_ABI_MASK | EF_RISCV_RVE | + EF_RISCV_TSO; + u32 must_match = EF_RISCV_FLOAT_ABI_MASK | EF_RISCV_RVE | ~known; + u32 merge_bits = EF_RISCV_RVC | EF_RISCV_TSO; + KitFloatAbi incoming_abi; + KitFloatAbi target_abi; + Slice first; + if (!l || !ob || l->c->target.obj != KIT_OBJ_ELF || + (l->c->target.arch != KIT_ARCH_RV32 && + l->c->target.arch != KIT_ARCH_RV64) || + !obj_get_elf_e_flags(ob, &incoming)) + return; + incoming_abi = elf_riscv_float_abi_from_e_flags(incoming); + target_abi = (KitFloatAbi)l->c->target.float_abi; + if (target_abi != KIT_FLOAT_ABI_DEFAULT && incoming_abi != target_abi) { + compiler_panic(l->c, SRCLOC_NONE, + "link: incompatible input '%.*s': RISC-V float ABI is %s, " + "link target requires %s (e_flags=0x%x)", + SLICE_ARG(label), link_float_abi_name(incoming_abi), + link_float_abi_name(target_abi), incoming); + } + if (!l->have_elf_e_flags) { + l->elf_e_flags = incoming; + l->elf_e_flags_source = pool_intern_slice(l->c->global, label); + l->have_elf_e_flags = 1; + return; + } + if ((l->elf_e_flags & must_match) != (incoming & must_match)) { + first = l->elf_e_flags_source + ? pool_slice(l->c->global, l->elf_e_flags_source) + : SLICE_LIT("<first input>"); + compiler_panic(l->c, SRCLOC_NONE, + "link: incompatible RISC-V ELF e_flags: input '%.*s' has " + "0x%x, input '%.*s' has 0x%x (float ABI, RVE, and reserved " + "bits must agree)", + SLICE_ARG(label), incoming, SLICE_ARG(first), + l->elf_e_flags); + } + l->elf_e_flags |= incoming & merge_bits; +} + /* ---- SrcLoc helper ---- */ /* SymHash is a HASHMAP_DEFINE instance — see link_internal.h. The thin @@ -109,6 +264,8 @@ LinkInputId link_add_obj(Linker* l, ObjBuilder* ob) { LinkInputId id; LinkInput* in; if (!l || !ob) return LINK_INPUT_NONE; + link_validate_obj_target(l, SLICE_LIT("<in-memory object>"), ob); + link_merge_elf_e_flags(l, ob, SLICE_LIT("<in-memory object>")); in = inputs_push(l, &id); in->kind = LINK_INPUT_OBJ; in->order = l->next_input_order++; @@ -124,7 +281,10 @@ LinkInputId link_add_obj_bytes(Linker* l, const char* name, const u8* data, KitBinFmt fmt; const ObjFormatImpl* impl; const char* reader_name; + Slice label; if (!l || !data || !len) return LINK_INPUT_NONE; + label = name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)"); + link_validate_bytes_target(l, label, SLICE_NULL, data, len, 0); fmt = kit_detect_fmt(data, len); impl = obj_format_lookup_bin(fmt); if (!impl || !impl->read) @@ -141,6 +301,7 @@ LinkInputId link_add_obj_bytes(Linker* l, const char* name, const u8* data, l->c, SRCLOC_NONE, "link_add_obj_bytes: %.*s returned NULL for '%.*s'", SLICE_ARG(slice_from_cstr(reader_name)), SLICE_ARG(name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)"))); + link_merge_elf_e_flags(l, ob, label); in = inputs_push(l, &id); in->order = l->next_input_order++; in->obj = ob; /* re-uses the ObjBuilder slot for ownership */ @@ -167,7 +328,10 @@ LinkInputId link_add_dso_bytes(Linker* l, const char* name, const u8* data, KitBinFmt fmt; ObjFormatDsoReader reader; const char* reader_name; + Slice label; if (!l || !data || !len) return LINK_INPUT_NONE; + label = name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)"); + link_validate_bytes_target(l, label, SLICE_NULL, data, len, 1); if (!obj_format_dso_reader_for_bytes(data, len, &fmt, &reader)) compiler_panic( l->c, SRCLOC_NONE, @@ -182,6 +346,7 @@ LinkInputId link_add_dso_bytes(Linker* l, const char* name, const u8* data, l->c, SRCLOC_NONE, "link_add_dso_bytes: %.*s returned NULL for '%.*s'", SLICE_ARG(slice_from_cstr(reader_name)), SLICE_ARG(name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)"))); + link_merge_elf_e_flags(l, ob, label); in = inputs_push(l, &id); in->kind = LINK_INPUT_DSO_BYTES; in->order = l->next_input_order++; @@ -271,6 +436,10 @@ LinkInputId link_add_archive_bytes(Linker* l, const char* name, const u8* data, ObjBuilder* ob = NULL; KitBinFmt mfmt = kit_detect_fmt(mem.data, mem.size); const ObjFormatImpl* member_impl = obj_format_lookup_bin(mfmt); + link_validate_bytes_target( + l, mem.name.len ? mem.name : SLICE_LIT("(unnamed)"), + name ? slice_from_cstr(name) : SLICE_LIT("(unnamed)"), mem.data, + mem.size, 0); if (target_impl && target_impl->archive_member) { ObjFormatArchiveMember desc; ObjFormatArchiveAction action; diff --git a/src/link/link_internal.h b/src/link/link_internal.h @@ -223,6 +223,12 @@ struct Linker { * dynamic ELF emit path (Phase 6). */ int emit_pie; u16 pe_subsystem; + /* RISC-V psABI e_flags accumulated from object inputs selected for this + * link. Float-ABI/RVE conflicts are rejected while registering inputs; + * RVC/TSO feature-presence bits are OR-merged. */ + u32 elf_e_flags; + Sym elf_e_flags_source; + u8 have_elf_e_flags; /* Caller-supplied PT_INTERP. layout_dyn falls back to a target- * derived default when this is 0. */ Sym interp_path; @@ -338,6 +344,7 @@ void link_emit_internal_abs64(LinkImage* img, LinkSectionId lsid, u32 offset, /* ---- Public entries (link_resolve.c) -------------------------------------- */ void link_ingest_archives(struct Linker*); +void link_merge_elf_e_flags(struct Linker*, ObjBuilder*, KitSlice label); /* PE/COFF only: synthesize a tiny ObjBuilder providing the mingw CRT * `__CTOR_LIST__` / `__CTOR_END__` / `__DTOR_LIST__` / `__DTOR_END__` * boundary symbols. See link_resolve.c for the contract. */ @@ -424,6 +431,10 @@ struct LinkImage { * symbol's dso_input_id back to the providing dylib's install-name). */ struct Linker* linker; + /* Final merged RISC-V ELF flags, copied after archive selection. */ + u32 elf_e_flags; + u8 have_elf_e_flags; + LinkSyms syms; /* LinkSymId = slot index + 1 */ SymHash globals; /* name -> LinkSymId for global/weak */ @@ -528,6 +539,27 @@ struct LinkImage { u8 headers_present; }; +/* Whether S is the canonical LinkSymbol for its non-local name. + * + * Resolution deliberately keeps one LinkSymbol per input symbol: after an + * undefined reference is resolved, its per-input slot mirrors the definition + * so relocations can retain their stable LinkSymId. img->globals remains the + * authority for the single canonical global/weak slot. Format emitters and + * post-link reports must use this predicate rather than mistaking those + * resolved reference slots for distinct definitions. + * + * Locals and nameless records are outside the global-name authority and are + * therefore not canonical globals. A named non-local missing from globals is + * retained defensively; synthetic/import bookkeeping can briefly have that + * shape while an image is being assembled. */ +static inline int link_symbol_is_canonical_global(const LinkImage* img, + const LinkSymbol* s) { + LinkSymId canonical; + if (!img || !s || !s->name || s->bind == SB_LOCAL) return 0; + canonical = symhash_get(&img->globals, s->name); + return canonical == LINK_SYM_NONE || canonical == s->id; +} + /* Page granularity used for ELF segment alignment and the file-offset / * vaddr congruence the runtime loader requires. 16 KiB matches AArch64 * Apple Silicon and the common Linux/AArch64 kernel config; 4 KiB pages diff --git a/src/link/link_layout.c b/src/link/link_layout.c @@ -2144,8 +2144,6 @@ static int link_section_name_is_dynamic(Slice nm) { static void link_validate_freestanding(Linker* l) { u32 ii; - u32 eflags0 = 0; - int have_eflags = 0; if (!l->freestanding_strict) return; for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) { @@ -2164,28 +2162,6 @@ static void link_validate_freestanding(Linker* l) { } if (!ob) continue; - /* (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)) { - 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. */ @@ -2225,6 +2201,8 @@ LinkImage* link_resolve(Linker* l) { img->text_base_set = l->text_base_set; img->text_base = l->text_base; img->shared = l->emit_shared; + img->elf_e_flags = l->elf_e_flags; + img->have_elf_e_flags = l->have_elf_e_flags; img->ninput_maps = LinkInputs_count(&l->inputs); metrics_count(l->c, "link.inputs", img->ninput_maps); diff --git a/src/link/link_relocatable.c b/src/link/link_relocatable.c @@ -234,25 +234,14 @@ static ObjSecId rel_find_compatible_section(ObjBuilder* out, static void rel_copy_sections(Linker* l, ObjBuilder* out, RelInputMap* maps, u32 ninputs) { u32 ii, j; - int have_eflags = 0; - u32 eflags = 0; for (ii = 0; ii < ninputs; ++ii) { LinkInput* in = LinkInputs_at(&l->inputs, ii); ObjBuilder* ob = in->obj; u32 nsec; - u32 in_eflags; if (in->kind == LINK_INPUT_DSO_BYTES) compiler_panic(l->c, SRCLOC_NONE, "link -r: DSO inputs are not supported"); if (!ob) continue; - if (obj_get_elf_e_flags(ob, &in_eflags)) { - if (!have_eflags) { - eflags = in_eflags; - have_eflags = 1; - } else if (eflags != in_eflags) { - compiler_panic(l->c, SRCLOC_NONE, "link -r: incompatible ELF e_flags"); - } - } nsec = obj_section_count(ob); maps[ii].nsection = nsec; maps[ii].section = (RelObjSecMap*)l->heap->alloc( @@ -292,7 +281,7 @@ static void rel_copy_sections(Linker* l, ObjBuilder* out, RelInputMap* maps, maps[ii].section[j].delta = delta; } } - if (have_eflags) obj_set_elf_e_flags(out, eflags); + if (l->have_elf_e_flags) obj_set_elf_e_flags(out, l->elf_e_flags); for (ii = 0; ii < ninputs; ++ii) { LinkInput* in = LinkInputs_at(&l->inputs, ii); diff --git a/src/link/link_resolve.c b/src/link/link_resolve.c @@ -503,19 +503,36 @@ static int resolve_elf_loader_owned_undef(Linker* l, LinkSymbol* s) { Slice nm; if (!l || !s || !s->name) return 0; if (!l->emit_pie || l->c->target.obj != KIT_OBJ_ELF) return 0; - if (l->c->target.arch != KIT_ARCH_X86_64 && - l->c->target.arch != KIT_ARCH_RV64 && l->c->target.arch != KIT_ARCH_RV32) - return 0; nm = pool_slice(l->c->global, s->name); - if (!slice_eq_cstr(nm, "__tls_get_addr")) return 0; - /* Some glibc dynamic TLS helpers are provided by the ELF interpreter itself - * (ld-linux), not by a regular DT_NEEDED DSO. GNU ld permits this - * unresolved-looking reference in dynamic executables; model it as a - * function import with no provider DSO so the PLT/dynsym machinery can emit - * any needed JUMP_SLOT without adding the interpreter as DT_NEEDED. RISC-V - * TLS-GD references that relax fully to local-exec will skip their call - * relocations later, leaving this dynsym entry harmlessly unused. */ - s->kind = SK_FUNC; + if (slice_eq_cstr(nm, "__tls_get_addr")) { + if (l->c->target.arch != KIT_ARCH_X86_64 && + l->c->target.arch != KIT_ARCH_RV64 && + l->c->target.arch != KIT_ARCH_RV32) + return 0; + /* Some glibc dynamic TLS helpers are provided by the ELF interpreter + * itself (ld-linux), not by a regular DT_NEEDED DSO. GNU ld permits this + * unresolved-looking reference in dynamic executables; model it as a + * function import with no provider DSO so the PLT/dynsym machinery can + * emit any needed JUMP_SLOT without adding the interpreter as DT_NEEDED. + * RISC-V TLS-GD references that relax fully to local-exec will skip their + * call relocations later, leaving this dynsym entry harmlessly unused. */ + s->kind = SK_FUNC; + } else if (slice_eq_cstr(nm, "__stack_chk_guard")) { + /* glibc's AArch64 and RISC-V ABIs expose the process canary from + * ld-linux, while libc only carries an undefined versioned reference to + * it. Treat the guard as an interpreter-owned object import. This keeps + * the loader out of DT_NEEDED (it is already PT_INTERP) and lets the + * ordinary GOT/dynsym machinery bind the address at startup. x86-64 reads + * its guard directly from fs:0x28 and never reaches this path. */ + if (l->c->target.os != KIT_OS_LINUX || + (l->c->target.arch != KIT_ARCH_ARM_64 && + l->c->target.arch != KIT_ARCH_RV64 && + l->c->target.arch != KIT_ARCH_RV32)) + return 0; + s->kind = SK_OBJ; + } else { + return 0; + } s->imported = 1; s->dso_input_id = LINK_INPUT_NONE; s->elf_version = 0; @@ -1196,6 +1213,11 @@ static void include_archive_member(Linker* l, const LinkArchive* ar, u32 idx; Sym coff_dll = 0; if (mem->included) return; + if (mem->obj) + link_merge_elf_e_flags( + l, mem->obj, + mem->name ? pool_slice(l->c->global, mem->name) + : SLICE_LIT("<unnamed archive member>")); in = LinkInputs_push(&l->inputs, &idx); if (!in) compiler_panic(l->c, SRCLOC_NONE, diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c @@ -1411,10 +1411,9 @@ void link_emit_elf(LinkImage* img, Writer* w) { * mirrors fields onto the undef). Only the canonical * (first registered) entry is in img->globals. Skip the * shadow copies. */ - if (!is_local && s->name) { - LinkSymId canonical = symhash_get(&img->globals, s->name); - if (canonical != LINK_SYM_NONE && canonical != s->id) continue; - } + if (!is_local && s->name && + !link_symbol_is_canonical_global(img, s)) + continue; { Slice nm_s = s->name ? pool_slice(c->global, s->name) : SLICE_NULL; nm = nm_s.s ? nm_s.s : ""; @@ -1665,15 +1664,13 @@ void link_emit_elf(LinkImage* img, Writer* w) { LinkSyms_at(&img->syms, img->entry_sym - 1)->vaddr; ehdr.e_phoff = ehdr_sz; ehdr.e_shoff = shdr_off; - /* e_flags carries the arch ABI bits (RISC-V float-ABI / RVC). This was - * previously hardcoded 0 for all arches; writing arch->e_flags lands - * the RV32/RV64 flags. (RV64 descriptor e_flags is unchanged, so its - * header now reflects RVC|FLOAT_ABI_DOUBLE — see integration notes.) */ + /* Preserve the linker's psABI merge. RVC/TSO are presence bits and were + * ORed across selected inputs; float-ABI/RVE/reserved bits were required to + * agree. A synthetic/no-input image falls back to the arch descriptor. */ ehdr.e_flags = arch->e_flags; - /* rv32: ilp32 and ilp32f share KIT_ARCH_RV32, so the descriptor's float-ABI - * bits are a placeholder. Override them from -mabi so the executable's ABI - * matches its objects (and a soft ilp32 image isn't mislabelled single). */ - if (e_machine == EM_RISCV && class32) { + if (e_machine == EM_RISCV && img->have_elf_e_flags) { + ehdr.e_flags = img->elf_e_flags; + } else if (e_machine == EM_RISCV) { u32 fa = elf_riscv_float_abi_to_e_flags(c->target.float_abi); ehdr.e_flags = (ehdr.e_flags & ~(u32)EF_RISCV_FLOAT_ABI_MASK) | fa; } diff --git a/src/obj/macho/link.c b/src/obj/macho/link.c @@ -2013,6 +2013,7 @@ static void build_symtab(MCtx* x) { if (!s->defined) continue; if (s->bind != SB_GLOBAL && s->bind != SB_WEAK) continue; if (s->name == 0) continue; + if (!link_symbol_is_canonical_global(img, s)) continue; if (s->kind == SK_ABS) continue; /* skip abs externs */ /* Locate which OutSec contains this vaddr to figure out n_sect. * n_sect is the 1-based index into the flat section_64 table the diff --git a/test/link/harness/link_exe_runner.c b/test/link/harness/link_exe_runner.c @@ -2,6 +2,7 @@ * * Usage: * link_exe_runner [--gc-sections] [--entry NAME] [--linker-script <path>] + * [--symbols <path>] * -o <out.exe> * [--archive [--whole-archive] <lib.a>] <in.o> ... * @@ -109,10 +110,27 @@ static int write_exe(const char* path, const uint8_t* data, size_t len) { return 0; } +static int write_data(const char* path, const uint8_t* data, size_t len) { + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + size_t w = 0; + if (fd < 0) return -1; + while (w < len) { + ssize_t k = write(fd, data + w, len - w); + if (k <= 0) { + close(fd); + return -1; + } + w += (size_t)k; + } + close(fd); + return 0; +} + int main(int argc, char** argv) { const char* out_path = NULL; const char* entry_name = "_start"; const char* script_path = NULL; + const char* symbols_path = NULL; int gc_sections = 0; int next_archive = 0; int next_whole = 0; @@ -141,6 +159,8 @@ int main(int argc, char** argv) { entry_name = argv[++i]; } else if (!strcmp(argv[i], "--linker-script") && i + 1 < argc) { script_path = argv[++i]; + } else if (!strcmp(argv[i], "--symbols") && i + 1 < argc) { + symbols_path = argv[++i]; } else if (!strcmp(argv[i], "-o") && i + 1 < argc) { out_path = argv[++i]; } else { @@ -266,12 +286,26 @@ int main(int argc, char** argv) { size_t out_len; const uint8_t* out_bytes = kit_writer_mem_bytes(w, &out_len); int wrc = write_exe(out_path, out_bytes, out_len); + if (wrc == 0 && symbols_path) { + KitWriter* sw = NULL; + if (kit_writer_mem(&g_heap, &sw) != KIT_OK || !sw || + kit_link_session_write_symbols(link, KIT_LINK_SYMBOLS_NM, sw) != + KIT_OK || + kit_writer_status(sw) != KIT_OK) { + wrc = -1; + } else { + size_t symbols_len = 0; + const uint8_t* symbols = kit_writer_mem_bytes(sw, &symbols_len); + wrc = write_data(symbols_path, symbols, symbols_len); + } + if (sw) kit_writer_close(sw); + } kit_link_session_free(link); if (script) kit_link_script_free(&ctx, script); kit_writer_close(w); free_compiler_target(c, kt); if (wrc) { - fprintf(stderr, "link-exe-runner: write failed\n"); + fprintf(stderr, "link-exe-runner: output write failed\n"); return 2; } return 0; diff --git a/test/link/link_compat_test.c b/test/link/link_compat_test.c @@ -0,0 +1,385 @@ +/* Public LinkSession input-target compatibility coverage. */ + +#include <kit/core.h> +#include <kit/link.h> +#include <kit/object.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "lib/kit_unit.h" + +static KitUnit g_u; +#define EXPECT(c, ...) CU_EXPECT(&g_u, c, __VA_ARGS__) + +typedef struct EmittedObject { + KitObjBuilder* ob; + KitWriter* writer; + KitSlice bytes; +} EmittedObject; + +static int emit_empty_object(KitCompiler* c, EmittedObject* out) { + size_t len = 0; + memset(out, 0, sizeof(*out)); + if (kit_obj_builder_new(c, &out->ob) != KIT_OK || !out->ob) return 0; + if (kit_obj_builder_finalize(out->ob) != KIT_OK) return 0; + if (kit_writer_mem(&g_u.heap, &out->writer) != KIT_OK || !out->writer) + return 0; + if (kit_obj_builder_emit(out->ob, out->writer) != KIT_OK) return 0; + out->bytes.data = kit_writer_mem_bytes(out->writer, &len); + out->bytes.len = len; + return out->bytes.data != NULL && out->bytes.len != 0; +} + +static void emitted_object_fini(EmittedObject* o) { + if (o->writer) kit_writer_close(o->writer); + if (o->ob) kit_obj_builder_free(o->ob); + memset(o, 0, sizeof(*o)); +} + +static void ar_field(char* dst, size_t width, const char* value) { + size_t n = strlen(value); + memset(dst, ' ', width); + if (n > width) n = width; + memcpy(dst, value, n); +} + +static uint8_t* make_archive(KitSlice member, size_t* len_out) { + static const char magic[] = "!<arch>\n"; + size_t padded = member.len + (member.len & 1u); + size_t total = 8u + 60u + padded; + uint8_t* bytes = (uint8_t*)malloc(total); + char size_buf[32]; + char* hdr; + if (!bytes) return NULL; + memset(bytes, 0, total); + memcpy(bytes, magic, 8u); + hdr = (char*)bytes + 8u; + ar_field(hdr + 0u, 16u, "foreign.o/"); + ar_field(hdr + 16u, 12u, "0"); + ar_field(hdr + 28u, 6u, "0"); + ar_field(hdr + 34u, 6u, "0"); + ar_field(hdr + 40u, 8u, "100644"); + snprintf(size_buf, sizeof(size_buf), "%lu", (unsigned long)member.len); + ar_field(hdr + 48u, 10u, size_buf); + hdr[58] = '`'; + hdr[59] = '\n'; + memcpy(bytes + 68u, member.data, member.len); + if (member.len & 1u) bytes[68u + member.len] = '\n'; + *len_out = total; + return bytes; +} + +static KitLinkSession* new_reloc_session(KitCompiler* c) { + KitLinkSessionOptions opts; + KitLinkSession* s = NULL; + memset(&opts, 0, sizeof(opts)); + opts.output_kind = KIT_LINK_OUTPUT_RELOCATABLE; + if (kit_link_session_new(c, &opts, &s) != KIT_OK) return NULL; + return s; +} + +static void check_input_target_boundary(void) { + KitCompiler* expected = NULL; + KitCompiler* foreign = NULL; + EmittedObject x64; + EmittedObject aa64; + KitLinkSession* s = NULL; + KitStatus st; + uint8_t* archive = NULL; + size_t archive_len = 0; + + memset(&x64, 0, sizeof(x64)); + memset(&aa64, 0, sizeof(aa64)); + EXPECT(kit_unit_compiler_new( + &g_u, kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &expected) == KIT_OK && + expected, + "x64 compiler"); + EXPECT(kit_unit_compiler_new( + &g_u, kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &foreign) == KIT_OK && + foreign, + "aarch64 compiler"); + if (!expected || !foreign) goto done; + EXPECT(emit_empty_object(expected, &x64), "emit x64 object"); + EXPECT(emit_empty_object(foreign, &aa64), "emit aarch64 object"); + if (!x64.bytes.data || !aa64.bytes.data) goto done; + + s = new_reloc_session(expected); + EXPECT(s != NULL, "valid link session"); + if (s) { + EXPECT(kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("native-x64.o"), + &x64.bytes) == KIT_OK, + "matching object accepted"); + kit_link_session_free(s); + s = NULL; + } + + s = new_reloc_session(expected); + EXPECT(s != NULL, "mismatch link session"); + if (s) { + g_u.suppress_fatal = 1; + st = kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("foreign-aa64.o"), + &aa64.bytes); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "foreign object rejected at add boundary"); + EXPECT(strstr(g_u.last_diag, "foreign-aa64.o") != NULL && + strstr(g_u.last_diag, "architecture mismatch") != NULL && + strstr(g_u.last_diag, "expected") != NULL && + strstr(g_u.last_diag, "got") != NULL, + "foreign object diagnostic is named and actionable: %s", + g_u.last_diag); + kit_link_session_free(s); + s = NULL; + } + + s = new_reloc_session(expected); + EXPECT(s != NULL, "DSO mismatch link session"); + if (s) { + g_u.suppress_fatal = 1; + st = kit_link_session_add_dso_bytes(s, KIT_SLICE_LIT("foreign-aa64.so"), + &aa64.bytes); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "foreign DSO/import input rejected at add boundary"); + EXPECT(strstr(g_u.last_diag, "foreign-aa64.so") != NULL && + strstr(g_u.last_diag, "architecture mismatch") != NULL, + "DSO diagnostic names the incompatible input: %s", g_u.last_diag); + kit_link_session_free(s); + s = NULL; + } + + archive = make_archive(aa64.bytes, &archive_len); + EXPECT(archive != NULL, "foreign archive fixture"); + s = new_reloc_session(expected); + EXPECT(s != NULL, "archive link session"); + if (archive && s) { + KitLinkArchiveInput input; + memset(&input, 0, sizeof(input)); + input.name = KIT_SLICE_LIT("libforeign.a"); + input.bytes.data = archive; + input.bytes.len = archive_len; + g_u.suppress_fatal = 1; + st = kit_link_session_add_archive_bytes(s, &input); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "foreign archive member rejected before selection"); + EXPECT(strstr(g_u.last_diag, "libforeign.a(foreign.o)") != NULL && + strstr(g_u.last_diag, "architecture mismatch") != NULL, + "archive diagnostic names container and member: %s", g_u.last_diag); + } + if (s) { + kit_link_session_free(s); + s = NULL; + } + + s = new_reloc_session(expected); + EXPECT(s != NULL, "builder link session"); + if (s) { + g_u.suppress_fatal = 1; + st = kit_link_session_add_obj(s, aa64.ob); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "foreign in-memory builder rejected"); + EXPECT(strstr(g_u.last_diag, "<in-memory object>") != NULL && + strstr(g_u.last_diag, "architecture mismatch") != NULL, + "in-memory diagnostic names input: %s", g_u.last_diag); + } + +done: + if (s) kit_link_session_free(s); + free(archive); + emitted_object_fini(&aa64); + emitted_object_fini(&x64); + if (foreign) kit_compiler_free(foreign); + if (expected) kit_compiler_free(expected); +} + +static void check_format_boundary(KitOSKind os, KitObjFmt obj, + const char* label) { + KitCompiler* expected = NULL; + KitCompiler* foreign = NULL; + EmittedObject native; + EmittedObject other; + KitLinkSession* s = NULL; + KitStatus st; + uint8_t* archive = NULL; + size_t archive_len = 0; + + memset(&native, 0, sizeof(native)); + memset(&other, 0, sizeof(other)); + EXPECT(kit_unit_compiler_new( + &g_u, kit_unit_target(KIT_ARCH_X86_64, os, obj), &expected) == + KIT_OK && + expected, + "%s x64 compiler", label); + EXPECT(kit_unit_compiler_new( + &g_u, kit_unit_target(KIT_ARCH_ARM_64, os, obj), &foreign) == + KIT_OK && + foreign, + "%s aarch64 compiler", label); + if (!expected || !foreign) goto done; + EXPECT(emit_empty_object(expected, &native), "%s emit x64 object", label); + EXPECT(emit_empty_object(foreign, &other), "%s emit aarch64 object", label); + if (!native.bytes.data || !other.bytes.data) goto done; + + s = new_reloc_session(expected); + EXPECT(s != NULL, "%s raw mismatch session", label); + if (s) { + g_u.suppress_fatal = 1; + st = kit_link_session_add_obj_bytes(s, kit_slice_cstr(label), &other.bytes); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "%s foreign raw object rejected", label); + EXPECT(strstr(g_u.last_diag, label) != NULL && + strstr(g_u.last_diag, "architecture mismatch") != NULL, + "%s raw diagnostic names expected/actual input: %s", label, + g_u.last_diag); + kit_link_session_free(s); + s = NULL; + } + + archive = make_archive(other.bytes, &archive_len); + EXPECT(archive != NULL, "%s archive fixture", label); + s = new_reloc_session(expected); + EXPECT(s != NULL, "%s archive mismatch session", label); + if (archive && s) { + KitLinkArchiveInput input; + memset(&input, 0, sizeof(input)); + input.name = kit_slice_cstr(label); + input.bytes.data = archive; + input.bytes.len = archive_len; + g_u.suppress_fatal = 1; + st = kit_link_session_add_archive_bytes(s, &input); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "%s foreign archive member rejected", label); + EXPECT(strstr(g_u.last_diag, label) != NULL && + strstr(g_u.last_diag, "foreign.o") != NULL, + "%s archive diagnostic names container/member: %s", label, + g_u.last_diag); + } + if (s) { + kit_link_session_free(s); + s = NULL; + } + + s = new_reloc_session(expected); + EXPECT(s != NULL, "%s builder mismatch session", label); + if (s) { + g_u.suppress_fatal = 1; + st = kit_link_session_add_obj(s, other.ob); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "%s foreign in-memory builder rejected", label); + } + +done: + if (s) kit_link_session_free(s); + free(archive); + emitted_object_fini(&other); + emitted_object_fini(&native); + if (foreign) kit_compiler_free(foreign); + if (expected) kit_compiler_free(expected); +} + +static void check_platform_and_format_rules(void) { + KitCompiler* linux = NULL; + KitCompiler* freebsd = NULL; + KitCompiler* macho = NULL; + KitCompiler* android = NULL; + EmittedObject linux_obj; + EmittedObject freebsd_obj; + EmittedObject macho_obj; + KitLinkSession* s = NULL; + KitStatus st; + + memset(&linux_obj, 0, sizeof(linux_obj)); + memset(&freebsd_obj, 0, sizeof(freebsd_obj)); + memset(&macho_obj, 0, sizeof(macho_obj)); + EXPECT(kit_unit_compiler_new( + &g_u, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &linux) == KIT_OK && + linux, + "platform-rule Linux compiler"); + EXPECT(kit_unit_compiler_new( + &g_u, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_FREEBSD, KIT_OBJ_ELF), + &freebsd) == KIT_OK && + freebsd, + "platform-rule FreeBSD compiler"); + EXPECT(kit_unit_compiler_new( + &g_u, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_MACOS, KIT_OBJ_MACHO), + &macho) == KIT_OK && + macho, + "format-rule Mach-O compiler"); + EXPECT(kit_unit_compiler_new( + &g_u, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_ANDROID, KIT_OBJ_ELF), + &android) == KIT_OK && + android, + "platform-rule Android compiler"); + if (!linux || !freebsd || !macho || !android) goto done; + EXPECT(emit_empty_object(linux, &linux_obj), "emit generic Linux ELF"); + EXPECT(emit_empty_object(freebsd, &freebsd_obj), "emit FreeBSD ELF"); + EXPECT(emit_empty_object(macho, &macho_obj), "emit x64 Mach-O"); + if (!linux_obj.bytes.data || !freebsd_obj.bytes.data || + !macho_obj.bytes.data) + goto done; + + s = new_reloc_session(linux); + EXPECT(s != NULL, "platform mismatch session"); + if (s) { + g_u.suppress_fatal = 1; + st = kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("freebsd-x64.o"), + &freebsd_obj.bytes); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "foreign operating-system ELF rejected"); + EXPECT(strstr(g_u.last_diag, "operating system mismatch") != NULL, + "platform mismatch diagnostic: %s", g_u.last_diag); + kit_link_session_free(s); + s = NULL; + } + + s = new_reloc_session(linux); + EXPECT(s != NULL, "format mismatch session"); + if (s) { + g_u.suppress_fatal = 1; + st = kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("foreign-x64.o"), + &macho_obj.bytes); + g_u.suppress_fatal = 0; + EXPECT(st == KIT_ERR, "foreign object format rejected"); + EXPECT(strstr(g_u.last_diag, "object format mismatch") != NULL, + "format mismatch diagnostic: %s", g_u.last_diag); + kit_link_session_free(s); + s = NULL; + } + + /* Generic EI_OSABI_NONE/Linux relocatables are valid inputs to Android; + * this is an explicit directional rule, not general OS borrowing. */ + s = new_reloc_session(android); + EXPECT(s != NULL, "Android generic-ELF session"); + if (s) { + EXPECT(kit_link_session_add_obj_bytes(s, KIT_SLICE_LIT("generic-linux.o"), + &linux_obj.bytes) == KIT_OK, + "Android accepts a generic/Linux ELF relocatable"); + } + +done: + if (s) kit_link_session_free(s); + emitted_object_fini(&macho_obj); + emitted_object_fini(&freebsd_obj); + emitted_object_fini(&linux_obj); + if (android) kit_compiler_free(android); + if (macho) kit_compiler_free(macho); + if (freebsd) kit_compiler_free(freebsd); + if (linux) kit_compiler_free(linux); +} + +int main(void) { + kit_unit_init(&g_u); + check_input_target_boundary(); + check_format_boundary(KIT_OS_MACOS, KIT_OBJ_MACHO, "foreign-macho.a"); + check_format_boundary(KIT_OS_WINDOWS, KIT_OBJ_COFF, "foreign-coff.a"); + check_platform_and_format_rules(); + kit_unit_summary(&g_u, "link_compat_test"); + return kit_unit_status(&g_u); +} diff --git a/test/link/macho-symbols.sh b/test/link/macho-symbols.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# Verify that resolved per-input reference slots do not become duplicate +# external definitions in a linked Mach-O LC_SYMTAB or linker symbol report. +# Distinct global aliases at the same address must remain distinct. + +set -eu + +root=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +runner=${LINK_EXE_RUNNER:-$root/build/test/link-exe-runner} +kit=${KIT:-$root/build/kit} +cc=${CC:-clang} +nm_bin=$(command -v llvm-nm 2>/dev/null || command -v nm 2>/dev/null || true) +work=$root/build/test/macho-symbols +src=$root/test/link/macho_symbols + +[ -x "$runner" ] || { + printf 'macho-symbols: missing runner: %s\n' "$runner" >&2 + exit 1 +} +[ -x "$kit" ] || { + printf 'macho-symbols: missing kit binary: %s\n' "$kit" >&2 + exit 1 +} +[ -n "$nm_bin" ] || { + printf 'macho-symbols: no platform nm oracle\n' >&2 + exit 1 +} + +rm -rf "$work" +mkdir -p "$work" + +defined_count() { + awk -v sym="$2" ' + $NF == sym && $(NF - 1) != "U" { ++n } + END { print n + 0 } + ' "$1" +} + +check_one() { + arch=$1 + triple=$2 + out=$work/$arch + mkdir -p "$out" + + for base in definition reference_a reference_b; do + "$cc" --target="$triple" -O0 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie -c "$src/$base.c" -o "$out/$base.o" \ + >"$out/compile-$base.stdout" 2>"$out/compile-$base.stderr" + done + "$cc" --target="$triple" -c "$src/aliases.S" -o "$out/aliases.o" \ + >"$out/compile-aliases.stdout" 2>"$out/compile-aliases.stderr" + + KIT_TEST_ARCH=$arch KIT_TEST_OBJ=macho "$runner" \ + --entry test_main --symbols "$out/symbols.txt" -o "$out/linked.macho" \ + "$out/definition.o" "$out/reference_a.o" "$out/reference_b.o" \ + "$out/aliases.o" >"$out/link.stdout" 2>"$out/link.stderr" + "$nm_bin" "$out/linked.macho" >"$out/platform-nm.txt" \ + 2>"$out/platform-nm.stderr" + "$kit" nm "$out/linked.macho" >"$out/kit-nm.txt" \ + 2>"$out/kit-nm.stderr" + + for table in "$out/platform-nm.txt" "$out/kit-nm.txt" \ + "$out/symbols.txt"; do + [ "$(defined_count "$table" _audit_macho_provided)" -eq 1 ] || { + printf 'macho-symbols: %s: duplicate canonical definition in %s\n' \ + "$arch" "$table" >&2 + exit 1 + } + for alias in _audit_macho_alias_a _audit_macho_alias_b; do + [ "$(defined_count "$table" "$alias")" -eq 1 ] || { + printf 'macho-symbols: %s: missing distinct alias %s in %s\n' \ + "$arch" "$alias" "$table" >&2 + exit 1 + } + done + done + printf 'ok macho canonical symbols %s\n' "$arch" +} + +check_one aa64 arm64-apple-macos +check_one x64 x86_64-apple-macos diff --git a/test/link/macho_symbols/aliases.S b/test/link/macho_symbols/aliases.S @@ -0,0 +1,7 @@ +.section __DATA,__data +.globl _audit_macho_alias_a +.globl _audit_macho_alias_b +.p2align 2 +_audit_macho_alias_a: +_audit_macho_alias_b: +.long 7 diff --git a/test/link/macho_symbols/definition.c b/test/link/macho_symbols/definition.c @@ -0,0 +1 @@ +int audit_macho_provided(void) { return 19; } diff --git a/test/link/macho_symbols/reference_a.c b/test/link/macho_symbols/reference_a.c @@ -0,0 +1,9 @@ +extern int audit_macho_provided(void); +extern int audit_macho_second_reference(void); + +int test_main(void) { + return audit_macho_provided() == 19 && + audit_macho_second_reference() == 19 + ? 0 + : 1; +} diff --git a/test/link/macho_symbols/reference_b.c b/test/link/macho_symbols/reference_b.c @@ -0,0 +1,3 @@ +extern int audit_macho_provided(void); + +int audit_macho_second_reference(void) { return audit_macho_provided(); }