A function returning a function pointer, refused for having 16 parameters
Reported by frankZ against master with a three-line repro:
void t(void){}
void (*look(int a, int b))(void){ (void)a; (void)b; return t; }
int main(void){ return look(1,2)==t ? 42 : 1; }
pascal26:2: error: C function definition: more than 16 parameters not supported (MAX_PROC_PARAMS)
The real casualty was test/cfn_return_fnptr_b105.c, whose own header says it
exists for sqlite3OsDlSym.
The boundary — and the row that never failed
| shape | before |
|---|---|
void (*p)(void) = t; — fn-pointer VARIABLE |
ok |
int apply(int cb(int), int x) — function-TYPED parameter |
ok |
void (*look(void))(void) { } — zero parameters |
ok |
void (*look(int a))(void) { } |
REFUSED |
void (*look(int a, int b))(void) { } |
REFUSED |
| the same with a forward declaration as well | REFUSED |
The first four rows are frankZ's probes; the zero-parameter row is the one this ticket exists to record. The family had a working member the whole time, so any test written around that member stayed green through the outage.
Cause
ParseCSubroutine declares paramsOverflow and checks it near the bottom,
after both arms of if fnRetIsFunc. It was assigned in the not fnRetIsFunc
arm only — the one that parses a parameter list. A declaration whose declarator
already carried its parameters (RET (*name(params))(args)) takes the other
arm, parses no list, and left the variable untouched; the check then read
whatever the stack held.
Proved by exhaustion over the source, not by instrumentation. The single
paramsOverflow := True sits inside the parameter loop that this path does not
enter, so a True at the check could not have come from an assignment at all.
That also explains the zero-parameter row, which a parse-desync theory does not:
a desync would break that shape too, while a stack slot that happens to hold
zero produces exactly the observed split.
The three siblings the branch sets in BOTH arms — name, nparams,
isVariadic — are why this was the only variable reachable in that state.
Why now
Latent for as long as the fnRetIsFunc arm has existed. d71642873 (busybox
rung 3) added an arm to the parameter loop for function-TYPED parameters, which
changed what sat in that stack slot. A change that cannot touch a variable and
still moves the answer is the tell for an uninitialised read — worth more than
the error text, which named a parameter count for a function with one parameter.
frankZ's own reading, offered as a hypothesis and not a measurement, was that
CParseFnSigGroup was being entered at declarator level and eating the inner
list. It is not entered there — it is not entered at all on this path. The
attribution half of that report was right and useful: the auto-filed range
(last-good 49d0ac95f76d, bad 65b719ab48ae) points at 18b3ec2a6, and it is
not that; 49d0ac95f76d is a tstate commit off seven's line, so the linear
range lies here. Do not bisect toward 18b3ec2a6.
Fix
paramsOverflow := False moved above the branch, with the reasoning at the
site. The duplicate initialiser in the parameter-list arm is removed rather than
left as a second statement of the same fact.
Test
test/c_fn_returning_fnptr_params.c, 6 rows diffed against gcc, wired into
test-core beside cfn_return_fnptr_b105. Row 1 is the zero-parameter shape,
labelled — it is the control that this family had a member which cannot detect
the defect. Rows 5 and 6 call THROUGH the returned pointer with arguments, so a
fix that recovered only the outer parameter count fails there instead of
passing.
--tier quick does not reach cfn_return_fnptr_b105; Track T's fuller tier is
what found this, which is the sampling model working as designed.
Log
- 2026-09-02 — resolved; the fix is commit 2148d95fa (the same commit carried the ticket and the test).