Skip to content

fix: get every CI job green (norm debt, ft_malloc link, oracle skew) - #1

Merged
LESdylan merged 7 commits into
developfrom
fix/ci-green
Jul 28, 2026
Merged

fix: get every CI job green (norm debt, ft_malloc link, oracle skew)#1
LESdylan merged 7 commits into
developfrom
fix/ci-green

Conversation

@LESdylan

Copy link
Copy Markdown
Member

Gets every CI job green. Three independent root causes, plus two latent bugs found on the way:

Why CI was red

  1. 42 Norm — norminette 3.3.59 reported 120 errors across ~60 files, accumulated invisibly because make norm no-ops on hosts without norminette while CI enforces it. Fixed by a behavior-preserving refactor sweep (function splits, file rebalancing, ternary/arg-count/local-count fixes, header cleanups). Full suite stays 3051/3051.
  2. Build · opt SAFE=0 (ft_malloc) — libft declares ft_malloc update = none, so submodules: recursive never fetches it; libft silently built its libc fallback and the link died on the force-bound malloc_live_bytes. The Makefile now materialises ft_malloc (--checkout) whenever SAFE=0 needs it. Root cause was compounded by a libft bug where every build moved .gitmodules into the build tree and fclean deleted it — fixed in libft, pointer bumped here.
  3. Tests — the suite diffed hellish against the runner's bash 5.2, while the project tracks 5.3: ~30 phantom failures from oracle skew. The Tests job now builds and caches a pinned bash 5.3.9 oracle. Two residual cases (echo $SHELL in scrubbed envs) exposed a real gap: hellish now synthesises $SHELL from passwd like bash (absent-only, un-exported).

Latent bugs fixed along the way

  • Forked exec children ran LSan on exit: after a failed execve, the child exited via exit(), letting the leak check scan half-execution state inherited at fork(). Layout-dependent, and its error status clobbered 126/127. The child now _exits.
  • libft slab_free guessed ownership by span+magic: a foreign heap pointer in a chunk gap could be silently dropped (leak) or corrupt a freelist. Now proves ownership per chunk via slab_get_block_at_ptr.
  • Bonus from the same session: the prompt's git dirty check no longer blocks on git statuscd into huge repos went from multi-second prompt stalls to instant (new pty gate: make git-prompt-test).

Verification

  • make test: 3051/3051 vs bash --posix (host, bash 5.3.9)
  • ubuntu-24.04 container with the pinned oracle: full suite green
  • verify_alloc.sh: both heaps, output parity, 0 leaks / flat live-bytes
  • norminette 3.3.59 over src incs: 0 errors
  • make and make OPT=1 both build; pty gates (git-prompt-test, hist-test, readline-test, anim-test) all pass

🤖 Generated with Claude Code

LESdylan added 7 commits July 28, 2026 12:02
The dirty star ran git status synchronously and blocked the prompt for
the whole scan: cd into a repo where the scan takes seconds froze the
first prompt (and every 3s TTL refresh froze again). The check now
waits at most 60ms, and only for a freshly entered root — normal repos
keep their exact immediate star — then finishes in the background on a
non-blocking, cloexec pipe harvested by a later render. Completion is
detected by pipe EOF, so the bg-job waitpid(-1) reaper stealing the
zombie is harmless; a root change abandons the in-flight child; a
signal-killed child voids its sample instead of caching a false clean;
slow repos stretch the TTL 3s->30s so git is not hammered.

Measured (pty, git shim sleeping 2s): cd->prompt 2.034s -> 0.077s, TTL
refresh 2.026s -> 0.008s. New pty gate: make git-prompt-test.
norminette 3.3.59 (the version CI pins) reported 120 errors that had
accumulated invisibly — make norm no-ops on hosts without norminette
while the CI gate enforces it. Behavior-preserving mechanical fixes:

- 33 over-25-line functions split into cohesive static helpers
- 18 over-5-function files rebalanced, siblings created where a family
  was full (alias_scan6, assignment_to_env2, expand_param_format7,
  parse_arena3, reparse_assign_words2, set_opts3, env_assoc3, token_old.h)
- assoc_next's five pointer out-params packed into a t_assoc_it
  iterator (env.h), fixing TOO_MANY_ARGS at every call site
- PARENA_ROUND macro (forbidden macro-func) became the shared
  parena_round() — LTO inlines it back on optimized builds
- ternaries, >5-local functions, >4-arg signatures, preproc constant
  expressions (SHOPT bits, parena sizes), header continuation indents,
  tab/space and alignment fixes

Every extraction replays the original statements in the original
order; the golden suite (3051 cases vs bash --posix) stays green.
bash fills SHELL with the current user's passwd login shell when the
variable is entirely absent from the environment (an inherited empty
SHELL= stays empty) and keeps it un-exported. hellish left it unset,
which diverges in scrubbed environments — docker containers and CI
runners — where the suite's echo $SHELL cases then fail against the
bash oracle.
The forked external-command child exited through exit() after a failed
execve, running atexit handlers — under the ASan/LSan debug build that
includes the leak check, scanning the half-execution state copied at
fork. Whether some argv string looks reachable there depends on stale
stack-frame contents, so unrelated layout changes made the check fire
randomly and its error exit clobbered the child's 127/126 with the
sanitizer status — flipping golden-suite cases. _exit skips atexit in a
process whose address space is about to die anyway; the child was
already forbidden from freeing parent state (see exit_clean rules).
libft declares its nested ft_malloc submodule update=none, so
git submodule update --init --recursive skips it: a fresh clone (and
CI's checkout) silently built the libc fallback, then died at link on
the force-bound malloc_live_bytes. The libft rule now materialises the
submodule (--checkout overrides the none policy) whenever SAFE=0 needs
it and it is absent, with a clear failure message when offline.
The Tests job diffed hellish against the runner's distro bash, and
ubuntu-24.04 ships 5.2 — bash 5.2 vs 5.3 disagree on ~30 golden cases,
which surfaced as phantom hellish failures. Build the 5.3.9 oracle once
from GNU sources (cached by runner+arch) and put it first in PATH.
Also upload suite.log as an artifact on failure so the diverging cases
are visible without re-running.
Picks up two libft fixes: slab_free now proves chunk ownership exactly
instead of guessing via span+magic (a foreign heap pointer landing in a
span gap could be silently dropped or corrupt a freelist), and the
build's dotfile sweep no longer moves .gitmodules into the build tree
where fclean deleted it — which is what kept un-declaring the
ft_malloc submodule on every deep rebuild.
@LESdylan
LESdylan merged commit c3ee54d into develop Jul 28, 2026
6 checks 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.

1 participant