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

@@ -586,17 +586,20 @@ export fn riter(src: str) iterator = {
fn move(forward: bool, it: *iterator) (rune | utf8.done) = {
let d: utf8.decoder;
d.src = it.src;
d.offs = it.offs;
// utf8.decoder.offs is `size` (#70); the iterator carries i32. The
// rune-return path keeps offs in [0, len), so the narrowing cast back
// is safe.
d.offs = it.offs: size;
if (forward) {
match (utf8.next(&d)) {
case let r: rune => { it.offs = d.offs; return r; };
case let r: rune => { it.offs = d.offs: i32; return r; };
case let dn: utf8.done => return dn;
case let m: utf8.more => abort("strings.move: invalid UTF-8");
case let e: utf8.invalid => abort("strings.move: invalid UTF-8");
};
} else {
match (utf8.prev(&d)) {
case let r: rune => { it.offs = d.offs; return r; };
case let r: rune => { it.offs = d.offs: i32; return r; };
case let dn: utf8.done => return dn;
case let m: utf8.more => abort("strings.move: invalid UTF-8");
case let e: utf8.invalid => abort("strings.move: invalid UTF-8");
@@ -639,10 +642,10 @@ export fn iterstr(it: *iterator) str = {
export fn slice(begin: *iterator, end: *iterator) str = {
let b: utf8.decoder;
b.src = begin.src;
b.offs = begin.offs;
b.offs = begin.offs: size; // decoder.offs is size (#70)
let e: utf8.decoder;
e.src = end.src;
e.offs = end.offs;
e.offs = end.offs: size;
return frombytes(utf8.slice(&b, &e));
};