Skip to content

StreamHeader misparses three legal slot shapes: no-byte-count slots, byte-counted references, negative version words #106

Description

@nsmith-

🤖 AI generated content

StreamHeader.read (src/rootfilespec/bootstrap/streamedobject.py:53-61) recognises only "byte count + version", "byte count + class record" and "bare word = object reference". Three shapes the spec describes fall outside that:

  1. Object slots without a byte count (:54-58). Buffer framing §6 lists five slot forms, two with no byte count: a bare kNewClassTag and a bare kClassMask | p, each followed by the object. A word like 0x80000055 has kByteCountMask clear, so it is taken for an object reference and the object body is read as the next member — a silent desync. A bare 0xFFFFFFFF is worse: read as ">i" it is -1, the mask test is truthy, and the "byte count" becomes -1073741825. Not seen in the corpora (ROOT does not write these today); derived from the code, not reproduced on a file.
  2. A reference carrying its own byte count. §6.1 "Object references": ROOT never writes one but its reader accepts it, and "a reader that requires §6's three shapes rejects a file ROOT reads without complaint". uproot-issue413.root's fLeaves entries are exactly this (40 00 00 04 | 00 00 0a 7f, confirmed in the bytes for TLeafI not declared in StreamerInfo, e.g. uproot-issue413.root #18). The code takes 40 00 00 04 as a byte count and then reads the tag's high half as a version word. Latent today only because that file fails earlier (TLeafI not declared in StreamerInfo, e.g. uproot-issue413.root #18).
  3. Negative version words (:27-28, :61). kNotAVersion = 0x8000 is tested on the first u16 after the byte count, but §4 says "The trigger is version <= 0, so a negative version word takes the same path." A version word such as 0xFFFF would be routed into the class-record branch. Hardening; no known file.

Suggested fix: make the slot parser enumerate §6's forms explicitly (including "byte count then a tag that is an object reference"), read the first word unsigned, and fail loudly on anything unrecognised rather than returning a Ref.

Severity / size: medium (1–2 are silent desyncs; 2 blocks #18's file) / S–M.

Related: #18, #105, #104, #74 (a slot with no byte count cannot be skipped and must still fail loudly).

Assisted-by: claude-code:claude-fable-5-1

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions