Refusing to encode is a claim about the buffer, and nothing was checking it

encode-rune! says nothing is written when it answers None, and every None case
in the table passed that claim without testing it: an encoder that lays the
lead byte down and only then notices the buffer is short returns None exactly
as a correct one does. So a known byte goes into scratch, a refused encoding
is asked for, and the byte is read back. Storing before the length test turns
the line from 65 -1 65 -1 65 into 65 -1 0 -1 0.

The read-only claim beside lower-ascii was reasoned from the emitted linkage
rather than observed, and observing it was worse than the guess. With
(set (at (bytes "Hi") 0) \h): at -O0 the store is emitted against the constant
and the program takes SIGSEGV; at -O2 LLVM deletes it as undefined behaviour
and the program prints "Hi" and exits 0. The same source either dies or
silently does nothing depending on a flag. The comment now says that instead
of predicting a segfault.
This commit is contained in:
Joseph Ferano 2026-09-12 04:01:25 +07:00
parent 4957497d02
commit ecf3882fa2
3 changed files with 30 additions and 5 deletions

View File

@ -637,11 +637,22 @@ let source = {flan|
;; to_upper both allocate a new string (core/strings/conversion.odin), which
;; is not available here; the obvious substitute lowering a [u8] in place
;; is a trap, and it is worth saying why rather than shipping it. A string
;; literal is emitted `private unnamed_addr constant` (emit.ml), so it lives
;; in read-only memory, and (bytes "Hello") is a [u8] pointing straight at it.
;; An in-place lower-ascii! would type check against that slice and segfault
;; on the store. Given a byte function, a caller that really does own its
;; buffer writes the two-line loop itself and can see what it is writing to.
;; literal is emitted `private unnamed_addr constant` (emit.ml), so (bytes
;; "Hello") is a [u8] pointing straight into read-only memory. An in-place
;; lower-ascii! type checks against that slice, and what happens next depends
;; on the optimiser which is the worst of the available answers. Measured,
;; with (set (at (bytes "Hi") 0) \h):
;;
;; -O0 the store is emitted against the constant and the program takes
;; SIGSEGV.
;; -O2 LLVM deletes the store as undefined behaviour and the program
;; carries on and prints "Hi".
;;
;; So the same source either dies or silently does nothing depending on a
;; flag, and the -O2 half is the quiet-wrongness class this file keeps
;; refusing elsewhere. Given a byte function instead, a caller that really
;; does own its buffer writes the two-line loop itself over storage it can
;; see the declaration of.
;;
;; ASCII only, and only the 26 letters: case outside ASCII is not a byte
;; operation at all it is per-code-point, it is not length-preserving (ß

View File

@ -190,6 +190,19 @@
(show-opt (encode-rune! (slice scratch 0 1) 0x41)) ; 1, exactly enough
(newline)
;; "Nothing is written when it refuses" is a claim about the buffer, not
;; about the return value, and the None cases above do not test it: an
;; encoder that lays down the lead byte and only then notices the buffer is
;; short returns None exactly as this one does, and every line above still
;; passes. So put a known byte in scratch, ask for an encoding that must be
;; refused, and read the byte back.
(show-i32 (round-trip 0x41)) ; 65, scratch[0] = A
(show-opt (encode-rune! (slice scratch 0 2) 0x65e5)) ; -1, needs 3 bytes
(show-i32 (i32 (at scratch 0))) ; 65 still
(show-opt (encode-rune! (slice scratch 0 4) 0xd800)) ; -1, surrogate
(show-i32 (i32 (at scratch 0))) ; 65 still
(newline)
;; Splitting. n separators give n+1 fields, always: an interior empty field
;; survives, a leading and a trailing one do too, and an input with no
;; separator at all is one field rather than none. The empty input is the

View File

@ -913,6 +913,7 @@ ERR@7 unexpected token: not the kind the caller was reading
-1 1 1 2 2 3 3 -1 -1 3 3 4 4 -1 \n\
0 65 127 128 2047 2048 65535 65536 1114111 \n\
-1 -1 -1 -1 -1 1 \n\
65 -1 65 -1 65 \n\
[a][b][c] [a][][b] [abc] [] [][] [][a] [a][] \n\
60\n\
97 122 97 64 91 65 90 65 96 123 53 \n\