Skip to content

cc: a compound literal did not zero the members its list leaves out - #628

Merged
staalmannen merged 1 commit into
mainfrom
claude/upgrade-ape-c-library-mmZGd
Oct 14, 2026
Merged

staalmannen merged 1 commit into
mainfrom
claude/upgrade-ape-c-library-mmZGd

Conversation

@staalmannen

Copy link
Copy Markdown
Owner

MEASURED ON THE VM: `initzero-test' reports 6 failures under pcc and 0 under gcc, so muon's crash is kencc's.

AND IT IS ONE MISSING CALL. compoundlit()' lowers a literal to a hidden auto -- adecl(CAUTO,...) -- and calls doinit(), which assigns only the members the list names. cc.y's adlist', an ordinary T x = {...}', is doinit() FOLLOWED BY contig(), and contig() is exactly "zero every byte the initialiser did not cover". compoundlit() was the one caller of doinit() in the compiler that never had the second half, which is why no ordinary C in this tree ever tripped over it. C99 6.7.8p21: an omitted member is initialised as an object with static storage duration would be.

THE PATTERN WAS THE DIAGNOSIS, NOT THE COUNT. .len' was garbage and .s' zero through a POINTER, and the other way round into a LOCAL -- the same two gaps of the same struct, in one program -- and section 6b's array literal passed outright. A compiler that simply never zeroed would have failed uniformly; that is an uninitialised slot. And section 5, a DECLARATION, passed for a real reason rather than by luck: it goes through adlist, so it has always had contig(). The asymmetry between the two productions is the whole bug.

contig() COULD NOT SIMPLY BE CALLED. Its bulk arm emits an ODWHILE -- a STATEMENT -- and everything compoundlit() builds has to survive cgen() in EXPRESSION context, since the literal's value is an operand. clitzero() reuses contig's front and back trims verbatim and spells the loop out as straight-line stores. The cost is one store per uncovered pointer-sized word: linear, recorded rather than capped, because a cap would have to reject legal C or fall back to a statement this expression cannot hold.

TWO THINGS THAT WOULD HAVE BEEN EASY TO GET WRONG, both written down beside the code. It must run BEFORE the existing OLIST->OCOMMA flattening: covered1() descends OLIST and OASI and nothing else, so against an OCOMMA tree every member reads as uncovered and would be zeroed on top of its own initialiser. And zeroing a PARTIALLY covered word is correct and deliberate -- initz() prepends, so every zero store runs before the member stores, which is the ordering contig() already relies on. CAUTO only: a file-scope literal is CSTATIC, doinit() emits DATA records and BSS is already zero.

sys/lib/tests/clitzero-model.py is the instrument, beside sualign-model.py and for the same reason -- the first build of any cc/ change is the VM's. It EXTRACTS the real function from dcl.c rather than restating it, and asks which bytes it stores zero into over six layouts by seven negative FRAME OFFSETS, since v is negative and every %' in that function is a negative modulo -- one sign error writes zeros over a neighbouring local. It has no external oracle the way sualign-model has the acid dump, so its CONTROL is a deliberate break: a copy with the tail loop removed must report a gap, and it does. All cases ok, nothing stored outside the object, and a fully-covered literal costs no stores at all, so (int){i}' and `&(T){...}' keep their addable path.

Not compiled anywhere: kencc needs Plan 9's <u.h>/<libc.h>, so the first build is the VM's -- the weaker position -J, the `auto' grammar fix and -C all shipped from.

Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs

MEASURED ON THE VM: `initzero-test' reports 6 failures under pcc and 0
under gcc, so muon's crash is kencc's.

AND IT IS ONE MISSING CALL.  `compoundlit()' lowers a literal to a
hidden auto -- adecl(CAUTO,...) -- and calls doinit(), which assigns
only the members the list names.  `cc.y's `adlist', an ordinary
`T x = {...}', is doinit() FOLLOWED BY contig(), and contig() is
exactly "zero every byte the initialiser did not cover".  compoundlit()
was the one caller of doinit() in the compiler that never had the
second half, which is why no ordinary C in this tree ever tripped over
it.  C99 6.7.8p21: an omitted member is initialised as an object with
static storage duration would be.

THE PATTERN WAS THE DIAGNOSIS, NOT THE COUNT.  `.len' was garbage and
`.s' zero through a POINTER, and the other way round into a LOCAL --
the same two gaps of the same struct, in one program -- and section
6b's array literal passed outright.  A compiler that simply never
zeroed would have failed uniformly; that is an uninitialised slot.
And section 5, a DECLARATION, passed for a real reason rather than by
luck: it goes through adlist, so it has always had contig().  The
asymmetry between the two productions is the whole bug.

contig() COULD NOT SIMPLY BE CALLED.  Its bulk arm emits an ODWHILE --
a STATEMENT -- and everything compoundlit() builds has to survive
cgen() in EXPRESSION context, since the literal's value is an operand.
clitzero() reuses contig's front and back trims verbatim and spells the
loop out as straight-line stores.  The cost is one store per uncovered
pointer-sized word: linear, recorded rather than capped, because a cap
would have to reject legal C or fall back to a statement this
expression cannot hold.

TWO THINGS THAT WOULD HAVE BEEN EASY TO GET WRONG, both written down
beside the code.  It must run BEFORE the existing OLIST->OCOMMA
flattening: covered1() descends OLIST and OASI and nothing else, so
against an OCOMMA tree every member reads as uncovered and would be
zeroed on top of its own initialiser.  And zeroing a PARTIALLY covered
word is correct and deliberate -- initz() prepends, so every zero store
runs before the member stores, which is the ordering contig() already
relies on.  CAUTO only: a file-scope literal is CSTATIC, doinit() emits
DATA records and BSS is already zero.

sys/lib/tests/clitzero-model.py is the instrument, beside
sualign-model.py and for the same reason -- the first build of any cc/
change is the VM's.  It EXTRACTS the real function from dcl.c rather
than restating it, and asks which bytes it stores zero into over six
layouts by seven negative FRAME OFFSETS, since v is negative and every
`%' in that function is a negative modulo -- one sign error writes
zeros over a neighbouring local.  It has no external oracle the way
sualign-model has the acid dump, so its CONTROL is a deliberate break:
a copy with the tail loop removed must report a gap, and it does.  All
cases ok, nothing stored outside the object, and a fully-covered
literal costs no stores at all, so `(int){i}' and `&(T){...}' keep
their addable path.

Not compiled anywhere: kencc needs Plan 9's <u.h>/<libc.h>, so the
first build is the VM's -- the weaker position -J, the `auto' grammar
fix and -C all shipped from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs
@staalmannen
staalmannen merged commit 0ac8c62 into main Oct 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants