lib/encoding/utf8: decoder offs i32->size, closing prev/next OOB (#70)

prev()'s walk-back decremented offs (i32) past 0 to -1 and returned
`more`; a subsequent next() then passed the signed `-1 < len` guard and
read d.src[-1] — a silent OOB decode of a garbage rune (no runtime
bounds net). Hare's decoder.offs is `size`: the underflow wraps to
SIZE_MAX so every `offs < len` guard exits safely (next returns more,
not a rune). Change offs to size and spell prev's loop as the Hare-form
`offs < len` guard; index sites take an i32 temp (ww's slice index is
i32 and `[...]` reads ':' as the slice separator).

No-runtime-net residual: remaining() would silently build a ptr-1/len+1
OOB view when called in the post-`more` state; guard it with a loud
abort (caller contract: don't call after `more`). The offs type ripples
into strings.ww's iterator<->decoder bridge (move/slice) — cast at the
four sites, safe on the rune-return path where offs is in range.

utf8/strings embed into all five selfhost combined.ww snapshots plus the
smoke.combined.ww test amalgamation; all regen'd. utf8test gains
prev_more_then_next_no_oob pinning the closed OOB.
This commit is contained in:
2026-06-13 10:45:06 +09:00
parent 427b67f656
commit a9dcea70ed
9 changed files with 348 additions and 154 deletions

View File

@@ -437,6 +437,32 @@ fn streq(a: str, b: str) bool = {
};
};
// #70: after prev() returns `more` it leaves offs out of range (SIZE_MAX,
// via the unsigned underflow). A subsequent next() must see offs >= len
// and return `more` — NOT read one byte before the buffer and decode a
// garbage rune. The byte before the slice (buf[2] = 0x41 = 'A') is the
// planted sentinel the old i32 offs=-1 path returned as rune 65.
@test fn prev_more_then_next_no_oob() void = {
let buf: [4]u8;
buf[0] = 0x58u8; buf[1] = 0x59u8; buf[2] = 0x41u8; // 'A' sentinel
buf[3] = 0xA0u8; // lone continuation
let d: utf8.decoder = utf8.decode(buf[3:4]);
match (utf8.next(&d)) {
case let e: utf8.invalid => void;
case => { abort(); };
};
match (utf8.prev(&d)) {
case utf8.more => void;
case => { abort(); };
};
match (utf8.next(&d)) {
case utf8.more => void; // safe: not src[-1]'s 'A'
case let r: rune => { abort(); };
case utf8.done => { abort(); };
case let e: utf8.invalid => { abort(); };
};
};
@test fn prev_incomplete_invalid() void = {
let src: [2]u8;
src[0] = 0xE3u8; src[1] = 0x81u8;