hget-readline-overflow: fix input-buffer overflow in hget readline()
A memory-safety bug in 9legacy's sys/src/cmd/hget.c: readline()
never resets its rp/wp pointers when the 4096-byte input buffer
drains, so the write pointer walks past the end of buf[] and the
next read() deposits up to 2048 bytes of response data into
whatever static storage the linker placed after it.
The twin of this fault in sys/src/cmd/webfs/buf.c ships as
contrib/webfs-readline-overflow. The two functions are copies of
one another and differ only in field order, which decides how the
damage presents. webfs puts rp and wp after buf, so the overrun
corrupts its own pointers and the next read faults inside
readline, where a reader will find it. hget puts rp and wp
before buf, so the overrun leaves the struct entirely and lands
in unrelated static data. Nothing faults at the time and the
program continues on corrupted state, which is why this copy
outlived the one next door.
Independent of TLS. It needs only a response whose headers and
body cycle the buffer past 4096 bytes, which any large page does.
CHANGES
v1 -- 2026-08-12 (initial)
hget-readline-overflow readline() resets rp/wp to
b.buf on drain.
Scope
One-line change to sys/src/cmd/hget.c. No kernel, no
library, no ABI change. No reboot.
Files
hget-readline-overflow.diff the fix
Apply
cd /
ape/patch -p0 < /path/to/hget-readline-overflow/hget-readline-overflow.diff
Apply this before contrib/tls-1.2 if you are installing both.
tls-1.2 adds thirteen lines to hget.c above readline, so applied
afterwards this one lands thirteen lines lower than its header
says: patch finds it by context, reports the offset and leaves a
.orig behind. The resulting source is identical either way; the
order only decides whether you get the message and the backup
file.
Rebuild
cd /sys/src/cmd && mk hget.install
Prerequisites
A 9legacy tree with sys/src/cmd/hget.c present. No other
patches required; this fix stands alone.
Sanity-check that the drain path is present before patching:
grep -n 'b.rp >= b.wp' /sys/src/cmd/hget.c
grep cannot tell a patched tree from an unpatched one --
initibuf() has contained the same rp/wp assignment since 4th
edition, so searching for it matches either way. A reverse
dry-run is definitive:
ape/patch -R --dry-run -p0 < hget-readline-overflow.diff
Verification
The symptom depends on what the linker placed after the
buffer, so it varies between trees and is not a dependable
signal on its own. 8l orders bss by symbol hash, not by
source order, so adding or renaming any static in hget moves
the landing site.
A direct check does not depend on the layout. Fetch a page
whose headers alone run to several hundred bytes:
hget https://en.wikipedia.org/ >/dev/null
then read the buffer's neighbours out of the running or
broken process with acid(1). On an unpatched tree they hold
response text. This is how the fault was found: on one tree
the overrun reached libc's allocator bookkeeping and the next
malloc aborted in plock, several call frames away from
anything to do with hget's input.
After the fix the same fetch leaves the neighbours untouched.
Rollback
cd /
ape/patch -R -p0 < /path/to/hget-readline-overflow/hget-readline-overflow.diff
Then rebuild as above.
Known gap, deliberately not fixed here
unreadline() in the same file copies a pushed-back line into
the buffer with memmove and tests no bound. With this fix
applied it cannot overflow: the header buffer bounds the line
at 2047 bytes, one refill bounds the remainder at 2048, and
the write ends at index 4095 of 4096. Without this fix the
remainder is unbounded and that memmove overflows too, so the
two are one fault with two symptoms. A bounds check there
would be unreachable code. It becomes worth adding if any of
the three numbers moves: the header buffer size, the refill
quantum, or the number of unreadline callers.
Plan 9 already has the shape both functions want. libbio's
Brdline compacts the remainder to the start of the buffer and
rewinds the cursor before refilling, and reads only the room
that is left; Bungetc pushes back by decrementing a count,
because the bytes never left the buffer and there is nothing
to copy. Rewriting hget's reader onto bio(2) is a larger
change than this package makes.
See also
contrib/webfs-readline-overflow -- the same fault in webfs.
Recommended alongside; neither depends on the other.
contrib/tls-1.2 -- HTTPS client stack, which drives hget hard
enough to make this fault easy to reach.
|