sizeof(p->arr) returns the POINTER size, not the array size — silent buffer overflow
- Type: bug (C frontend, silent memory corruption) — Track C (cfront)
- Found: 2026-08-02 by the Track B agent, via vendored pdfgen writing a
truncated PDF
/CreationDate. Filed, not fixed:compiler/**is not Track B's to edit. - Severity: this one writes past the end of an array. Filed to
urgent/.
Minimal repro
#include <stdio.h>
struct info { char title[64]; char date[64]; };
struct doc { struct info *info; };
int main(void) {
struct info i; struct doc d; d.info = &i;
printf("%d\n", (int)sizeof(i.date)); /* 64 correct */
printf("%d\n", (int)sizeof(d.info->date)); /* 8 WRONG, want 64 */
return 0;
}
| expression | pxx | gcc |
|---|---|---|
sizeof(struct info) |
128 | 128 |
sizeof(i.date) (direct member) |
64 | 64 |
sizeof(d.info->date) (through a pointer) |
8 | 64 |
sizeof(pd->info->date) |
8 | 64 |
So the array member is decaying to a pointer before sizeof is applied, but
only when reached through ->. Direct . access is correct, which is why this
survived: the obvious test passes.
It overflows, it does not merely truncate
sizeof(p->buf) is one of C's most common idioms — snprintf(p->name, sizeof(p->name), ...), memset, strncpy, read, fgets. When the real
array is SMALLER than a pointer, the bogus 8 is larger than the buffer and the
bound becomes an overflow:
struct small { char buf[4]; int guard; };
struct holder { struct small *s; };
...
sm.guard = 0x41414141;
memset(h.s->buf, 'X', sizeof(h.s->buf)); /* writes 8 bytes into 4 */
| pxx | gcc | |
|---|---|---|
sizeof(h.s->buf) |
8 | 4 |
guard after the memset |
0x58585858 (clobbered) | 0x41414141 |
The adjacent field is overwritten with 'XXXX'. Nothing warns, nothing crashes.
For arrays larger than 8 the same bug truncates instead — both directions are
silently wrong.
How it surfaced (the plausible-wrong-value shape)
Vendored pdfgen (lib/vendor/pdfgen/pdfgen.c:1049):
strftime(obj->info->date, sizeof(obj->info->date), "%Y%m%d%H%M%SZ", &tm);
date is char[64]; sizeof gave 8, so strftime got a buffer bound of 8 and
the PDF came out with a truncated date. Against a gcc-built pdfgen oracle
on the same input:
/CreationDate |
|
|---|---|
| gcc-built pdfgen | (D:20260802102817Z) — 15 chars |
| pxx-built pdfgen | (D:2026080) — 7 chars |
The PDF stayed structurally valid and opened fine, which is exactly the failure mode this repo's debugging note describes: not a crash, a plausible wrong value far from the cause.
Confirmed by one-token substitution
Not inferred — isolated. pdfgen compiled as a pure C translation unit
(#include "pdfgen.c", no Pascal anywhere, zero warnings), then rebuilt
with the single expression sizeof(obj->info->date) replaced by the literal
64 and nothing else changed:
| build | /CreationDate |
|---|---|
| pxx, unmodified | (D:2026080) |
pxx, sizeof(...) → 64 |
(D:20260802083149Z) |
| gcc oracle | (D:20260802102817Z) |
One token, and the bug goes away. The pure-C build also settles attribution:
the time/bcmp Pascal-binding warnings do not appear there at all, and the
date is truncated identically with and without them.
Ruled out while narrowing
Both of these were the initial suspects and both are innocent — recorded so the next reader does not re-walk them:
time(&now)in a pure C compile writes its out-parameter correctly and matches gcc, despite the arity warning that appears in mixed Pascal+C builds (that warning is real but separate — [[bug-b-crtl-host-header-and-arity-mismatches-building-pdfgen]]).strftime/localtime_rproduce the correct 15-character string in a pure C compile. (One unrelated nit noticed: pxx'slocaltime_rreturns UTC where gcc's applies the local timezone — separate, minor, not this bug.)
The cause is sizeof, and only through ->.
Gate
sizeof(p->arr) equals the declared array size for arrays both larger and
smaller than a pointer, through one and several levels of ->, matching gcc;
plus the overflow probe above showing the adjacent field intact. A gcc-built
pdfgen oracle comparison on /CreationDate is the end-to-end check.
Resolution 2026-08-03 (claude-C@opus5)
ParseCSizeof's member path handled exactly ONE link. -> and . both lex as
tkDot (clexer maps -> to it), so sizeof(d.info->date) matched d .
info, sized info — a pointer, 8 — and the balanced-) scan swallowed
->date without anyone noticing. Direct . was right, which is why the obvious
test passed.
Rewritten as a walk over the whole chain: at each link, if another tkDot
follows the field, step into that field's record (RecFieldPtrElemRec for a
pointer field, RecFieldRecId for a record one) and continue; otherwise run the
existing terminal computation, which already handled the array / pointer /
record / [i]-element cases correctly. A link that is neither a record nor a
pointer-to-record ends the walk with recId = REC_NONE, leaving the default
pointer-size answer rather than a confidently wrong one.
Verified against gcc
Every row of the ticket's tables, diffed against a gcc build of the same source:
| before | after | gcc | |
|---|---|---|---|
sizeof(d.info->date) |
8 | 64 | 64 |
sizeof(pd->info->date) |
8 | 64 | 64 |
sizeof(h.s->buf) (char[4]) |
8 | 4 | 4 |
guard after the memset |
0x58585858 | 0x41414141 | 0x41414141 |
End-to-end, the ticket's oracle: vendored pdfgen built with pxx now writes
/CreationDate (D:20260803124231Z) — 15 chars, same shape as the gcc-built
oracle's (D:20260803124220Z) — where it used to write (D:2026080).
Regression test test/csizeof_member_chain_through_pointer.c (gated, exit 42):
two/three/four-link chains, the smaller-than-a-pointer overflow probe with an
adjacent-field guard, and an element-through-the-chain case
(sizeof(d.info->date)/sizeof(d.info->date[0]) = 64). It returns 42 under
both gcc and pxx.
tools/gate.sh quick GREEN (self-host fixedpoint, testmgr quick, FPC canary).
Log
- 2026-08-03 — resolved, commit a51cbe7d3.