From 0cac22a5ef06fb09c5886a954e954d6aaa1667b0 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 04:04:28 +0700 Subject: [PATCH] Record that a write through a string literal fails two ways At -O0 it stores into read-only memory and takes SIGSEGV; at -O2 LLVM deletes it as undefined and the program prints the unmodified string and exits 0. Same source, and which way it fails depends on a flag, which is worse than either outcome on its own. Nothing refuses it and nothing cheaply can: bytes turns a string into a [u8], the language lets you write through a slice, and by then nothing records where the bytes came from. That is provenance, which plan.org defers as open decision #3. Emitting literals as mutable globals is not a fix - it moves which flag misbehaves and costs their read-only placement. Written down rather than half-fixed, with the rule the string lane already follows: a function over a string must not write through it. --- NEXT.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/NEXT.md b/NEXT.md index c22d022..dc81c86 100644 --- a/NEXT.md +++ b/NEXT.md @@ -446,6 +446,27 @@ so wasm32 needs no exception proposal. ## Sharp edges +- **Writing through a string literal is undefined, and the two build modes + disagree about how.** `(let [s (bytes "Hi")] (set (at s 0) \h))` stores into + a `private unnamed_addr constant`. At `-O0` that is a store to read-only + memory and the program takes SIGSEGV; at `-O2` LLVM deletes it as undefined + and the program prints `Hi` and exits 0. Same source, and which way it fails + depends on a flag — the worst shape available, and worse than either outcome + alone. + + Nothing refuses it. `bytes` turns a `string` into a `[u8]`, the language lets + you write through a slice, and by then nothing records that the bytes came + from a constant. The honest fix is provenance — knowing a slice's origin — + which is plan.org open decision #3 and deliberately deferred. A cheaper one + that is *not* a fix: emitting literals as mutable globals only moves which + flag misbehaves, and costs their read-only placement. + + Found by the string lane while deciding whether `lower-ascii` should mutate + in place. It ships the copying version for exactly this reason, and that is + the rule to follow until provenance exists: **a function over a `string` must + not write through it.** + + Most of these are edges the language keeps and you should know about. Two — the top-level namespace and the shift count, both found by review after milestone 4 — were bugs that reached LLVM or ran wrong, and are **fixed**; each says so. They stay written down because each one is now a rule the checker enforces, and a later change could quietly drop it.