{"record":{"id":"f31839fa17f26882","repo":"NationalSecurityAgency/ghidra","slug":"bad-nodesize","errorCode":null,"errorMessage":"Bad nodeSize: {}","messagePattern":"Bad nodeSize: (.+?)","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"GPL/DMG/src/dmg/java/mobiledevices/dmg/btree/BTreeRootNodeDescriptor.java","lineNumber":30,"sourceCode":"public class BTreeRootNodeDescriptor extends BTreeNodeDescriptor {\n\n\tprivate BTreeHeaderRecord headerRecord;\n\tprivate BTreeUserDataRecord userDataRecord;\n\tprivate BTreeMapRecord mapRecord;\n\tprivate List<BTreeNodeDescriptor> nodes = new ArrayList<BTreeNodeDescriptor>();\n\n\tpublic BTreeRootNodeDescriptor( GBinaryReader reader ) throws IOException {\n\t\tsuper( reader );\n\n\t\theaderRecord   = new BTreeHeaderRecord( reader );\n\t\tuserDataRecord = new BTreeUserDataRecord( reader );\n\t\tmapRecord      = new BTreeMapRecord( reader, headerRecord );\n\n\t\tnodes.add( this );\n\n\t\tint nodeSize = Short.toUnsignedInt(headerRecord.getNodeSize());\n\t\tif (nodeSize == 0) {\n\t\t\tthrow new IOException(\"Bad nodeSize: \" + nodeSize);\n\t\t}\n\n\t\tfor ( int i = nodeSize ; i < reader.length() ; i += nodeSize ) {\n\t\t\treader.setPointerIndex( i );\n\t\t\tBTreeNodeDescriptor node = new BTreeNodeDescriptor( reader );\n\t\t\tnodes.add( node );\n\t\t\tnode.readRecordOffsets( reader, i, headerRecord );\n\t\t\tnode.readRecords( reader, i );\n\t\t}\n\n\t\tthis.readRecordOffsets( reader, 0, headerRecord );\n\t}\n\n\tpublic BTreeHeaderRecord getHeaderRecord() {\n\t\treturn headerRecord;\n\t}\n\n\tpublic BTreeUserDataRecord getUserDataRecord() {","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/NationalSecurityAgency/ghidra/blob/d5f144c24d6bc53c9cbf4448c6d11143e7696206/GPL/DMG/src/dmg/java/mobiledevices/dmg/btree/BTreeRootNodeDescriptor.java#L12-L48","documentation":"Thrown while parsing the root node of an HFS+/HFSX B-tree embedded inside a DMG. After reading the BTreeHeaderRecord the code extracts nodeSize via Short.toUnsignedInt and guards zero: a zero nodeSize would make the subsequent node-walk loop (for i = nodeSize; ...; i += nodeSize) never advance, so it is an explicit corruption guard. The value comes straight from on-disk bytes, so it indicates the header bytes are wrong rather than a misuse of the API.","triggerScenarios":"Constructing new BTreeRootNodeDescriptor(reader) on a reader positioned over a B-tree whose header record's nodeSize field is 0. This occurs when the DMG's filesystem structures are misaligned or truncated, e.g. after failed AES decryption of an encrypted DMG yields bytes that get reinterpreted as a header.","commonSituations":"Wrong decryption key for an encrypted DMG (decryption produces plausible-looking but wrong bytes); a truncated or partially-downloaded DMG where the header region is short or zeroed; processing a non-HFS image through the HFS B-tree path. Because nodeSize is read with Short.toUnsignedInt, a byte-swap mismatch (endianness) on a big-endian on-disk field can also surface as 0.","solutions":["Verify the DMG file is complete and uncorrupted: re-download and confirm checksum/size before parsing.","For encrypted DMGs, confirm the decryption key is correct (wrong key produces garbage that parses to nodeSize 0).","Inspect the bytes of the BTreeHeaderRecord at the reader's current offset to confirm endianness and that nodeSize is a sane value (commonly 4096, 512, 1024, 2048, 8192).","If you control the input, ensure the source produces a valid HFS+/HFSX B-tree with a non-zero nodeSize in its header record."],"exampleFix":"// before\nBTreeRootNodeDescriptor root = new BTreeRootNodeDescriptor(reader);\n\n// after - peek nodeSize before committing to the full parse\nreader.mark();\nBTreeHeaderRecord hdr = new BTreeHeaderRecord(reader);\nreader.reset();\nint nodeSize = Short.toUnsignedInt(hdr.getNodeSize());\nif (nodeSize <= 0) {\n    throw new IOException(\"Refusing to parse: B-tree nodeSize is \" + nodeSize + \" (corrupt or wrongly decrypted image)\");\n}\nBTreeRootNodeDescriptor root = new BTreeRootNodeDescriptor(reader);","handlingStrategy":"validation","validationCode":"// Read the header record independently first to pre-validate nodeSize\nBTreeHeaderRecord probe = new BTreeHeaderRecord(reader duplicating/seeked to the header offset);\nint nodeSize = Short.toUnsignedInt(probe.getNodeSize());\nif (nodeSize <= 0) {\n    throw new IOException(\"Cannot construct BTreeRootNodeDescriptor: nodeSize=\" + nodeSize + \" indicates corrupt/undecrypted image\");\n}","typeGuard":"private static boolean isValidNodeSize(BTreeHeaderRecord hdr) {\n    int n = Short.toUnsignedInt(hdr.getNodeSize());\n    return n == 512 || n == 1024 || n == 2048 || n == 4096 || n == 8192 || n == 16384 || n == 32768;\n}","tryCatchPattern":"try {\n    BTreeRootNodeDescriptor root = new BTreeRootNodeDescriptor(reader);\n} catch (IOException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"Bad nodeSize\")) {\n        // Corrupt or wrongly-decrypted B-tree header; do not retry the same bytes\n        throw new IOException(\"B-tree parse failed (nodeSize guard): likely corrupt or undecrypted DMG\", e);\n    }\n    throw e;\n}","preventionTips":["Validate the DMG checksum and completeness before parsing B-tree structures.","Confirm encrypted-DMG decryption succeeded (key correct) before entering the B-tree reader.","Pre-read and sanity-check the header record's nodeSize against known HFS+ node sizes before constructing the root descriptor."],"tags":["dmg","hfs","btree","parsing","corrupt-file","decryption"],"backgroundTag":null,"analyzedSha":"d5f144c24d6bc53c9cbf4448c6d11143e7696206","analyzedAt":"2026-08-14T01:00:57.564Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}