Verbose is a small experimental language I build — its compiler proves properties about your code and emits tiny x86-64 machine code: no runtime, no GC, no libc. This post stands on its own.
You run a program. It should tell you which tier a customer belongs to. It prints:
2864743186439
Exit code: 0. No error, no warning, no crash. The program is pleased with itself.
The right answer was premium.
And the unsettling part is that this number isn't noise. It isn't corrupted memory, it isn't a random value. It is the right answer — just read the wrong way. Two lines to prove it.
Decode the number
In hexadecimal:
2864743186439 (decimal)
= 0x29B00000007 (hex)
Now cut it in half:
0x29B 00000007
└─┬─┘ └───┬──┘
│ │
0x29B = 7
= 667 length
"the text starts at byte 667, and is 7 bytes long"
Read the source file at byte 667, for 7 bytes, and you get exactly:
premium
The answer was right there. Complete, correct, at the correct address. The program was holding it — and printed it as an integer.
Two numbers that mean "text"
We met this format last week, looking at the HTTP server emitted in 943 bytes. Its parser pulled the method and path out of the request, and I stressed one point: nothing is copied. You only note where it starts and how long it is.
That's a packed span: two numbers stored inside a single integer.
One 64-bit integer, cut in half:
┌────────────────────────┬────────────────────────┐
│ start (the address) │ length (the bytes) │
└────────────────────────┴────────────────────────┘
667 7
To the machine: the number 2864743186439.
To the language: the text "premium".
It's extremely cheap. No copy, no allocation, one register carries a string of any size. That's precisely why last week's server fits in 943 bytes.
But there's a catch, and it's the subject of this whole post: nothing in the number itself says it is text. The fact that these 64 bits are a span rather than a quantity exists only in the compiler's head. The day it forgets, there's no crash — just a number printed where a word belonged.
An analogy: it's a street address. "12 Lilac Road" designates a house. Forget that it's an address and you can still compute 12 + 4 = 16. The arithmetic is valid. It just doesn't mean anything anymore.
Four ways to forget, in one week
Nine defects were closed on Verbose between August 3rd and 8th. Four of them are the same mistake in four different places — and all four produce a wrong answer with exit code 0.
What you ask for What you got What was right
─────────────────────────────────────────────────────────────────────
the customer's tier 2864743186439 premium
a customer record 1 {"name":"Ada",
"bonus":5000}
a composed sentence Hello 2287825359413968899, Hello Ada,
age 36 age 36
"does this text == ab?" (compares addresses) (compares bytes)
The first three are forgetting at print time: the compiler could produce the value but not render it as text. The second is especially telling — it wasn't even printing a span, but the internal index of the memory slot where the record was stored. A 1. Neatly filed, completely mute.
The fourth is a different animal, and the most interesting. It concerns comparison. The code generated for == was:
pop rcx ; the second span
pop rax ; the first span
cmp rax, rcx ; compare... the two numbers
sete al
That compares two addresses, not two strings. Two identical texts stored at two different places in memory are therefore declared different. And two different texts that happened to start at the same place would be declared identical.
The disassembly shows it unambiguously — the "ab" constant is a literal span compiled in:
movabs rax, 0x13700000002 ; "ab": start 0x137, length 2
This one deserves a special mention, because it arrived after the family had been declared closed. Three silent wrong answers fixed, category declared empty… and the fourth mechanism showed up the same day. Hence the first lesson pinned in the repo:
Prefer naming the mechanism you closed over declaring a category empty.
That's a rule about honesty, not modesty. "I fixed Result printing" is checkable. "There are no more silent wrong answers" is not — and turned out false within hours.
The same forgetting, in its dangerous form
A fifth defect comes from the same root, but it doesn't merely answer wrong.
In Verbose you can declare a bound on a text field: name : text [..N] — at most N bytes. The native emitter exploits that declaration to size the buffer at compile time. Reasonable: you promised at most N, it reserves N.
Except the fill pass wrote the actual bytes, measured at runtime. Nothing enforced the promise.
The buffer is SIZED from the DECLARATION (at most N)
The buffer is FILLED from REALITY (whatever arrives)
────────────────
and nobody compares the two
An input-controlled stack buffer overflow. And — here's the sting — remotely reachable, through the method and path fields of an HTTP request. On echo_path.verbose, a 3001-byte path wrote roughly 2740 attacker-chosen bytes past the buffer.
echo_path.verbose is precisely the example I showed last week to illustrate routing. So let me say it plainly: the little server I presented to you as a success had, at that moment, a remotely exploitable overflow. It's fixed now (bounds enforced at runtime, natively and then self-hosted) — but that's the order things happened in, and hiding it would teach nobody anything.
Why these are the worst bugs
A crash is good news. It's loud, dated, reproducible; it takes you straight to the offending line. Nobody builds on a crash.
The silent wrong answer gets adopted. It exits 0, it has the right shape, it passes the tests that check "it didn't crash". It lands in a file, in an invoice, in a decision. And when it's finally spotted, you have to unwind everything built on top of it.
It's also why you don't find them by reading code: there's nothing to see. The code compiles, runs, exits 0. You need a mechanism that compares two executions — the reference implementation and the self-hosted one, on the same program, demanding identical output bytes. That's the third pillar of self-hosting quality, after byte-identical self-reproduction (the fixed point) and verification at emission time.
And that harness learned something too this week, pinned as the second lesson:
A differential sweep with placeholder inputs can miss a defect that is correct on half its input space.
The text-equality bug slipped through exactly that way: with dummy inputs, both branches aren't exercised. Comparing addresses sometimes gives the right answer — often enough to look alive. The sweep now materialises real fixtures and exercises both arms.
What to take away
a text = two numbers (start, length) in a single integer
│
├── forgotten when printing → a number instead of a word (3 defects)
├── forgotten when comparing → addresses compared (1 defect)
└── forgotten when sizing → buffer overflow (1 vuln)
One representation, five ways to forget what it means. That's the price of the economy: copying nothing is fast, but it moves the responsibility from the type to the compiler — and a compiler that forgets doesn't warn you.
Epilogue: and what about the measuring instrument?
This post was written when the following week supplied its own punchline.
Everything above rests on one number: "the self-hosted compiler accepts 100 corpus files." That's the project metric, the one that says where things stand.
It was wrong.
The emitted binary started at rule #0 — not because anything declared it, but because the emitter lays procedures out in source order and points the entry at the first one. In other words: the program that ran was whichever one had been written first. Across the 100 files, in 37 cases, that first rule was a helper the real one calls.
One command demonstrates it:
$ gen0 0 < examples/aes_gcm.verbose > aes_gcm && ./aes_gcm 0 1 16 255
99 124 202 22 # the FIPS-197 S-box, byte-exact.
# Not a trace of GCM anywhere in it.
The file is called aes_gcm. We thought we were measuring AES-GCM. We were measuring its substitution table. Eight sha256_* files actually entered at rotr32; six aes_* files at aes_sbox; and the language's own parser entered at skip_spaces.
Nothing crashed. The compiler accepted the file, produced a binary, and that binary ran and exited 0 with perfectly correct bytes — belonging to a different program. This is exactly the silent wrong answer described above, moved up one floor: no longer in the product, but in the apparatus used to judge the product.
And it wasn't alone. The same week, we found the compiler hadn't been reproducible for weeks: a hash-set traversal was leaking each process's random ordering into the emitted byte layout. Same source, same size, different bytes. Again: no error, no symptom.
There's no reassuring moral here, and that's the point. A measuring instrument doesn't verify itself; you have to decide to go and look at it, knowing you'll probably find yourself less far along than advertised. The number was corrected, not touched up.
And the limit, named as in previous posts: v0.10.0 still isn't tagged, and main is now 88 commits ahead of v0.9.0. Sixth week the case has strengthened. It isn't code that's missing.
Next time your program prints a large, strange number, try it: convert to hex, cut it in half. There's a non-zero chance it's telling you exactly where your answer lives.
French original on arcker.org.
Top comments (0)