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:
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