You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
TDirectory.get_KeyList: fSeekKey is 0 0 (bad key but KeyList is valid, e.g. uproot-issue261.root) #17
Findings below come from the root-io-spec reference now vendored in this repo at reference/root-io-spec (pinned at 9c3f776). Short version: the key on a key-list record is decorative — ROOT never validates it — and both files are third-party output with that key filled in wrongly. Dropping our check makes both files read completely; I tested it.
What is actually in the bytes
uproot-issue261.root: directory has fSeekKeys = 10048, fNbytesKeys = 106. The key at 10048 is
Three fields are wrong: fNbytes is 58 (= fKeylen + 4) where it should be 106, fSeekKey should be 10048, fSeekPdir should be 100. The payload after it is fine — count 1, then a valid TTree/events entry with fSeekKey 10176, fSeekPdir 100. uproot-issue412.root is byte-for-byte the same pattern (key at 10042, fNbytes 55 vs fNbytesKeys 101, both seeks 0, one valid TTree/test entry).
What the spec says
Directories §6.2: "The only supported way to find it is fSeekKeys from the directory record." §4.1: fNbytesKeys is used as a raw read length. And ROOT's own TDirectoryFile::ReadKeys builds the header key, reads it, and deletes it without checking anything (root/io/io/src/TDirectoryFile.cxx:1444-1468); the >= 64 && <= fsize sanity check is applied only to the entries. TKey::Streamer itself rejects only negative fKeylen/fObjlen/fNbytes (root/io/io/src/TKey.cxx:1423-1438), so zeros pass.
Records §3.5 does say a reader SHOULD check fSeekKey — but that is for records reached by walking the chain, which is where the redundancy is a useful corruption check. This record is reached through fSeekKeys.
The file, and the writer
The spec's foreign-corpus notes (gen/foreign/IGNORE.toml) diagnose uproot-issue261.root for a different defect: "The record chain has a 70-byte hole: at 10427, where a record header should be, the first word is 0." I confirmed that too. It does not affect us — we never walk the chain — and the spec concludes ROOT "opens it and reads its TTree without a word" (Records §1). Also: both files set the >2 GB header flag (fVersion 1061800) on files of 10 kB and 2.3 MB, which per Directory.md §3 should be set only when fEND > 2000000000. Writer attribution is inference, but this is clearly not ROOT.
Code
src/rootfilespec/bootstrap/TDirectory.py:152-161 (KeyListLocator.read_from) and :113-124 (get_KeyList). Patching KeyListLocator.read_from to skip the fSeekKey/fSeekPdir checks makes both files pass the read test in full (uproot-issue412.root walks /test/b1, /test/b2). get_KeyList additionally raises on fNbytes != fNbytesKeys, which this file also violates; trust fNbytesKeys per §4.1. The in-code comment at TDirectory.py:154 ("this is when there is no streamerinfo for the file") is not right — both files have a StreamerInfo record (at 228 and 222 respectively).
Verdict: file at fault, handle leniently. Size: S — relax two checks (optionally warn), delete the NotImplementedError. No dependencies. Closes 2 of the 22 current xfails.
Activity
nsmith- commentedon Sep 18, 2026
Findings below come from the root-io-spec reference now vendored in this repo at
reference/root-io-spec(pinned at9c3f776). Short version: the key on a key-list record is decorative — ROOT never validates it — and both files are third-party output with that key filled in wrongly. Dropping our check makes both files read completely; I tested it.What is actually in the bytes
uproot-issue261.root: directory hasfSeekKeys = 10048,fNbytesKeys = 106. The key at 10048 isThree fields are wrong:
fNbytesis 58 (=fKeylen+ 4) where it should be 106,fSeekKeyshould be 10048,fSeekPdirshould be 100. The payload after it is fine — count 1, then a validTTree/eventsentry withfSeekKey10176,fSeekPdir100.uproot-issue412.rootis byte-for-byte the same pattern (key at 10042,fNbytes55 vsfNbytesKeys101, both seeks 0, one validTTree/testentry).What the spec says
Directories §6.2: "The only supported way to find it is
fSeekKeysfrom the directory record." §4.1:fNbytesKeysis used as a raw read length. And ROOT's ownTDirectoryFile::ReadKeysbuilds the header key, reads it, anddeletes it without checking anything (root/io/io/src/TDirectoryFile.cxx:1444-1468); the>= 64 && <= fsizesanity check is applied only to the entries.TKey::Streameritself rejects only negativefKeylen/fObjlen/fNbytes(root/io/io/src/TKey.cxx:1423-1438), so zeros pass.Records §3.5 does say a reader SHOULD check
fSeekKey— but that is for records reached by walking the chain, which is where the redundancy is a useful corruption check. This record is reached throughfSeekKeys.The file, and the writer
The spec's foreign-corpus notes (
gen/foreign/IGNORE.toml) diagnoseuproot-issue261.rootfor a different defect: "The record chain has a 70-byte hole: at 10427, where a record header should be, the first word is 0." I confirmed that too. It does not affect us — we never walk the chain — and the spec concludes ROOT "opens it and reads itsTTreewithout a word" (Records §1). Also: both files set the >2 GB header flag (fVersion1061800) on files of 10 kB and 2.3 MB, which per Directory.md §3 should be set only whenfEND > 2000000000. Writer attribution is inference, but this is clearly not ROOT.Code
src/rootfilespec/bootstrap/TDirectory.py:152-161(KeyListLocator.read_from) and:113-124(get_KeyList). PatchingKeyListLocator.read_fromto skip thefSeekKey/fSeekPdirchecks makes both files pass the read test in full (uproot-issue412.rootwalks/test/b1,/test/b2).get_KeyListadditionally raises onfNbytes != fNbytesKeys, which this file also violates; trustfNbytesKeysper §4.1. The in-code comment atTDirectory.py:154("this is when there is no streamerinfo for the file") is not right — both files have aStreamerInforecord (at 228 and 222 respectively).Verdict: file at fault, handle leniently. Size: S — relax two checks (optionally warn), delete the
NotImplementedError. No dependencies. Closes 2 of the 22 current xfails.