A Pascal RTL name can hijack a libc function in every C program
- Type: bug (silent wrong answer) — Track C / A (C name resolution)
- Opened: 2026-08-09
- Found by: Track B, adding the Python
mathsurface tolib/rtl/math.pasfor [[feature-rtl-math-surface-gaps]].make lib-testwent RED oncscanf_math, a C test that had nothing to do with the change.
Measured
test/cscanf_math.c and a purpose-built probe, gcc as the oracle:
| call | gcc | pxx, after adding Pow/Log/CopySign to lib/rtl/math.pas |
|---|---|---|
pow(2.0, 10.0) |
1024 |
1 |
pow(2.0, 0.5) |
1.41421 |
1 |
log(4.0) |
1.386294361 |
1.098612289 |
copysign(3.0, -1.0) |
-3 |
0.785398 — that is atan2(1,1) |
atan2(0.5, 1.0) |
0.463647609 |
0.785398163 — that is atan2(1,1) |
isnan, isinf, nan, log10, log2, exp, sqrt, hypot, fmod |
all still correct |
No warning, no error. The C program compiles and prints wrong numbers.
atan2 shipped broken for one commit before this was caught, and the way it
escaped is worth recording: the first canary written for this bug checked only
atan2(1.0, 1.0), whose arguments are SYMMETRIC and whose answer (pi/4) is also
atan(1) — so a hijacked, argument-swapped or argument-ignoring atan2 passes
it. test/cmath_trig_family_b385.c (in make test, not lib-test) caught it,
which is Track T's whole purpose. The canary now uses asymmetric arguments.
A test for a substitution bug must use inputs whose answers DIFFER under the
substitution — a degenerate input tests nothing.
The mechanism
lib/rtl/pxxcio.pas is auto-pulled into every C program (ParseCProgram,
the same way the Pascal driver pulls builtin/textfile), and its uses clause
is:
uses platform, builtinheap, math;
So the whole Pascal math unit is in scope for C name resolution in every C
program ever compiled, and resolution is case-insensitive. Any name added to
that unit can therefore shadow a libc function of the same name.
This is not a new hazard, it is a known one that was worked around rather than
fixed — lib/crtl/src/math.c says so in its own comment:
NOT named log2/log10: those collide case-insensitively with Pascal Log2/Log10 (same silently-broken binding as exp/Exp, b377) — C callers come through the math.h function-like macros.
crtl protected ITSELF by renaming its functions __crtl_log2 / __crtl_log10.
Nothing protects the other direction: the Pascal RTL has no idea it is
publishing into C's namespace, and lib/rtl/math.pas is a file Track B edits
routinely.
Why the priority is not low
The failure mode is a silent wrong answer in unrelated code, which is this project's worst class, and the trigger is an ordinary, correct-looking Track B edit. It fired the same day it was possible to fire. Every future RTL addition is a coin flip against libc's name list until this is fixed.
Track B's workaround for now is to simply not add those names, which costs
math.pow, math.log and math.copysign in NilPy
([[bug-n-math-trunc-and-log-need-frontend-intercepts]] carries them as
frontend intercepts instead) — a real feature loss to dodge a resolution bug.
The diagnostic already exists — it just does not fire on the dangerous case
Found while fixing crtl's nan() (2026-08-09). Adding a PARAMLESS Pascal NaN
next to C's one-argument nan(const char *) produces:
warning: C declaration of 'nan' does not match the Pascal routine 'NaN' which
takes 0 parameter(s), not 1 — binding to the C declaration, not the Pascal
routine
So the compiler already notices the collision, already knows which side the C caller meant, and already does the right thing — when the arities differ.
Every silent case in this ticket is a SAME-ARITY collision: Pow(x,y) against
pow(double,double), Log(x) against log(double), CopySign(x,y) against
copysign(double,double), Atan2(y,x) against atan2(double,double). Same
count, so no warning, and the Pascal routine silently wins.
That narrows the fix considerably: the machinery to detect and correctly resolve these is present, and the rule it applies on arity mismatch ("bind to the C declaration, not the Pascal routine") is exactly the rule that should apply unconditionally for a name declared in a crtl header. Direction 1 below is therefore mostly a matter of dropping the arity precondition — not new analysis.
Directions
- C resolution should prefer crtl over Pascal units for any name declared
in a crtl header — the C program asked for
<math.h>'spow, so<math.h>'spowshould win, and a Pascal unit should never be consulted for it. This is already what happens on an arity mismatch (see above); the precondition is the bug. - Or
pxxcioshould not export its dependencies' namespaces into C — it needsmathfor its own bodies, not on behalf of its callers. - Failing either, an explicit deny-list is a bad third option: it needs maintaining against libc's full name set forever, and gets it wrong silently.
Direction 1 or 2 removes a whole class; the deny-list only postpones it.
Gate
The probe above matching gcc with Pow/Log/CopySign restored to
lib/rtl/math.pas, plus make lib-test and the C suites green.
2026-08-10 — the fix is written and verified, and BLOCKED on a second bug
Attempted the split the user asked for (crtl owns its math; pxxcio drops
uses math). It works: the whole libm surface goes byte-identical to gcc,
isnan(5.5) stops answering TRUE, and the --system-libs=c red (b113) clears.
It cannot land yet. Removing uses math triggers
[[bug-c-crtl-auto-pull-depends-on-the-pascal-preludes-unit-count]] — with only
two units in pxxcio's uses clause the crtl auto-pull silently does not fire, so
stdlib.c and string.c are never emitted and any call into them jumps to
garbage. Any third unit avoids it, which is why this has never been seen:
math merely happens to be the third one today.
The verified implementation is banked on that ticket. Do not land the two
halves separately — adding crtl's four functions while uses math remains
makes things worse (two competing definitions; asin/acos/atan2 return NaN).
Scope note, measured while doing it
Only FOUR names were still crossing the boundary by collision: ceil, floor,
sqrt, fmod. Every other crtl module already reaches shared code through
explicitly prefixed __pxx_* PAL entry points (100+ of them), and crtl already
defines 64 math functions of its own — the migration away from the Pascal
binding has been happening one incident at a time (exp after b377, then
log2/log10, then sin/cos/tan/sinh/cosh/tanh/hypot). Finishing it deliberately
is smaller than the next incident.
The design rule this belongs to (user, 2026-08-10)
"Own language first": a declaration from the caller's own language beats a cross-language match, and that outranks import order. Cross-language binding stays as the FALLBACK — it is the pxx interop feature, and making lookup case-sensitive was explicitly rejected as defeating it. Companion: share what the machine provides, duplicate what the language specifies.
RESOLVED 2026-08-10 — Direction 2 shipped: pxxcio no longer exports a namespace
lib/rtl/pxxcio.pas is now uses platform, builtinheap;. The Pascal math
unit is no longer in scope for C name resolution in every C program, so the
whole hijack surface is gone — not narrowed, gone: a C program sees crtl's
<math.h> and nothing else unless it asks for something else.
The last four names that were still crossing by collision (floor, ceil,
sqrt, fmod) now have real C bodies in lib/crtl/src/math.c. floor/ceil
landed earlier in 11019fe12; sqrt and fmod landed with this change, and
sqrt is deliberately the same algorithm as lib/rtl/math.pas's so the two
languages still agree bit-for-bit on the one function where that matters.
This was blocked for a day by
[[bug-c-crtl-auto-pull-depends-on-the-pascal-preludes-unit-count]], whose real
root cause turned out to be <inttypes.h>'s functions living in stdlib.c —
and it was uses math that had been accidentally papering over it, via a
three-step coincidence (math -> math_ext -> C-imported abs/labs ->
CPullCrtlForPrototypes synthesises #include <stdlib.h> for the whole
program). lib/crtl/src/math.c was riding the same accident for its strtoull
call and now includes <stdlib.h> itself. See that ticket for the full write-up
and the gcc-differential numbers.
Direction 1 ("C resolution should prefer crtl over Pascal units") is NOT
shipped and is still worth doing — it is the general rule, and the user has
since stated it as a design principle: own language first — a declaration from
the caller's own language beats a cross-language match, and that outranks import
order. What shipped here removes the one prelude that made every C program a
victim; a user program that itself does uses math can still collide. That is
why the __crtl_-prefixed names for exp/log2/log10/sin/cos/tan/
sinh/cosh/tanh/hypot stay in place, and why math.c's header now says so.
A Track U decide-* on the resolution rule is the next step.
Verified
- Full libm surface (33 functions x 38 arguments) vs a
gcc -O1 -lmoracle, compared as raw bit patterns: 148 differences, every one also present in thestable_linux_amd64/default/pinnedcontrol. Before the split: 547. isnan(5.5)= 0.pow(2,10)= 1024. b113 links libc, not libm, exit 7.gate.sh quickGREEN,make lib-test,make test-core.
Log
- 2026-08-10 — resolved, commit bab16e1b3.