Codex, working the defensive half without having written the parser, probed M1
with adversarial input and found defects my own tests missed. Verified
independently before fixing, and two more were found while confirming:
negative_array_count ACCEPTED as an empty array
negative_hash_count ACCEPTED as an empty hash
negative_bignum_words NoMethodError leaked outside StreamError
negative_ivar_count ACCEPTED
negative_string_len cursor moved BACKWARD, wrong error raised
The last is the worst. take(-5) does not trip the count > remaining guard, so
byteslice returns nil and @position decreases. A parser whose cursor can rewind
on attacker input is a loop primitive, not merely a wrong error.
The M1 gate claimed bignum length confusion was covered. It was not. Oversized
widths were tested and negative counts never were, because the same author
chose both the implementation and the cases it would face. That is the
negative-control failure one level up, and it is exactly what an author cannot
catch alone.
Fixes: every count and length now flows through read_count with a role label
and a nonnegative check, take rejects negative byte counts outright, and
MalformedCountError joins the StreamError hierarchy so nothing leaks a raw
NoMethodError. Negative link indices were already guarded; regression tests now
pin that.
Also closes a detection blind spot Codex identified. read_ivar and read_object
discarded the parsed name nodes after taking their values, so a sink tag placed
in an instance-variable-name position vanished from Result#sinks. Node now
carries an auxiliary collection that Node#each traverses, and name and class
nodes are retained. Proven: a stream with a userdef tag in the name position
now reports Evil#_load where it previously reported nothing.
46 parser tests, 80 assertions. All controls pass, exploit gate still passes.