commit e55665b3c5ce178b17e7838c903429bf62eaf2cc
parent 09bf3f299ea580495f0b4ce4a88dcacfe7a10f31
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 5 Jun 2026 21:34:21 -0700
Fix O1 label-address block liveness
Diffstat:
5 files changed, 104 insertions(+), 47 deletions(-)
diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md
@@ -5,44 +5,6 @@ fixed, remove it instead of checking it off or keeping a closed entry.
Add new deferred fixes below as they are discovered.
-## O1: `&&label` whose address is taken but never `goto`'d → undefined `.Lcfblk.N`
-
-At `-O1`, taking a label's address with the GNU labels-as-values extension
-(`&&label`) when that label is **not** also a computed-goto target produces a
-dangling reference to an internal control-flow-block symbol: the optimizer
-elides/merges the block but the address-of relocation survives, so the output
-has a relocation against an undefined `.Lcfblk.N`. The JIT path reports
-`fatal: link: undefined reference to '.Lcfblk.6'`; an emitted object carries an
-undefined `.Lcfblk.N` (visible via `kit nm`). O0 is fine (the block is kept).
-The plain `goto *p; done:` form is also fine because `done` is a real goto
-target, so its block is retained.
-
-This is distinct from the fixed `.Lkit_jt.*` / `.Lkit_ro.*` DCE leak class:
-those were unreferenced orphan data symbols minted before O1 replay. `.Lcfblk.N`
-is a referenced MC/code-label symbol; the object has live text relocations to
-it, so the linker is correctly rejecting the undefined symbol.
-
-Minimal repro:
-
-```c
-int p(void) { L: return &&L != 0; }
-```
-
-This emits a live text relocation against an undefined `.Lcfblk.N` at O1 and
-`kit run -O1` fails to link. The same construct inside a dead inline is clean:
-
-```c
-static inline void *q(void) { L: return &&L; }
-int p(void) { return 0; }
-```
-
-Likely in the O1 CFG/block-merge passes (`src/opt/pass_cfg.c` / block dedup):
-the target block of `IR_LOAD_LABEL_ADDR` must be treated as address-taken/live
-through CFG cleanup and native emission, or any CFG merge/elision must retarget
-the label-address relocation to the surviving block label. Found while writing
-the backtrace anchor test (doc/plan/BACKTRACE.md); worked around there by
-anchoring on a function address instead of `&&label`.
-
## `__kit_syscallN` (`rt/include/kit/syscall.h`) is declared/documented but unimplemented
The header declares `__kit_syscall0..6` and documents them as lowering to the
diff --git a/src/opt/pass_analysis.c b/src/opt/pass_analysis.c
@@ -217,11 +217,31 @@ static void block_list_add_unique(Arena* arena, OptBlockList* list, u32 block) {
block_list_add(arena, list, block);
}
+static void order_dfs(OptAnalysis* a, u32 block);
+
+static void order_label_addr_target(OptAnalysis* a, const Inst* in) {
+ switch ((IROp)in->op) {
+ case IR_LOAD_LABEL_ADDR:
+ order_dfs(a, (u32)in->extra.imm);
+ break;
+ case IR_LOCAL_STATIC_DATA_LABEL_ADDR: {
+ CgIrLocalStaticLabelAux* aux =
+ (CgIrLocalStaticLabelAux*)in->extra.aux;
+ if (aux) order_dfs(a, (u32)aux->target);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
static void order_dfs(OptAnalysis* a, u32 block) {
if (block >= a->nblocks || a->reachable[block]) return;
a->reachable[block] = 1;
Block* bl = &a->f->blocks[block];
for (u32 i = 0; i < bl->nsucc; ++i) order_dfs(a, bl->succ[i]);
+ for (u32 i = 0; i < bl->ninsts; ++i)
+ order_label_addr_target(a, &bl->insts[i]);
a->po[a->npo] = block;
a->po_index[block] = a->npo;
++a->npo;
diff --git a/src/opt/pass_cfg.c b/src/opt/pass_cfg.c
@@ -62,25 +62,50 @@ static int scope_control_succ_count(const Block* bl, const Inst* in,
}
}
+static void push_reachable(Func* f, u8* reachable, u32* stack, u32* sp,
+ u32 block) {
+ if (block < f->nblocks && !reachable[block]) {
+ reachable[block] = 1;
+ stack[(*sp)++] = block;
+ }
+}
+
+static void mark_label_addr_targets_reachable(Func* f, const Inst* in,
+ u8* reachable, u32* stack,
+ u32* sp) {
+ switch ((IROp)in->op) {
+ case IR_LOAD_LABEL_ADDR:
+ push_reachable(f, reachable, stack, sp, (u32)in->extra.imm);
+ break;
+ case IR_LOCAL_STATIC_DATA_LABEL_ADDR: {
+ CgIrLocalStaticLabelAux* aux =
+ (CgIrLocalStaticLabelAux*)in->extra.aux;
+ if (aux) push_reachable(f, reachable, stack, sp, (u32)aux->target);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
static u8* mark_reachable(Func* f) {
u8* reachable = arena_zarray(f->arena, u8, f->nblocks ? f->nblocks : 1u);
if (f->entry >= f->nblocks) return reachable;
u32* stack = arena_array(f->arena, u32, f->nblocks ? f->nblocks : 1u);
u32 sp = 0;
- reachable[f->entry] = 1;
- stack[sp++] = f->entry;
+ push_reachable(f, reachable, stack, &sp, f->entry);
while (sp) {
u32 b = stack[--sp];
if (b >= f->nblocks) continue;
Block* bl = &f->blocks[b];
for (u32 s = 0; s < bl->nsucc; ++s) {
u32 t = bl->succ[s];
- if (t < f->nblocks && !reachable[t]) {
- reachable[t] = 1;
- stack[sp++] = t;
- }
+ push_reachable(f, reachable, stack, &sp, t);
}
+ for (u32 i = 0; i < bl->ninsts; ++i)
+ mark_label_addr_targets_reachable(f, &bl->insts[i], reachable, stack,
+ &sp);
}
return reachable;
}
diff --git a/src/opt/pass_native_emit.c b/src/opt/pass_native_emit.c
@@ -1305,9 +1305,8 @@ static void emit_block(NativeEmitCtx* e, u32 block, u32 order_index,
if (block >= e->f->nblocks) return;
if (!e->label_placed[block]) {
e->label_placed[block] = 1u;
- if (block != e->f->entry)
- e->target->label_place(e->target,
- ensure_label(e, block, (SrcLoc){0, 0, 0}));
+ e->target->label_place(e->target,
+ ensure_label(e, block, (SrcLoc){0, 0, 0}));
}
Block* bl = &e->f->blocks[block];
int is_last_block = order_index + 1u == e->f->emit_order_n;
diff --git a/test/opt/run.sh b/test/opt/run.sh
@@ -117,4 +117,55 @@ if grep -Eq '[[:space:]][uU][[:space:]]+\.Lkit_ro' "$WORK/live_ro.nm" ||
exit 1
fi
+cat > "$WORK/live_label_addr.c" <<'EOF'
+int p(void) { L: return &&L != 0 ? 42 : 7; }
+int q(void) { void *x = &&L; return x != 0 ? 11 : 3; L: return 5; }
+EOF
+"$KIT" cc -O1 -c "$WORK/live_label_addr.c" \
+ -o "$WORK/live_label_addr.o" > "$WORK/live_label_addr.cc.out" 2>&1
+"$KIT" nm "$WORK/live_label_addr.o" > "$WORK/live_label_addr.nm" 2>&1
+if grep -Eq '[[:space:]][uU][[:space:]]+\.Lcfblk' "$WORK/live_label_addr.nm"; then
+ printf 'O1 label-address check FAILED: live &&label target is undefined:\n' >&2
+ sed 's/^/ | /' "$WORK/live_label_addr.nm" >&2
+ exit 1
+fi
+if "$KIT" run -O1 -e p "$WORK/live_label_addr.c" \
+ > "$WORK/live_label_addr.run.out" 2>&1; then
+ rc=0
+else
+ rc=$?
+fi
+if [ "$rc" -ne 42 ]; then
+ printf 'O1 label-address runtime check FAILED: exit %d (want 42)\n' "$rc" >&2
+ sed 's/^/ | /' "$WORK/live_label_addr.run.out" >&2
+ exit 1
+fi
+if "$KIT" run -O1 -e q "$WORK/live_label_addr.c" \
+ > "$WORK/live_label_addr_q.run.out" 2>&1; then
+ rc=0
+else
+ rc=$?
+fi
+if [ "$rc" -ne 11 ]; then
+ printf 'O1 label-address runtime check FAILED: q exit %d (want 11)\n' \
+ "$rc" >&2
+ sed 's/^/ | /' "$WORK/live_label_addr_q.run.out" >&2
+ exit 1
+fi
+
+cat > "$WORK/dead_inline_label_addr.c" <<'EOF'
+static inline void *q(void) { L: return &&L; }
+int p(void) { return 0; }
+EOF
+"$KIT" cc -O1 -c "$WORK/dead_inline_label_addr.c" \
+ -o "$WORK/dead_inline_label_addr.o" \
+ > "$WORK/dead_inline_label_addr.cc.out" 2>&1
+"$KIT" nm "$WORK/dead_inline_label_addr.o" \
+ > "$WORK/dead_inline_label_addr.nm" 2>&1
+if grep -q '\.Lcfblk' "$WORK/dead_inline_label_addr.nm"; then
+ printf 'O1 label-address check FAILED: dead inline leaked .Lcfblk:\n' >&2
+ sed 's/^/ | /' "$WORK/dead_inline_label_addr.nm" >&2
+ exit 1
+fi
+
printf 'tiny-inline: ok\n'