Re: [PATCH v6] hfsplus: validate B-tree record offset table
Jiaming Zhang <[email protected]> Sat, 8 Aug 2026 22:50:10 +0800
| Newsgroups | gmane.linux.file-systems,gmane.linux.kernel |
|---|---|
| Message-ID | <CANypQFZ-kejru9P3D7rvCzDp5hP-xqM9okq7LpiHEobbU=oQpw@mail.gmail.com> |
Viacheslav Dubeyko <[email protected]> 于2026年8月8日周六 08:49写道: > > On Thu, 2026-08-06 at 15:33 +0800, Jiaming Zhang wrote: > > A crafted HFS+ image can contain a corrupted B-tree node. The node > > descriptor may contain a record count that does not fit in the node, > > and > > record offsets may be unordered, unaligned, outside the node, or > > point into > > the offset table itself. > > > > Several B-tree helpers consume these on-disk fields before validating > > them: > > hfs_bnode_dump() can walk past the offset table when num_recs is > > corrupted, > > hfs_brec_lenoff() can produce an underflowed length or a record range > > that > > overlaps the offset table. This can make the unlink/writeback path > > repeatedly call hfs_bnode_read_u16() with invalid offsets while > > holding the > > HFS+ B-tree lock, producing a flood of "requested invalid offset" > > messages. > > Other writeback workers then block on tree->tree_lock and the system > > reports tasks hung in hfsplus_write_inode(). > > > > Validate num_recs against the node size before walking the record > > offset > > table. Reject record ranges that are unordered, unaligned, outside > > the > > node, or overlapping the offset table. Reject invalid record indexes > > before > > reading their offset entries, and avoid decrementing an already-zero > > leaf_count. > > > > Closes: > > https://lore.kernel.org/lkml/CANypQFb_2TqKGrztAXj5m0_v+QChxXDnQVeifzV8J25Vuju10Q@mail.gmail.com/ > > Assisted-by: Codex:gpt-5.5-xhigh > > Signed-off-by: Jiaming Zhang <[email protected]> > > --- > > Changes in v6: > > - hfs_bmap_free(): Drop offset checks and print error message when B- > > tree > > map record length is invalid. > > > > Changes in v5: > > - Switch helpers to invalid checker and invert callers. > > - hfs_brec_offsets_invalid(): Drop the redundant offset-table > > argument > > since it is already covered by the offset-table overlap check and > > take > > just the two neighboring record offsets (off and next_off). > > - hfs_brec_len_invalid(): reject a length against node_size. > > - Use hfs_brec_len_invalid() for both length checks in > > hfs_bmap_get_map_page(). > > - Validate the record offset in hfs_bmap_free(). > > - Check the __hfs_brec_find() return code in [2] and [3]. [1] and [4] > > call > > it to find the insertion slot for a new index key after a split, - > > ENOENT is > > one of expected, even -EINVAL the following hfs_brec_insert() just > > inserts > > at slot 0 of a node already validated by hfs_bnode_find(), so it > > stays > > in-bounds and cannot trigger the invalid-offset flood. Hence no > > check was > > added. > > > > [1] > > https://elixir.bootlin.com/linux/v7.2-rc3/source/fs/hfsplus/brec.c#L160 > > [2] > > https://elixir.bootlin.com/linux/v7.2-rc3/source/fs/hfsplus/brec.c#L208 > > [3] > > https://elixir.bootlin.com/linux/v7.2-rc3/source/fs/hfsplus/brec.c#L382 > > [4] > > https://elixir.bootlin.com/linux/v7.2-rc3/source/fs/hfsplus/brec.c#L449 > > > > Changes in v4: > > - Rename hfs_find_reset() to hfs_find_result_init(). > > - Reset find result fields in __hfs_brec_find(). > > - Move num_recs validation next to descriptor field initialization. > > - Rename hfs_brec_range_valid() to hfs_brec_offpair_valid(). > > - Use U16_MAX for invalid offset/len/keylen sentinels and update > > callers. > > - Add hfs_brec_len_valid() to check validity of len/keylen. > > - Handle invalid B-tree map record lengths in hfs_bmap_get_map_page() > > and hfs_bmap_free(). > > - Return -EINVAL instead of -EIO for invalid remove cursor/leaf_count > > state. > > > > Changes in v3: > > - Drop the keylen == len check. > > - Drop the explicit zero-record check in __hfs_brec_find(). > > - Move find cursor reset into hfs_find_reset() and call it from > > hfs_find_init() and hfs_brec_find(). > > - Rename helper-local variables as suggested. > > > > fs/hfsplus/bfind.c | 23 ++++++------ > > fs/hfsplus/bnode.c | 16 ++++++--- > > fs/hfsplus/brec.c | 53 ++++++++++++++++++--------- > > fs/hfsplus/btree.c | 16 +++++++-- > > fs/hfsplus/hfsplus_fs.h | 79 > > +++++++++++++++++++++++++++++++++++++++++ > > 5 files changed, 155 insertions(+), 32 deletions(-) > > > > diff --git a/fs/hfsplus/bfind.c b/fs/hfsplus/bfind.c > > index 9a55fa6d5294..ca9813f58a6d 100644 > > --- a/fs/hfsplus/bfind.c > > +++ b/fs/hfsplus/bfind.c > > @@ -18,6 +18,7 @@ int hfs_find_init(struct hfs_btree *tree, struct > > hfs_find_data *fd) > > > > fd->tree = tree; > > fd->bnode = NULL; > > + hfs_find_result_init(fd); > > ptr = kzalloc(tree->max_key_len * 2 + 4, GFP_KERNEL); > > if (!ptr) > > return -ENOMEM; > > @@ -106,17 +107,21 @@ int __hfs_brec_find(struct hfs_bnode *bnode, > > struct hfs_find_data *fd, > > u16 off, len, keylen; > > int rec; > > int b, e; > > - int res; > > + int res = -ENOENT; > > > > BUG_ON(!rec_found); > > + hfs_find_result_init(fd); > > + if (hfs_bnode_num_recs_invalid(bnode)) > > + goto fail; > > + > > b = 0; > > e = bnode->num_recs - 1; > > - res = -ENOENT; > > do { > > rec = (e + b) / 2; > > len = hfs_brec_lenoff(bnode, rec, &off); > > keylen = hfs_brec_keylen(bnode, rec); > > - if (keylen == 0) { > > + if (hfs_brec_len_invalid(bnode, len) || > > + hfs_brec_len_invalid(bnode, keylen)) { > > res = -EINVAL; > > goto fail; > > } > > > I've realized that we have additional issue in HFS+ logic. Potentially, > keylen and len could be not invalid but we could have negative value > later in the logic: > > done: > fd->record = e; > fd->keyoffset = off; > fd->keylength = keylen; > fd->entryoffset = off + keylen; > fd->entrylength = len - keylen; <-- negative value here. > > The negative value of fd->entrylength is checked in multiple places. > However, there is extents tree logic that has no such check [1]: > > static int __hfsplus_ext_write_extent(struct inode *inode, > struct hfs_find_data *fd) > { > <skipped> > > } else { > if (res) > return res; > hfs_bnode_write(fd->bnode, hip->cached_extents, > fd->entryoffset, fd->entrylength); > hip->extent_state &= ~HFSPLUS_EXT_DIRTY; > } > > <skipped> > } > > Could you please double check my conclusion? We can fix the issue in > another patch. Could you please fix the issue? > I agree with your conclusion. A crafted image can keep both len and keylen valid but make fd->entrylength negative (i.e. keylen > len), which may lead to out-of-bound read and kernel memory leaking. To fix this issue, we can check validity of fd->entrylength in __hfsplus_ext_write_extent() like the check in __hfsplus_ext_read_extent(). For example: diff --git a/fs/hfsplus/extents.c b/fs/hfsplus/extents.c index 813e68b8ecd6..eb7c11524d18 100644 --- a/fs/hfsplus/extents.c +++ b/fs/hfsplus/extents.c @@ -110,6 +110,8 @@ static int __hfsplus_ext_write_extent(struct inode *inode, } else { if (res) return res; + if (fd->entrylength != sizeof(hfsplus_extent_rec)) + return -EIO; hfs_bnode_write(fd->bnode, hip->cached_extents, fd->entryoffset, fd->entrylength); hip->extent_state &= ~HFSPLUS_EXT_DIRTY; Do you think this fix is acceptable? If so, I'm happy to send another patch. Best Regards, Jiaming Zhang