Skip to content

Fix the uninitialized brainfuck tape (fib.bf going haywire after 89)#1

Merged
g-w1 merged 1 commit into
mainfrom
fix-uninitialized-tape
May 10, 2026
Merged

Fix the uninitialized brainfuck tape (fib.bf going haywire after 89)#1
g-w1 merged 1 commit into
mainfrom
fix-uninitialized-tape

Conversation

@g-w1
Copy link
Copy Markdown
Owner

@g-w1 g-w1 commented May 10, 2026

What was wrong

hello.bf worked but fib.bf "slowly went haywire" after printing 89 — the blog post blamed "some weird miscompilation in the number-printing routine." It isn't a miscompilation; the emitted x86-64 is fine. The brainfuck tape was never zero-initialized — it was aliased on top of the ELF section-header table.

In main.zig:

  • bss_o = shstrtab_o + shstrtab.len comes out exactly equal to sections_off (because data is empty), so the address handed to the program as cell 0 (r10 = base_point + bss_o) pointed straight at the section-header table in the file image.
  • the single PT_LOAD had p_filesz == p_memsz, so there was no zero-fill region at all — the .bss section is SHT_NOBITS but the segment gave us nothing zeroed.

So cells 0–7 read as 0 only by luck (they land inside the all-zero NULL section header) and cell 8+ read leftover section-header bytes. hello.bf only ever touches cells 0–6, so it survived. fib.bf keeps one decimal digit per cell and walks rightward as the numbers grow — the instant 89 → 144 needs a third digit cell it reads a never-written cell, gets garbage, and the output falls apart.

(Bonus: read() was reading from fd 1 (stdout) instead of fd 0 (stdin) — fixed too.)

The fix

Put the tape past the end of the file image and bump p_memsz to cover it, so the loader zero-fills it for us. Minimal change, no modernizing — still builds with the same Zig 0.8.1 it was written against.

-    const bss_o = shstrtab_o + shstrtab.len;
     const sections_off = header_off + c.len;
     ...
     const filesize = sh_off;
+    const bss_o = filesize;                                  // past the file image
+    const memsize = filesize + @as(usize, opts.bss_len);
     ...
-        .p_memsz = cast(filesize),
+        .p_memsz = cast(memsize),

Proof, on a real x86-64 Linux box (new CI workflow)

.github/workflows/bf.yml installs Zig 0.8.1, runs zig build, compiles the sample programs to native ELF executables and checks their output:

  • hello.bfHello World!
  • fib.bf0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610past 89
  • torture test: >×3000, write 'A', walk back, all reads correct (BAB) — the pre-fix compiler couldn't keep even one page of tape, let alone a zeroed one
  • test2.bfNo

Known follow-ups (left out to keep this minimal)

  • cells are 8 bytes apart (add r10, 8) but .bss is bss_len bytes (default 30000), so only ~3750 cells are usable; either narrow the stride or scale bss_len.
  • -b is parsed into a ?u32 but never passed to genElfAndWriteToFs (ElfOpts.bss_len is also u16).

🤖 Generated with Claude Code

…ion-header table)

The compiler in the blog post worked for hello.bf but fib.bf "slowly went
haywire" after printing 89 — diagnosed there as "some weird miscompilation in
the number-printing routine." It wasn't a miscompilation; the emitted code is
fine. The data tape was never zeroed:

  - `bss_o = shstrtab_o + shstrtab.len` comes out exactly equal to
    `sections_off`, so the address handed to the program as cell 0 pointed
    straight at the ELF section-header table in the file image.
  - the single PT_LOAD had `p_filesz == p_memsz`, so there was no zero-fill
    region at all — the `.bss` *section* (SHT_NOBITS) bought us nothing.

So cells 0..7 read as 0 only by luck (they land inside the all-zero NULL
section header) and cell 8+ read leftover section-header bytes. hello.bf only
touches cells 0..6, so it survived; fib.bf keeps one decimal digit per cell and
walks rightward as the numbers grow — the instant 89 -> 144 needs a third digit
cell it reads a never-written cell, gets garbage, and the output falls apart.

Fix: put the tape past the end of the file image and set `p_memsz` accordingly,
so the loader zero-fills it. Also `read()` was reading from fd 1 (stdout)
instead of fd 0 (stdin).

Adds a GitHub Actions workflow that builds bz on x86-64 Linux, compiles the
sample programs to native ELF, runs them, and checks output — including fib.bf
getting past 89 and a torture test that touches a cell ~3000 deep into the
(now genuinely zeroed) tape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@g-w1 g-w1 merged commit 641282f into main May 10, 2026
2 checks passed
@g-w1 g-w1 deleted the fix-uninitialized-tape branch May 10, 2026 05:01
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