Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

hfsplus: fix generic/642 failure

The xfstests' test-case generic/642 finishes with
corrupted HFS+ volume:

sudo ./check generic/642
[sudo] password for slavad:
FSTYP -- hfsplus
PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.0.0-rc1+ #26 SMP PREEMPT_DYNAMIC Mon Mar 23 17:24:32 PDT 2026
MKFS_OPTIONS -- /dev/loop51
MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch

generic/642 6s ... _check_generic_filesystem: filesystem on /dev/loop51 is inconsistent
(see xfstests-dev/results//generic/642.full for details)

Ran: generic/642
Failures: generic/642
Failed 1 of 1 tests

sudo fsck.hfs -d /dev/loop51
** /dev/loop51
Using cacheBlockSize=32K cacheTotalBlock=1024 cacheSize=32768K.
Executing fsck_hfs (version 540.1-Linux).
** Checking non-journaled HFS Plus Volume.
The volume name is untitled
** Checking extents overflow file.
** Checking catalog file.
** Checking multi-linked files.
** Checking catalog hierarchy.
** Checking extended attributes file.
invalid free nodes - calculated 1637 header 1260
Invalid B-tree header
Invalid map node
(8, 0)
** Checking volume bitmap.
** Checking volume information.
Verify Status: VIStat = 0x0000, ABTStat = 0xc000 EBTStat = 0x0000
CBTStat = 0x0000 CatStat = 0x00000000
** Repairing volume.
** Rechecking volume.
** Checking non-journaled HFS Plus Volume.
The volume name is untitled
** Checking extents overflow file.
** Checking catalog file.
** Checking multi-linked files.
** Checking catalog hierarchy.
** Checking extended attributes file.
** Checking volume bitmap.
** Checking volume information.
** The volume untitled was repaired successfully.

The fsck tool detected that Extended Attributes b-tree is corrupted.
Namely, the free nodes number is incorrect and map node
bitmap has inconsistent state. Analysis has shown that during
b-tree closing there are still some lost b-tree's nodes in
the hash out of b-tree structure. But this orphaned b-tree nodes
are still accounted as used in map node bitmap:

tree_cnid 8, nidx 0, node_count 1408, free_nodes 1403
tree_cnid 8, nidx 1, node_count 1408, free_nodes 1403
tree_cnid 8, nidx 3, node_count 1408, free_nodes 1403
tree_cnid 8, nidx 54, node_count 1408, free_nodes 1403
tree_cnid 8, nidx 67, node_count 1408, free_nodes 1403
tree_cnid 8, nidx 0, prev 0, next 0, parent 0, num_recs 3, type 0x1, height 0
tree_cnid 8, nidx 1, prev 0, next 0, parent 3, num_recs 1, type 0xff, height 1
tree_cnid 8, nidx 3, prev 0, next 0, parent 0, num_recs 1, type 0x0, height 2
tree_cnid 8, nidx 54, prev 29, next 46, parent 3, num_recs 0, type 0xff, height 1
tree_cnid 8, nidx 67, prev 8, next 14, parent 3, num_recs 0, type 0xff, height 1

This issue happens in hfs_bnode_split() logic during detection
the possibility of moving half ot the records out of the node.
The hfs_bnode_split() contains a loop that implements
a roughly 50/50 split of the B-tree node's records by scanning
the offset table to find where the data crosses the node's midpoint.
If this logic detects the incapability of spliting the node, then
it simply calls hfs_bnode_put() for newly created node. However,
node is not set as HFS_BNODE_DELETED and real deletion of node
doesn't happen. As a result, the empty node becomes orphaned but
it is still accounted as used. Finally, fsck tool detects this
inconsistency of HFS+ volume.

This patch adds call of hfs_bnode_unlink() before hfs_bnode_put()
for the case if new node cannot be used for spliting the existing
node.

sudo ./check generic/642
FSTYP -- hfsplus
PLATFORM -- Linux/x86_64 hfsplus-testing-0001 7.0.0-rc1+ #26 SMP PREEMPT_DYNAMIC Fri Apr 3 12:39:13 PDT 2026
MKFS_OPTIONS -- /dev/loop51
MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch

generic/642 40s ... 39s
Ran: generic/642
Passed all 1 tests

Closes: https://github.com/hfs-linux-kernel/hfs-linux-kernel/issues/242
cc: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de>
cc: Yangtao Li <frank.li@vivo.com>
cc: linux-fsdevel@vger.kernel.org
Signed-off-by: Viacheslav Dubeyko <slava@dubeyko.com>
Link: https://lore.kernel.org/r/20260403230556.614171-6-slava@dubeyko.com
Signed-off-by: Viacheslav Dubeyko <slava@dubeyko.com>

+20 -12
+20 -12
fs/hfsplus/brec.c
··· 239 239 struct hfs_bnode_desc node_desc; 240 240 int num_recs, new_rec_off, new_off, old_rec_off; 241 241 int data_start, data_end, size; 242 + size_t rec_off_tbl_size; 243 + size_t node_desc_size = sizeof(struct hfs_bnode_desc); 244 + size_t rec_size = sizeof(__be16); 242 245 243 246 tree = fd->tree; 244 247 node = fd->bnode; ··· 268 265 return next_node; 269 266 } 270 267 271 - size = tree->node_size / 2 - node->num_recs * 2 - 14; 272 - old_rec_off = tree->node_size - 4; 268 + rec_off_tbl_size = node->num_recs * rec_size; 269 + size = tree->node_size / 2; 270 + size -= node_desc_size; 271 + size -= rec_off_tbl_size; 272 + old_rec_off = tree->node_size - (2 * rec_size); 273 + 273 274 num_recs = 1; 274 275 for (;;) { 275 276 data_start = hfs_bnode_read_u16(node, old_rec_off); 276 277 if (data_start > size) 277 278 break; 278 - old_rec_off -= 2; 279 + old_rec_off -= rec_size; 279 280 if (++num_recs < node->num_recs) 280 281 continue; 281 - /* panic? */ 282 282 hfs_bnode_put(node); 283 + hfs_bnode_unlink(new_node); 283 284 hfs_bnode_put(new_node); 284 285 if (next_node) 285 286 hfs_bnode_put(next_node); ··· 294 287 /* new record is in the lower half, 295 288 * so leave some more space there 296 289 */ 297 - old_rec_off += 2; 290 + old_rec_off += rec_size; 298 291 num_recs--; 299 292 data_start = hfs_bnode_read_u16(node, old_rec_off); 300 293 } else { ··· 302 295 hfs_bnode_get(new_node); 303 296 fd->bnode = new_node; 304 297 fd->record -= num_recs; 305 - fd->keyoffset -= data_start - 14; 306 - fd->entryoffset -= data_start - 14; 298 + fd->keyoffset -= data_start - node_desc_size; 299 + fd->entryoffset -= data_start - node_desc_size; 307 300 } 308 301 new_node->num_recs = node->num_recs - num_recs; 309 302 node->num_recs = num_recs; 310 303 311 - new_rec_off = tree->node_size - 2; 312 - new_off = 14; 304 + new_rec_off = tree->node_size - rec_size; 305 + new_off = node_desc_size; 313 306 size = data_start - new_off; 314 307 num_recs = new_node->num_recs; 315 308 data_end = data_start; 316 309 while (num_recs) { 317 310 hfs_bnode_write_u16(new_node, new_rec_off, new_off); 318 - old_rec_off -= 2; 319 - new_rec_off -= 2; 311 + old_rec_off -= rec_size; 312 + new_rec_off -= rec_size; 320 313 data_end = hfs_bnode_read_u16(node, old_rec_off); 321 314 new_off = data_end - size; 322 315 num_recs--; 323 316 } 324 317 hfs_bnode_write_u16(new_node, new_rec_off, new_off); 325 - hfs_bnode_copy(new_node, 14, node, data_start, data_end - data_start); 318 + hfs_bnode_copy(new_node, node_desc_size, 319 + node, data_start, data_end - data_start); 326 320 327 321 /* update new bnode header */ 328 322 node_desc.next = cpu_to_be32(new_node->next);