← board

One cause, three refusals

Measured 2026-09-02, binary sha256 32a2ce1d9806, busybox 1.36.1 at 394 applets / 521 TUs.

/* bd1.c -- FAILS: pascal26:25: error: stray token at top level: '__BEGIN_DECLS' */
#include <sys/xattr.h>
int main(void){return 0;}

/* bd2.c -- COMPILES */
#include <sys/cdefs.h>
#include <sys/xattr.h>
int main(void){return 0;}

Both emit warning: #include <sys/xattr.h> resolved from the host system (/usr/include), not pxx's own headers. That warning is the instrument working, and it named the cause before the error did.

The mechanism, in full

/usr/include/x86_64-linux-gnu/sys/xattr.h opens with __BEGIN_DECLS on line 25 and does not include <sys/cdefs.h> itself. Under glibc that is safe: something upstream in a normal include chain has already pulled <features.h>, which pulls <sys/cdefs.h>. In a pxx TU nothing does, so the macro is undefined and its token reaches the top level, where it is correctly reported as a stray token. lib/crtl/include/sys/cdefs.h:5 defines __BEGIN_DECLS — it is just never included on this path.

So there are two candidate fixes and they are not equivalent:

  1. Provide the three headers in crtl (sys/inotify.h, sys/random.h, sys/xattr.h). Removes the host fallback for these TUs entirely. This is the real fix; the host header is an ABI hazard beyond this macro, which is what the warning is about.
  2. Make the host-fallback path establish glibc's preamble first. Cheaper, wider blast radius, and it keeps compiling against host headers — it converts a loud failure into a silent ABI dependency, which is the wrong direction.

Prefer 1. 2 is only interesting if the same leak turns up in host headers we have no intention of shadowing.

Why it took 394 applets to see

No smaller applet set pulled these three sources in. The refusal is not new; the coverage is.

THE i386 MEASUREMENT, which reframes this ticket

Measured 2026-09-04, binary sha256 1968c7a7da57, commit 5f598d4a7, at the 394-applet scope (tools/busybox-applets-394.txt). Reproduced in two lines, independent of busybox:

$ pascal26              bd1.c     # #include <sys/xattr.h>
pascal26:1: warning: ... resolved from the host system (/usr/include), not
                     pxx's own headers -- ABI/macro mismatches may silently misbehave
pascal26:25: error: stray token at top level: '__BEGIN_DECLS'

$ pascal26 --target=i386 bd1.c
pascal26:1: error: C include file not found: "sys/xattr.h"
            (searched: .../lib/crtl/include/, ...)        <- no /usr/include

The fallback is native-only, and that is correct -- host headers are the host's ABI. The consequence is not: every crtl header gap is INVISIBLE on x86-64 for as long as glibc has a header of that name, and visible on every cross target. x86-64 was not passing those TUs; it was compiling them against somebody else's headers.

The 15 headers, and the 16 TUs that want them (i386)

header busybox TU
glob.h shell/hush.c
resolv.h networking/nslookup.c
sys/inotify.h miscutils/inotifyd.c
sys/vt.h loginutils/vlock.c
sys/xattr.h miscutils/setfattr.c
linux/fb.h miscutils/fbsplash.c
linux/if.h networking/ether-wake.c, ifenslave.c, ifplugd.c
linux/if_arp.h networking/libiproute/ll_types.c
linux/if_vlan.h networking/libiproute/iplink.c
linux/jffs2.h miscutils/flash_eraseall.c
linux/major.h miscutils/raidautorun.c
linux/random.h miscutils/seedrng.c
linux/rfkill.h miscutils/rfkill.c
mtd/ubi-user.h miscutils/ubi_tools.c

linux/random.h is wanted by seedrng.c as well as sys/random.h; the x86-64 run reported the latter via __BEGIN_DECLS and the i386 run stops at the former, so both are missing and neither run alone says so.

Why __BEGIN_DECLS was a bad name for this

The macro leak happens only for host headers that spell __BEGIN_DECLS without pulling <sys/cdefs.h> themselves. That is 3 of the 15. The symptom undercounted its own cause by five to one, and a fix aimed at the symptom -- pre-including crtl's sys/cdefs.h on the fallback path -- would have closed this ticket green with twelve headers still missing and i386 still refusing all sixteen TUs. The slug is kept so existing citations resolve; the summary is the part that had to change.

Consequence for anyone measuring on x86-64 only

bug-c-ir-unsupported-ast-node-kind-1-in-flash-eraseall was root-caused on x86-64 as a missing loff_t (correct, and fixed). On i386 the same TU never reaches that line -- it stops at linux/jffs2.h. A TU can have two independent blockers with only the native one visible, so "fixed on x86-64" is not "fixed", and a per-target row is what closes an item here.


RESOLVED 2026-09-04 — all nine named TUs compile at i386, in four batches

The headers were provided, not the fallback fixed, which is what the summary above asked for and it was the right call: making the native fallback supply glibc's preamble would have made x86-64 green while i386 still could not build those TUs at all, and the divergence would then have read as an i386 defect.

batch headers TUs unblocked
1 linux/major.h, linux/raid/md_u.h, linux/random.h, linux/rfkill.h, sys/random.h (+src/sys/random.c) raidautorun, rfkill, seedrng
2 sys/xattr.h, sys/inotify.h, 62 missing errno.h names (+ two sources) inotifyd, setfattr
3 linux/fb.h, mtd/ubi-user.h fbsplash, ubi_tools
4 glob.h (+src/glob.c), resolv.h, arpa/nameser.h, arpa/nameser_compat.h (+ two sources), and AF_INET6 in inet_ntop/inet_pton hush, nslookup

networking/interface.c needed nothing new — it cleared once a header another TU wanted was in place, which is a second instance of the lesson below.

THE TICKET'S LIST WAS A LOWER BOUND AND IT PROVED IT TWICE

It named one header per TU because it was built from FIRST refusals. raidautorun.c wanted <linux/major.h> and then <linux/raid/md_u.h>, the second invisible until the first existed. Five of the fifteen headers it named already existed by the time work started. Re-measure a list of this shape before working from it — and the closing measurement is a full run over all 400 translation units, not the nine that were named.

Two implementations, not shadow headers

glob.h and resolv.h are the first entries here that needed real code rather than a transcription: 291 lines of glob() over fnmatch + dirent, and ~1000 lines of DNS across nameser.c and resolv.c. Both are diffed against glibc on five targets, and both found something the diff alone would not have:

Two chosen divergences, recorded

known-incompat/incompat-b-crtls-dns-parser-refuses-two-malformed-packets-glibc-accepts — crtl refuses a forward DNS compression pointer and a reply with QR clear; glibc accepts both. Both crtl answers are the RFC-conforming ones and no conforming server emits either shape.

What this ticket did NOT fix

Log