kit

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

commit d79afab6903f07ccae1fe5b750fbaa371ea62599
parent 33d98c62e5201d8ce5e5436096615915e6d7e6c3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 25 May 2026 13:12:26 -0700

build: enforce public libcfree exports

Diffstat:
MMakefile | 23++++++++---------------
Minclude/cfree/core.h | 6++++++
Ascripts/lib_reloc_defined_prefixes.py | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/link/link_relocatable.c | 8+++++++-
Mtest/test.mk | 12++++++++++--
5 files changed, 148 insertions(+), 18 deletions(-)

diff --git a/Makefile b/Makefile @@ -155,7 +155,7 @@ LIB_OBJS = $(patsubst src/%.c,$(BUILD_DIR)/lib/%.o,$(LIB_SRCS)) \ $(LANG_OBJS) \ $(patsubst src/%.S,$(BUILD_DIR)/lib/%.o,$(LIB_ASMS)) LIB_DEPS = $(LIB_OBJS:.o=.d) -LIB_AR_STAGING = $(BUILD_DIR)/.ar-staging/libcfree +LIB_RELOC_OBJ = $(BUILD_DIR)/libcfree.o DRIVER_SRCS = $(wildcard driver/*.c) ifneq ($(CFREE_LANG_CPP_ENABLED),1) @@ -177,22 +177,15 @@ bin: $(BIN) # Replace the archive (`ar rcs` only adds/updates), so removing a .c file # also removes its .o from the archive on the next build. -$(LIB_AR): $(LIB_OBJS) +$(LIB_RELOC_OBJ): $(LIB_OBJS) @mkdir -p $(dir $@) @rm -f $@ - @rm -rf $(LIB_AR_STAGING) - @mkdir -p $(LIB_AR_STAGING) - @set -e; \ - for obj in $(LIB_OBJS); do \ - rel=$${obj#$(BUILD_DIR)/}; \ - name=$$(printf '%s' "$$rel" | tr '/' '_'); \ - case "$$obj" in \ - /*) target="$$obj" ;; \ - *) target="$$PWD/$$obj" ;; \ - esac; \ - ln -sf "$$target" "$(LIB_AR_STAGING)/$$name"; \ - done - $(AR) rcs $@ $(LIB_AR_STAGING)/*.o + $(LD) -r -o $@ $(LIB_OBJS) + +$(LIB_AR): $(LIB_RELOC_OBJ) + @mkdir -p $(dir $@) + @rm -f $@ + $(AR) rcs $@ $(LIB_RELOC_OBJ) $(BIN): $(DRIVER_OBJS) $(LIB_AR) $(CC) $(HOST_SYSROOT_LDFLAGS) -o $@ $(DRIVER_OBJS) $(LIB_AR) diff --git a/include/cfree/core.h b/include/cfree/core.h @@ -16,6 +16,12 @@ #include <stddef.h> #include <stdint.h> +#if defined(__GNUC__) || defined(__clang__) || defined(__cfree__) +#define CFREE_API __attribute__((visibility("default"))) +#else +#define CFREE_API +#endif + /* Opaque handles shared across component headers. */ typedef struct CfreeCompiler CfreeCompiler; typedef struct CfreeCompileSession CfreeCompileSession; diff --git a/scripts/lib_reloc_defined_prefixes.py b/scripts/lib_reloc_defined_prefixes.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Print disallowed global definitions after a relocatable archive link. + +The check extracts every object from libcfree.a, links them into one +relocatable object, and reports any remaining externally visible definition +whose C symbol name is not in libcfree's public prefix set. +""" + +import argparse +import os +import pathlib +import subprocess +import sys +import tempfile + + +ALLOWED_PREFIXES = ("Cfree", "cfree_", "CFREE") + + +def run(args: list[str], **kwargs) -> subprocess.CompletedProcess[str]: + return subprocess.run(args, check=True, text=True, **kwargs) + + +def nm_lines(path: pathlib.Path, nm: str) -> list[str]: + return run([nm, str(path)], capture_output=True).stdout.splitlines() + + +def parse_nm_defined_globals(lines: list[str]) -> set[str]: + syms: set[str] = set() + for line in lines: + line = line.rstrip() + if not line or line.endswith(":"): + continue + parts = line.split(None, 2) + if len(parts) != 3: + continue + _, type_, name = parts + if type_ != "U" and type_.isupper(): + syms.add(name) + return syms + + +def public_c_name(sym: str) -> str: + if sym.startswith("_"): + stripped = sym[1:] + if stripped.startswith(ALLOWED_PREFIXES): + return stripped + return sym + + +def is_allowed(sym: str) -> bool: + return public_c_name(sym).startswith(ALLOWED_PREFIXES) + + +def link_relocatable( + archive: pathlib.Path, output: pathlib.Path, ar: str, cc: str +) -> None: + archive = archive.resolve() + output = output.resolve() + with tempfile.TemporaryDirectory(prefix="cfree-lib-reloc.") as td: + work = pathlib.Path(td) + run([ar, "x", str(archive)], cwd=work) + objects = sorted(work.glob("*.o")) + if not objects: + raise RuntimeError(f"{archive} did not contain any .o members") + + args = [cc, "-r", "-nostdlib", "-o", str(output)] + args.extend(str(obj) for obj in objects) + output.parent.mkdir(parents=True, exist_ok=True) + run(args, capture_output=True) + + +def disallowed_reloc_defs( + archive: pathlib.Path, output: pathlib.Path, ar: str, cc: str, nm: str +) -> list[str]: + link_relocatable(archive, output, ar, cc) + return sorted( + public_c_name(sym) + for sym in parse_nm_defined_globals(nm_lines(output, nm)) + if not is_allowed(sym) + ) + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("archive", help="Path to libcfree.a") + ap.add_argument( + "--output", + default="build/libcfree.reloc.o", + help="Path for the temporary relocatable-link output", + ) + ap.add_argument("--ar", default=os.environ.get("AR", "ar"), help="ar binary") + ap.add_argument("--cc", default=os.environ.get("CC", "cc"), help="C compiler driver") + ap.add_argument("--nm", default=os.environ.get("NM", "nm"), help="nm binary") + args = ap.parse_args() + + try: + bad = disallowed_reloc_defs( + pathlib.Path(args.archive), + pathlib.Path(args.output), + args.ar, + args.cc, + args.nm, + ) + except (FileNotFoundError, RuntimeError, subprocess.CalledProcessError) as e: + print(f"libcfree relocatable symbol check failed: {e}", file=sys.stderr) + if isinstance(e, subprocess.CalledProcessError) and e.stderr: + print(e.stderr, file=sys.stderr, end="") + return 2 + + for sym in bad: + print(sym) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/link/link_relocatable.c b/src/link/link_relocatable.c @@ -190,6 +190,8 @@ static ObjSymId rel_copy_symbol(Linker* l, ObjBuilder* out, ObjSecId sec = OBJ_SEC_NONE; u64 value = s->value; u64 align = s->common_align; + SymBind bind = (SymBind)s->bind; + SymVis vis = (SymVis)s->vis; ObjSymId id; if (!force_common && s->section_id != OBJ_SEC_NONE && s->section_id < maps[input_idx].nsection) { @@ -201,7 +203,11 @@ static ObjSymId rel_copy_symbol(Linker* l, ObjBuilder* out, value = 0; align = common_align; } - id = obj_symbol_ex(out, s->name, (SymBind)s->bind, (SymVis)s->vis, + if (rel_sym_is_def(s) && (s->vis == SV_HIDDEN || s->vis == SV_INTERNAL)) { + bind = SB_LOCAL; + vis = SV_DEFAULT; + } + id = obj_symbol_ex(out, s->name, bind, vis, force_common ? SK_COMMON : (SymKind)s->kind, sec, value, force_common ? common_size : s->size, align); if (id == OBJ_SYM_NONE) diff --git a/test/test.mk b/test/test.mk @@ -493,12 +493,20 @@ test-musl-rv64: test-glibc-rv64: @$(MAKE) test-glibc CFREE_LIBC_ARCHES=rv64 -# Fail if libcfree.a depends on any external symbol not in the allowlist. -# Drift in either direction (new dep, or stale entry) is a failure. +# Fail if libcfree.a depends on any external symbol not in the allowlist, or +# if a relocatable link exposes non-public global definitions. +# External dependency drift in either direction (new dep, or stale entry) is a +# failure. LIB_DEPS_ACTUAL = build/libcfree.deps.txt +LIB_RELOC = build/libcfree.reloc.o +LIB_RELOC_BAD = build/libcfree.reloc.bad-symbols.txt test-lib-deps: lib @mkdir -p $(dir $(LIB_DEPS_ACTUAL)) @python3 scripts/lib_external_deps.py $(LIB_AR) > $(LIB_DEPS_ACTUAL) @diff -u test/lib_deps.allowlist $(LIB_DEPS_ACTUAL) \ || { echo "libcfree.a external symbol set drifted from test/lib_deps.allowlist"; exit 1; } + @python3 scripts/lib_reloc_defined_prefixes.py $(LIB_AR) \ + --output $(LIB_RELOC) --ar $(AR) --cc $(CC) > $(LIB_RELOC_BAD) + @test ! -s $(LIB_RELOC_BAD) \ + || { echo "libcfree relocatable link exposes non-public symbols"; cat $(LIB_RELOC_BAD); exit 1; }