kit

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

commit db75a878a607b07c93cb48829fec994292984aa5
parent 1604e6a34af780121e05fcdf883b8caf90295d8c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 08:12:50 -0700

feat(cc): -ftrivial-auto-var-init=zero|uninitialized

Implement the clang/gcc hardening flag in kit cc. A new codegen-policy
field KitCodeOptions.trivial_auto_var_init (KitAutoVarInit enum) threads
from the cc flag parse through KitFrontendCompileOptions.code into the C
frontend (parse_c) and onto the parser; parse_init_declarator zero-inits
any automatic variable that has no explicit initializer via zero_init_at
(arrays/structs/unions/scalars; VLAs and incomplete arrays left alone).

=zero and =uninitialized are supported; =pattern is parsed but rejected as
not-yet-supported (needs a non-zero per-scalar byte fill incl. FP bits).

This closes the bootstrap hardening gap: the mk/flags.mk AUTO_INIT_CFLAGS
probe (CC -ftrivial-auto-var-init=zero -x c -E /dev/null) now passes under
CC=kit cc, so make bootstrap-* applies the same zero-init hardening that
masks the latent uninit-stack-read flake.

test-driver-cc 114/0 (6 new autovar cases incl. a functional deterministic
zero-init run); test-toy 0 fail (no-flag path byte-identical; the -2 pass
vs baseline is T1.3's 96/100 .cbackend.skip); test-cg-api 211/0.

Diffstat:
Mdoc/plan/TODO.md | 21++++++++++-----------
Mdriver/cmd/cc.c | 21+++++++++++++++++++++
Minclude/kit/core.h | 13+++++++++++++
Mlang/c/c.c | 3++-
Mlang/c/parse/parse.c | 11++++++++++-
Mlang/c/parse/parse.h | 3++-
Mlang/c/parse/parse_init.c | 4++--
Mlang/c/parse/parse_priv.h | 3+++
Mtest/driver/run.sh | 47+++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 110 insertions(+), 16 deletions(-)

diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md @@ -40,17 +40,16 @@ Add new deferred fixes below as they are discovered. repro (`kit run -O0 test/toy/cases/54_scalar_intrinsics.toy` in a loop) to get the exact file:line, then initialize that local. The hardening flag can stay regardless. -- **Support `-ftrivial-auto-var-init=zero|pattern` in `kit cc`.** The host build now - hardens with this flag (mk/flags.mk `AUTO_INIT_CFLAGS`), but it is probed and only - applied when `$(CC)` accepts it — so it is silently dropped for bootstrap stages - where `$(CC)=kit cc`, which does not yet implement the flag. Teach the C frontend to - parse `-ftrivial-auto-var-init=zero` / `=pattern` / `=uninitialized` and emit the - corresponding implicit zero/pattern store for every automatic variable whose storage - is not fully written before first read (the same semantics as clang/gcc). This (a) - extends the same hardening to kit-built-by-kit, and (b) makes the latent uninit-read - bug above suppressed in self-hosted builds too. Until then a bootstrap-only build - (`make bootstrap-*`, where `$(CC)=kit cc`) does not get the hardening, so it could - still trip the latent flake. +- **`-ftrivial-auto-var-init=pattern` in `kit cc`.** `=zero` and `=uninitialized` + are implemented: the C frontend zero-inits every automatic variable that has no + explicit initializer (`KitCodeOptions.trivial_auto_var_init`, threaded through + `parse_c` → `parse_init_declarator` → `zero_init_at`), and the mk/flags.mk + `AUTO_INIT_CFLAGS` probe now passes under `$(CC)=kit cc`, so bootstrap stages get + the same hardening (closing the gap that left `make bootstrap-*` exposed to the + latent uninit-read flake). `=pattern` is parsed but rejected as unsupported — + implementing it needs a non-zero byte-fill for every scalar leaf (including the + FP bit pattern), i.e. generalizing `zero_init_at` to a fill value. Lower priority: + the hardening uses `=zero`. - **c_target `memory.grow` backing-store bug.** `memory_grow_large/C` fails (`expected 42 got 139`, SIGSEGV): a `(memory 1 300)` module grows to 300 pages diff --git a/driver/cmd/cc.c b/driver/cmd/cc.c @@ -92,6 +92,7 @@ typedef struct CcOptions { int debug_info; /* -g */ int function_sections; /* -ffunction-sections */ int data_sections; /* -fdata-sections */ + int auto_var_init; /* -ftrivial-auto-var-init= (KitAutoVarInit) */ int lto; /* -flto/-fno-lto */ int warnings_are_errors; /* -Werror */ uint32_t max_errors; /* -fmax-errors=N */ @@ -726,6 +727,25 @@ static int cc_parse(int argc, char** argv, CcOptions* o) { o->data_sections = 1; continue; } + if (driver_strneq(a, "-ftrivial-auto-var-init=", 24)) { + const char* mode = a + 24; + if (driver_streq(mode, "zero")) { + o->auto_var_init = KIT_AUTOVAR_ZERO; + } else if (driver_streq(mode, "uninitialized")) { + o->auto_var_init = KIT_AUTOVAR_UNINIT; + } else if (driver_streq(mode, "pattern")) { + driver_errf(CC_TOOL, + "-ftrivial-auto-var-init=pattern is not yet supported; " + "use =zero"); + return 1; + } else { + driver_errf(CC_TOOL, "-ftrivial-auto-var-init=: unknown mode '%s' " + "(expected zero, pattern, or uninitialized)", + mode); + return 1; + } + continue; + } if (driver_streq(a, "-fno-data-sections")) { o->data_sections = 0; continue; @@ -1761,6 +1781,7 @@ static void cc_fill_c_opts(const CcOptions* o, KitCCompileOptions* copts) { copts->code.emit_asm_source = o->emit_asm_source ? true : false; copts->code.function_sections = o->function_sections ? true : false; copts->code.data_sections = o->data_sections ? true : false; + copts->code.trivial_auto_var_init = (uint8_t)o->auto_var_init; copts->code.lto = o->lto ? true : false; copts->code.epoch = o->epoch; copts->code.path_map = o->npath_map ? o->path_map : NULL; diff --git a/include/kit/core.h b/include/kit/core.h @@ -257,6 +257,15 @@ typedef struct KitPathPrefixMap { const char* new_prefix; } KitPathPrefixMap; +/* Hardening policy for automatic variables with no explicit initializer + * (clang/gcc -ftrivial-auto-var-init). UNINIT keeps the C default (no implicit + * store); ZERO emits a zero store; PATTERN emits a recognizable byte pattern. */ +typedef enum KitAutoVarInit { + KIT_AUTOVAR_UNINIT = 0, + KIT_AUTOVAR_ZERO = 1, + KIT_AUTOVAR_PATTERN = 2 +} KitAutoVarInit; + typedef struct KitCodeOptions { int opt_level; /* 0 direct; 1+ require KIT_OPT_ENABLED */ bool debug_info; /* emit source/debug records when supported */ @@ -279,6 +288,10 @@ typedef struct KitCodeOptions { * per-symbol section when no explicit frontend section was requested. */ bool function_sections; bool data_sections; + /* KitAutoVarInit: implicitly initialize automatic variables that have no + * explicit initializer. Frontends that lower locals honor it; others ignore + * it. 0 (UNINIT) is the C default. */ + uint8_t trivial_auto_var_init; /* Cross-translation-unit LTO. Drivers that have all sources up front use * this to stage semantic frontends into one KitCg session and finalize once. * Separate compilation still emits an ordinary object until serialized IR diff --git a/lang/c/c.c b/lang/c/c.c @@ -105,7 +105,8 @@ static KitStatus c_frontend_compile_cg(KitFrontendState* frontend, kit_frontend_metrics_scope_end(c, "compile.c.pp_push_input"); kit_frontend_metrics_scope_begin(c, "compile.c.parse_codegen"); - parse_c(c, pool, pp, decls, cg, (KitSymVis)fe_opts->code.default_visibility); + parse_c(c, pool, pp, decls, cg, (KitSymVis)fe_opts->code.default_visibility, + (int)fe_opts->code.trivial_auto_var_init); kit_frontend_metrics_scope_end(c, "compile.c.parse_codegen"); kit_frontend_metrics_scope_begin(c, "compile.c.cleanup"); diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -922,6 +922,14 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { pcg_store(p); pcg_drop(p); } + } else if (p->auto_var_init == KIT_AUTOVAR_ZERO && var_ty && + !(var_ty->kind == TY_ARRAY && var_ty->arr.incomplete)) { + /* -ftrivial-auto-var-init=zero: an automatic variable with no explicit + * initializer is implicitly zeroed (clang/gcc semantics). When the + * variable is fully written before any read the store is dead and the + * optimizer drops it. VLAs and incomplete arrays are left alone. */ + pcg_set_loc(p, loc); + zero_init_at(p, s, var_ty, 0, var_ty); } } } @@ -1512,7 +1520,7 @@ static u8 parser_default_visibility(KitSymVis vis) { } void parse_c(Compiler* c, Pool* pool, Pp* pp, DeclTable* decls, CG* cg, - KitSymVis default_visibility) { + KitSymVis default_visibility, int auto_var_init) { Parser p; CKw i; u32 syscall_i; @@ -1525,6 +1533,7 @@ void parse_c(Compiler* c, Pool* pool, Pp* pp, DeclTable* decls, CG* cg, p.abi = c; p.pool = pool; p.default_visibility = parser_default_visibility(default_visibility); + p.auto_var_init = (u8)auto_var_init; for (i = (CKw)1; i < KW_COUNT; ++i) { p.kw_sym[i] = kit_sym_intern(p.pool->c, kit_slice_cstr(kw_names[i])); diff --git a/lang/c/parse/parse.h b/lang/c/parse/parse.h @@ -7,6 +7,7 @@ /* C11 frontend. Reads preprocessed tokens, records C declarations, and drives * the public CG API for executable code and object data. */ -void parse_c(Compiler*, Pool*, Pp*, DeclTable*, CG*, KitSymVis); +void parse_c(Compiler*, Pool*, Pp*, DeclTable*, CG*, KitSymVis, + int auto_var_init); #endif diff --git a/lang/c/parse/parse_init.c b/lang/c/parse/parse_init.c @@ -286,8 +286,8 @@ void emit_struct_copy_into_slot(Parser* p, FrameSlot dst_slot, } /* Recursively zero-initialize the sub-object at `offset` of type `ty`. */ -static void zero_init_at(Parser* p, FrameSlot slot, const Type* arr_ty, - u32 offset, const Type* ty) { +void zero_init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, + const Type* ty) { if (ty->kind == TY_ARRAY) { u32 esz = c_abi_sizeof(p->abi, ty->arr.elem); for (u32 i = 0; i < ty->arr.count; ++i) { diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h @@ -212,6 +212,7 @@ typedef struct Parser { KitCompiler* abi; Pool* pool; u8 default_visibility; /* SymVis */ + u8 auto_var_init; /* KitAutoVarInit: implicit init for uninit locals */ const Type** cg_type_stack; u8* cg_value_flags; @@ -563,6 +564,8 @@ KitCgSym emit_string_literal_to_rodata(Parser* p, const u8* bytes, /* parse_init.c */ void init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, const Type* ty); +void zero_init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, + const Type* ty); void parse_static_init_at(Parser* p, u8* buf, u32 buflen, u32 offset, const Type* ty); void define_static_object(Parser* p, ObjSymId sym, ObjSecId section_id, diff --git a/test/driver/run.sh b/test/driver/run.sh @@ -1196,5 +1196,52 @@ else skip_test "run-backtrace-source" "host cannot natively kit-run" fi +# ---- -ftrivial-auto-var-init: zero-init uninitialized automatic variables ---- +# =pattern is parsed but not yet implemented; =bogus is a usage error; =zero and +# =uninitialized compile cleanly. +run_fail "cc-autovar-pattern-rejected" "$KIT" cc \ + -ftrivial-auto-var-init=pattern -c "$work/main.c" -o "$work/avi-pat.o" +contains "cc-autovar-pattern-msg" "$work/cc-autovar-pattern-rejected.err" \ + "not yet supported" +run_fail "cc-autovar-bogus-rejected" "$KIT" cc \ + -ftrivial-auto-var-init=bogus -c "$work/main.c" -o "$work/avi-bog.o" +run_ok "cc-autovar-zero-compiles" "$KIT" cc \ + -ftrivial-auto-var-init=zero -c "$work/main.c" -o "$work/avi-zero.o" +run_ok "cc-autovar-uninit-compiles" "$KIT" cc \ + -ftrivial-auto-var-init=uninitialized -c "$work/main.c" -o "$work/avi-uninit.o" + +# Functional: an uninitialized local array read back under =zero is all-zero, so +# `s` stays 100 and the exit status is deterministic. Gated on native run. +cat > "$work/avi-zero-run.c" <<'SRC' +int main(void) { + int a[64]; + int s = 100; + int* p = a; /* address-taken: forces a real stack slot, not a value */ + for (int i = 0; i < 64; i++) s += p[i]; + return s & 0xff; +} +SRC +if "$KIT" cc "$work/main.c" -o "$work/avi-probe" \ + > "$work/avi-probe.out" 2> "$work/avi-probe.err" && + "$work/avi-probe" > /dev/null 2>&1; then + if "$KIT" cc -ftrivial-auto-var-init=zero "$work/avi-zero-run.c" \ + -o "$work/avi-zero-run" \ + > "$work/avi-zero-run.out" 2> "$work/avi-zero-run.err"; then + "$work/avi-zero-run" + avi_rc=$? + if [ "$avi_rc" -eq 100 ]; then + ok "cc-autovar-zero-is-deterministic" + else + printf 'exit=%s want=100\n' "$avi_rc" > "$work/avi-zero-run.diag" + not_ok "cc-autovar-zero-is-deterministic" "$work/avi-zero-run.diag" + fi + else + not_ok "cc-autovar-zero-is-deterministic" "$work/avi-zero-run.err" + fi +else + skip_test "cc-autovar-zero-is-deterministic" \ + "host cannot natively run kit cc output" +fi + kit_summary driver-cc kit_exit