diff --git a/third_party/libdmg-hfsplus/src/hfs/btree.c b/third_party/libdmg-hfsplus/src/hfs/btree.c index d31ff6d2bc5c9..a54a5b058c2e2 100644 --- a/third_party/libdmg-hfsplus/src/hfs/btree.c +++ b/third_party/libdmg-hfsplus/src/hfs/btree.c @@ -913,6 +913,12 @@ static uint32_t removeNode(BTree* tree, uint32_t node) { return TRUE; } +/** + * Splits a node in half by descriptor count. The caller is responsible for + * updating the parent node. + * + * Returns the index of the new node. + */ static uint32_t splitNode(uint32_t node, BTNodeDescriptor* descriptor, BTree* tree) { int nodesToMove; @@ -1040,9 +1046,22 @@ static int moveRecordsDown(BTree* tree, BTNodeDescriptor* descriptor, int record return TRUE; } +/* + * doAddRecord inserts a B-tree record into a specific node. This is a low-level + * operation and callers are responsible for determining that: + * - this is the correct node for this key + * - the key is not already in the node + * - the node has enough space for the new record and key + * + * If any of these constraints are violated, doAddRecord may silently corrupt + * `tree`. + * + * If `doAddRecord` detects a problem, it prints a terse diagnostic to stderr + * and returns FALSE. If it inserts the record, it returns TRUE. If it + * encounters an I/O error, it halts the program (with an assertion failure). + */ static int doAddRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t length, unsigned char* content) { BTNodeDescriptor* descriptor; - BTKey* key; off_t recordOffset; off_t recordDataOffset; off_t lastRecordDataOffset; @@ -1054,25 +1073,45 @@ static int doAddRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t leng descriptor = readBTNodeDescriptor(root, tree); - if(descriptor == NULL) + if(descriptor == NULL) { + fprintf(stderr, "cannot read node descriptor for node %u\n", root); return FALSE; + } + + // HFS+ B-tree records consist of: + // 1. A 2-byte key length field. + // 2. The key data itself (searchKey->keyLength bytes). + // 3. The record data (length bytes). + // 4. A 2-byte entry in the node's offset table. + // + // If the node doesn't have enough space for the new record, fail. + // + // If the B-tree has already been corrupted, available space may be negative, + // so this calculation must use signed arithmetic. + intmax_t requiredSpace = sizeof(searchKey->keyLength) + searchKey->keyLength + length + sizeof(uint16_t); + ASSERT(requiredSpace > 0, "record size calculation overflow in doAddRecord"); + intmax_t availableSpace = (intmax_t)getFreeSpace(root, descriptor, tree); + if (availableSpace < requiredSpace) { + fprintf(stderr, "cannot fit %jd-byte complete record into into B-tree node %u with %jd free bytes\n", requiredSpace, root, availableSpace); + free(descriptor); + return FALSE; + } lastRecordDataOffset = 0; for(i = 0; i < descriptor->numRecords; i++) { recordOffset = getRecordOffset(i, root, tree); - key = READ_KEY(tree, recordOffset, tree->io); + BTKey* key = READ_KEY(tree, recordOffset, tree->io); recordDataOffset = recordOffset + key->keyLength + sizeof(key->keyLength); res = COMPARE(tree, key, searchKey); if(res == 0) { + fprintf(stderr, "doAddRecord: key already exists in node %u", root); free(key); free(descriptor); - return FALSE; } else if(res > 0) { free(key); - break; } @@ -1092,8 +1131,7 @@ static int doAddRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t leng offset = recordOffset - (root * tree->headerRec->nodeSize); FLIPENDIAN(offset); - ASSERT(WRITE(tree->io, (root * tree->headerRec->nodeSize) + tree->headerRec->nodeSize - (sizeof(uint16_t) * (i + 1)), - sizeof(uint16_t), &offset), "WRITE"); + ASSERT(WRITE(tree->io, (root * tree->headerRec->nodeSize) + tree->headerRec->nodeSize - (sizeof(uint16_t) * (i + 1)), sizeof(uint16_t), &offset), "WRITE"); } else { // just insert ourself at the end recordOffset = getRecordOffset(i, root, tree); @@ -1120,7 +1158,19 @@ static int doAddRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t leng return TRUE; } -static int addRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t length, unsigned char* content, int* callAgain) { +/** + * addRecord recursively traverses the B-tree to insert a new record. + * + * If the insertion causes a node split, this function returns the node number + * of the new (right-hand) sibling node. The caller (the parent node's recursive + * call) is then responsible for inserting a key and pointer to this new node + * into its own records. If no split occurred, it returns 0. + * + * If the operation cannot be completed in a single pass, callAgain is set to + * TRUE and the node is split. The top-level caller (addToBTree) must then + * restart the entire operation after the index is updated for new node splits. + */ +static uint32_t addRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t length, unsigned char* content, int* callAgain) { BTNodeDescriptor* descriptor; BTKey* key; off_t recordOffset; @@ -1169,24 +1219,31 @@ static int addRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t length } if(descriptor->kind == kBTLeafNode) { + // Leaf Node insertion: + // If there's enough space, just add the record. + // Otherwise, split the node and add the record to the correct sibling. if(freeSpace < (sizeof(searchKey->keyLength) + searchKey->keyLength + length + sizeof(uint16_t))) { newNode = splitNode(root, descriptor, tree); if(i < descriptor->numRecords) { - doAddRecord(tree, root, searchKey, length, content); + ASSERT(doAddRecord(tree, root, searchKey, length, content), "addRecord: doAddRecord failed in leaf node after split"); } else { - doAddRecord(tree, newNode, searchKey, length, content); + ASSERT(doAddRecord(tree, newNode, searchKey, length, content), "addRecord: doAddRecord failed in new leaf node after split"); } free(descriptor); return newNode; } else { - doAddRecord(tree, root, searchKey, length, content); + ASSERT(doAddRecord(tree, root, searchKey, length, content), "addRecord: doAddRecord failed in leaf node"); free(descriptor); return 0; } } else { + // Index Node traversal: + // Find the child node that should contain the key. if(lastRecordDataOffset == 0) { + // The new record needs to become the new first record of a subtree we are + // traversing. Update this node's record to report its new lower bound. if(descriptor->numRecords == 0) { hfs_panic("empty index node in btree"); return 0; @@ -1199,18 +1256,20 @@ static int addRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t length FLIPENDIAN(nodeBigEndian); - free(key); + free(key); key = READ_KEY(tree, (root * tree->headerRec->nodeSize) + 14, tree->io); recordDataOffset = recordOffset + key->keyLength + sizeof(key->keyLength); if(searchKey->keyLength != key->keyLength) { + // Move everything else to fit the new record size, since the key length + // is changing. if(searchKey->keyLength > key->keyLength && freeSpace < (searchKey->keyLength - key->keyLength)) { // very unlikely. We need to split this node before we can resize the key of this index. Do that first, and tell them to call again. *callAgain = TRUE; return splitNode(root, descriptor, tree); } - moveRecordsDown(tree, descriptor, 1, root, searchKey->keyLength - key->keyLength, 0); + moveRecordsDown(tree, descriptor, 1, root, (int)searchKey->keyLength - (int)key->keyLength, 0); } free(key); @@ -1237,16 +1296,16 @@ static int addRecord(BTree* tree, uint32_t root, BTKey* searchKey, size_t length newNode = splitNode(root, descriptor, tree); if(i < descriptor->numRecords) { - doAddRecord(tree, root, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)); + ASSERT(doAddRecord(tree, root, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)), "addRecord: doAddRecord failed in index node after split"); } else { - doAddRecord(tree, newNode, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)); + ASSERT(doAddRecord(tree, newNode, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)), "addRecord: doAddRecord failed in new index node after split"); } free(key); free(descriptor); return newNode; } else { - doAddRecord(tree, root, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)); + ASSERT(doAddRecord(tree, root, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)), "addRecord: doAddRecord failed in index node"); free(key); free(descriptor); @@ -1349,8 +1408,17 @@ int addToBTree(BTree* tree, BTKey* searchKey, size_t length, unsigned char* cont newDescriptor.numRecords = 1; newDescriptor.reserved = 0; - offset = 14; - freeOffset = offset + sizeof(searchKey->keyLength) + searchKey->keyLength + length; + // initial offset: skip the 14-byte BTNodeDescriptor + uint16_t offset = 14; + + // Make sure data won't run off the end of the node; leave room for stored offsets + size_t bigFreeOffset = offset + sizeof(searchKey->keyLength) + searchKey->keyLength + length; + size_t maxOffset = (size_t)(tree->headerRec->nodeSize) - (sizeof(uint16_t)*2); + if (bigFreeOffset > maxOffset) { + fprintf(stderr, "addToBTree: cannot create first leaf with full record size %zu -- cap is %zu\n", bigFreeOffset - offset, maxOffset - offset); + return FALSE; + } + freeOffset = (uint16_t)bigFreeOffset; ASSERT(WRITE_KEY(tree, tree->headerRec->rootNode * tree->headerRec->nodeSize + offset, searchKey, tree->io), "WRITE_KEY"); ASSERT(WRITE(tree->io, tree->headerRec->rootNode * tree->headerRec->nodeSize + offset + sizeof(searchKey->keyLength) + searchKey->keyLength, @@ -1371,6 +1439,21 @@ int addToBTree(BTree* tree, BTKey* searchKey, size_t length, unsigned char* cont return TRUE; } +/** + * removeRecord recursively traverses the B-tree to find and remove a record. + * + * If this node was split as a result of the operation, this returns the index + * number of the new sibling node, which the caller must integrate into the + * parent index. + * + * If a node becomes empty after removal, gone is set to TRUE and the node + * itself has been removed; the caller must update the parent index accordingly. + * + * If the operation could not be completed in a single pass, callAgain is set + * to TRUE and the caller must retry the deletion after adjusting the index + * for the split node. The tree is in an inconsistent state until this additonal + * pass is performed. + */ static uint32_t removeRecord(BTree* tree, uint32_t root, BTKey* searchKey, int* callAgain, int* gone) { BTNodeDescriptor* descriptor; int length; @@ -1412,6 +1495,8 @@ static uint32_t removeRecord(BTree* tree, uint32_t root, BTKey* searchKey, int* free(key); if(descriptor->kind == kBTLeafNode) { + // Record found in a leaf node. Remove it and potentially remove the + // entire node if it becomes empty. if(i != (descriptor->numRecords - 1)) { length = getRecordOffset(i + 1, root, tree) - recordOffset; moveRecordsDown(tree, descriptor, i + 1, root, -length, -1); @@ -1432,11 +1517,14 @@ static uint32_t removeRecord(BTree* tree, uint32_t root, BTKey* searchKey, int* return 0; } } else { + // This is not a leaf node, but we are about to remove this node's + // zeroth child, so plan to update this node's key on the way back up. nodeToTraverse = getNodeNumberFromPointerRecord(recordDataOffset, tree->io); checkForChangedKey = TRUE; break; } } else if(res > 0) { + // The target is somewhere before the record we are inspecting. free(key); if(lastRecordDataOffset == 0 || descriptor->kind == kBTLeafNode) { @@ -1455,6 +1543,11 @@ static uint32_t removeRecord(BTree* tree, uint32_t root, BTKey* searchKey, int* } if(nodeToTraverse == 0) { + if(lastRecordDataOffset == 0 || descriptor->kind == kBTLeafNode) { + // not found; + free(descriptor); + return 0; + } nodeToTraverse = getNodeNumberFromPointerRecord(lastRecordDataOffset, tree->io); } @@ -1465,6 +1558,8 @@ static uint32_t removeRecord(BTree* tree, uint32_t root, BTKey* searchKey, int* newNode = removeRecord(tree, nodeToTraverse, searchKey, callAgain, &childGone); if(childGone) { + // A child node was removed because it became empty. We must now + // remove the pointer to that child from this (the parent) node. if(i != (descriptor->numRecords - 1)) { length = getRecordOffset(i + 1, root, tree) - recordOffset; moveRecordsDown(tree, descriptor, i + 1, root, -length, -1); @@ -1472,31 +1567,31 @@ static uint32_t removeRecord(BTree* tree, uint32_t root, BTKey* searchKey, int* descriptor->numRecords--; ASSERT(writeBTNodeDescriptor(descriptor, root, tree), "writeBTNodeDescriptor"); - } else { - if(checkForChangedKey) { - // we will remove the first item in the child node, so our index has to change + } else if(checkForChangedKey) { + // we will remove the first item in the child node, so our index has to change - key = READ_KEY(tree, getRecordOffset(0, nodeToTraverse, tree), tree->io); + key = READ_KEY(tree, getRecordOffset(0, nodeToTraverse, tree), tree->io); - if(searchKey->keyLength != key->keyLength) { - if(key->keyLength > searchKey->keyLength && freeSpace < (key->keyLength - searchKey->keyLength)) { - // very unlikely. We need to split this node before we can resize the key of this index. Do that first, and tell them to call again. - *callAgain = TRUE; - return splitNode(root, descriptor, tree); - } - - moveRecordsDown(tree, descriptor, i + 1, root, key->keyLength - searchKey->keyLength, 0); + if(searchKey->keyLength != key->keyLength) { + if(key->keyLength > searchKey->keyLength && freeSpace < (key->keyLength - searchKey->keyLength)) { + // very unlikely. We need to split this node before we can resize the key of this index. Do that first, and tell them to call again. + *callAgain = TRUE; + return splitNode(root, descriptor, tree); } - ASSERT(WRITE_KEY(tree, recordOffset, key, tree->io), "WRITE_KEY"); - FLIPENDIAN(nodeToTraverse); - ASSERT(WRITE(tree->io, recordOffset + sizeof(uint16_t) + key->keyLength, sizeof(uint32_t), &nodeToTraverse), "WRITE"); - FLIPENDIAN(nodeToTraverse); - - free(key); + moveRecordsDown(tree, descriptor, i + 1, root, key->keyLength - searchKey->keyLength, 0); } + + ASSERT(WRITE_KEY(tree, recordOffset, key, tree->io), "WRITE_KEY"); + FLIPENDIAN(nodeToTraverse); + ASSERT(WRITE(tree->io, recordOffset + sizeof(uint16_t) + key->keyLength, sizeof(uint32_t), &nodeToTraverse), "WRITE"); + FLIPENDIAN(nodeToTraverse); + + free(key); } + // If the child call to removeRecord (or our own internal record insertion) + // resulted in a split or a node deletion, handle the consequences here. if(newNode == 0) { if(descriptor->numRecords == 0) { removeNode(tree, root); @@ -1514,9 +1609,11 @@ static uint32_t removeRecord(BTree* tree, uint32_t root, BTKey* searchKey, int* newNode = splitNode(root, descriptor, tree); if(i < descriptor->numRecords) { - doAddRecord(tree, root, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)); + ASSERT(doAddRecord(tree, root, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)), + "removeRecord: doAddRecord failed in index node after split"); } else { - doAddRecord(tree, newNode, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)); + ASSERT(doAddRecord(tree, newNode, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)), + "removeRecord: doAddRecord failed in new index node after split"); } if(descriptor->numRecords == 0) { @@ -1528,15 +1625,13 @@ static uint32_t removeRecord(BTree* tree, uint32_t root, BTKey* searchKey, int* free(descriptor); return newNode; } else { - doAddRecord(tree, root, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)); - + ASSERT(doAddRecord(tree, root, key, sizeof(newNodeBigEndian), (unsigned char*)(&newNodeBigEndian)), + "removeRecord: doAddRecord failed in index node"); free(key); free(descriptor); return 0; } } - - return FALSE; } int removeFromBTree(BTree* tree, BTKey* searchKey) {