]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
hfsplus: validate B-tree record offset table
authorJiaming Zhang <r772577952@gmail.com>
Thu, 6 Aug 2026 07:33:58 +0000 (15:33 +0800)
committerViacheslav Dubeyko <slava@dubeyko.com>
Tue, 11 Aug 2026 23:22:26 +0000 (16:22 -0700)
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 <r772577952@gmail.com>
Reviewed-by: Viacheslav Dubeyko <slava@dubeyko.com>
Tested-by: Viacheslav Dubeyko <slava@dubeyko.com>
Signed-off-by: Viacheslav Dubeyko <slava@dubeyko.com>
Link: https://lore.kernel.org/r/20260806073358.1184938-1-r772577952@gmail.com
Signed-off-by: Viacheslav Dubeyko <slava@dubeyko.com>
fs/hfsplus/bfind.c
fs/hfsplus/bnode.c
fs/hfsplus/brec.c
fs/hfsplus/btree.c
fs/hfsplus/hfsplus_fs.h

index 9a55fa6d529429b806e5947a4b751486ce0678e1..ca9813f58a6dd158a02724138471b646bc0d0a8a 100644 (file)
@@ -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;
                }
@@ -130,7 +135,8 @@ int __hfs_brec_find(struct hfs_bnode *bnode, struct hfs_find_data *fd,
        if (rec != e && e >= 0) {
                len = hfs_brec_lenoff(bnode, e, &off);
                keylen = hfs_brec_keylen(bnode, e);
-               if (keylen == 0) {
+               if (hfs_brec_len_invalid(bnode, keylen) ||
+                   hfs_brec_len_invalid(bnode, len)) {
                        res = -EINVAL;
                        goto fail;
                }
@@ -158,11 +164,7 @@ int hfs_brec_find(struct hfs_find_data *fd, search_strategy_t do_key_compare)
        __be32 data;
        int height, res;
 
-       fd->record = -1;
-       fd->keyoffset = -1;
-       fd->keylength = -1;
-       fd->entryoffset = -1;
-       fd->entrylength = -1;
+       hfs_find_result_init(fd);
 
        tree = fd->tree;
        if (fd->bnode)
@@ -274,7 +276,8 @@ int hfs_brec_goto(struct hfs_find_data *fd, int cnt)
 
        len = hfs_brec_lenoff(bnode, fd->record, &off);
        keylen = hfs_brec_keylen(bnode, fd->record);
-       if (keylen == 0) {
+       if (hfs_brec_len_invalid(bnode, len) ||
+           hfs_brec_len_invalid(bnode, keylen)) {
                res = -EINVAL;
                goto out;
        }
index d088fb7eb0dfa3e77a3bcaee3914bd3523aef3b5..3a1a1fa6f2e257510afae29514e0534b9a8ca5f5 100644 (file)
@@ -352,15 +352,22 @@ void hfs_bnode_dump(struct hfs_bnode *node)
        struct hfs_bnode_desc desc;
        __be32 cnid;
        int i, off, key_off;
+       u16 num_recs;
 
        hfs_dbg("node %d\n", node->this);
        hfs_bnode_read(node, &desc, 0, sizeof(desc));
+       num_recs = node->num_recs;
        hfs_dbg("next %d, prev %d, type %d, height %d, num_recs %d\n",
                be32_to_cpu(desc.next), be32_to_cpu(desc.prev),
                desc.type, desc.height, be16_to_cpu(desc.num_recs));
 
+       if (hfs_bnode_num_recs_invalid(node)) {
+               hfs_dbg("invalid num_recs %u\n", num_recs);
+               return;
+       }
+
        off = node->tree->node_size - 2;
-       for (i = be16_to_cpu(desc.num_recs); i >= 0; off -= 2, i--) {
+       for (i = num_recs; i >= 0; off -= 2, i--) {
                key_off = hfs_bnode_read_u16(node, off);
                hfs_dbg(" key_off %d", key_off);
                if (i && node->type == HFS_NODE_INDEX) {
@@ -561,6 +568,9 @@ struct hfs_bnode *hfs_bnode_find(struct hfs_btree *tree, u32 num)
        node->height = desc->height;
        kunmap_local(desc);
 
+       if (hfs_bnode_num_recs_invalid(node))
+               goto node_error;
+
        switch (node->type) {
        case HFS_NODE_HEADER:
        case HFS_NODE_MAP:
@@ -586,9 +596,7 @@ struct hfs_bnode *hfs_bnode_find(struct hfs_btree *tree, u32 num)
        for (i = 1; i <= node->num_recs; off = next_off, i++) {
                rec_off -= 2;
                next_off = hfs_bnode_read_u16(node, rec_off);
-               if (next_off <= off ||
-                   next_off > tree->node_size ||
-                   next_off & 1)
+               if (hfs_brec_offsets_invalid(node, off, next_off))
                        goto node_error;
                entry_size = next_off - off;
                if (node->type != HFS_NODE_INDEX &&
index e3df89284079dbf9f49608b371b5bf86c5841606..f416e562d439766960bef2cb317ace1f2c8cddd3 100644 (file)
@@ -9,6 +9,8 @@
  * Handle individual btree records
  */
 
+#include <linux/limits.h>
+
 #include "hfsplus_fs.h"
 #include "hfsplus_raw.h"
 
@@ -20,41 +22,49 @@ static int hfs_btree_inc_height(struct hfs_btree *);
 u16 hfs_brec_lenoff(struct hfs_bnode *node, u16 rec, u16 *off)
 {
        __be16 retval[2];
-       u16 dataoff;
+       u16 data_off;
+       u16 next_off;
+
+       if (hfs_brec_record_invalid(node, rec)) {
+               *off = U16_MAX;
+               return U16_MAX;
+       }
 
-       dataoff = node->tree->node_size - (rec + 2) * 2;
-       hfs_bnode_read(node, retval, dataoff, 4);
+       data_off = node->tree->node_size - (rec + 2) * 2;
+       hfs_bnode_read(node, retval, data_off, 4);
        *off = be16_to_cpu(retval[1]);
-       return be16_to_cpu(retval[0]) - *off;
+       next_off = be16_to_cpu(retval[0]);
+       if (hfs_brec_offsets_invalid(node, *off, next_off)) {
+               *off = U16_MAX;
+               return U16_MAX;
+       }
+       return next_off - *off;
 }
 
 /* Get the length of the key from a keyed record */
 u16 hfs_brec_keylen(struct hfs_bnode *node, u16 rec)
 {
-       u16 retval, recoff;
+       u16 retval, recoff, len;
 
        if (node->type != HFS_NODE_INDEX && node->type != HFS_NODE_LEAF)
                return 0;
+       if (hfs_brec_record_invalid(node, rec))
+               return U16_MAX;
 
        if ((node->type == HFS_NODE_INDEX) &&
           !(node->tree->attributes & HFS_TREE_VARIDXKEYS) &&
           (node->tree->cnid != HFSPLUS_ATTR_CNID)) {
                retval = node->tree->max_key_len + 2;
        } else {
-               recoff = hfs_bnode_read_u16(node,
-                       node->tree->node_size - (rec + 1) * 2);
-               if (!recoff)
-                       return 0;
-               if (recoff > node->tree->node_size - 2) {
-                       pr_err("recoff %d too large\n", recoff);
-                       return 0;
-               }
+               len = hfs_brec_lenoff(node, rec, &recoff);
+               if (hfs_brec_len_invalid(node, len))
+                       return len;
 
                retval = hfs_bnode_read_u16(node, recoff) + 2;
                if (retval > node->tree->max_key_len + 2) {
                        pr_err("keylen %d too large\n",
                                retval);
-                       retval = 0;
+                       retval = U16_MAX;
                }
        }
        return retval;
@@ -181,14 +191,20 @@ int hfs_brec_remove(struct hfs_find_data *fd)
        struct hfs_btree *tree;
        struct hfs_bnode *node, *parent;
        int end_off, rec_off, data_off, size;
+       int res;
 
        tree = fd->tree;
        node = fd->bnode;
 again:
+       if (hfs_brec_record_invalid(node, fd->record))
+               return -EINVAL;
+
        rec_off = tree->node_size - (fd->record + 2) * 2;
        end_off = tree->node_size - (node->num_recs + 1) * 2;
 
        if (node->type == HFS_NODE_LEAF) {
+               if (tree->leaf_count == 0)
+                       return -EINVAL;
                tree->leaf_count--;
                mark_inode_dirty(tree->inode);
        }
@@ -205,7 +221,9 @@ again:
                hfs_bnode_put(node);
                node = fd->bnode = parent;
 
-               __hfs_brec_find(node, fd, hfs_find_rec_by_key);
+               res = __hfs_brec_find(node, fd, hfs_find_rec_by_key);
+               if (res && res != -ENOENT)
+                       return res;
                goto again;
        }
        hfs_bnode_write_u16(node,
@@ -368,6 +386,7 @@ static int hfs_brec_update_parent(struct hfs_find_data *fd)
        int newkeylen, diff;
        int rec, rec_off, end_rec_off;
        int start_off, end_off;
+       int res;
 
        tree = fd->tree;
        node = fd->bnode;
@@ -379,7 +398,9 @@ again:
        parent = hfs_bnode_find(tree, node->parent);
        if (IS_ERR(parent))
                return PTR_ERR(parent);
-       __hfs_brec_find(parent, fd, hfs_find_rec_by_key);
+       res = __hfs_brec_find(parent, fd, hfs_find_rec_by_key);
+       if (res && res != -ENOENT)
+               return res;
        if (fd->record < 0)
                return -ENOENT;
        hfs_bnode_dump(parent);
index 394542a47e60099728a32b343280050fdb17abc9..2ea8cd5658e1c95b318e04689fcaf125f8af0fa7 100644 (file)
@@ -168,8 +168,8 @@ static struct page *hfs_bmap_get_map_page(struct hfs_bnode *node,
        }
 
        ctx->len = hfs_brec_lenoff(node, rec_idx, &off16);
-       if (!ctx->len)
-               return ERR_PTR(-ENOENT);
+       if (hfs_brec_len_invalid(node, ctx->len))
+               return ERR_PTR(-EINVAL);
 
        if (!is_bnode_offset_valid(node, off16))
                return ERR_PTR(-EIO);
@@ -622,6 +622,12 @@ void hfs_bmap_free(struct hfs_bnode *node)
        if (IS_ERR(node))
                return;
        len = hfs_brec_lenoff(node, 2, &off);
+       if (hfs_brec_len_invalid(node, len)) {
+               pr_err("invalid bmap record length: node %u, len %u\n",
+                      node->this, len);
+               hfs_bnode_put(node);
+               return;
+       }
        while (nidx >= len * 8) {
                u32 i;
 
@@ -648,6 +654,12 @@ void hfs_bmap_free(struct hfs_bnode *node)
                        return;
                }
                len = hfs_brec_lenoff(node, 0, &off);
+               if (hfs_brec_len_invalid(node, len)) {
+                       pr_err("invalid bmap record length: node %u, len %u\n",
+                              node->this, len);
+                       hfs_bnode_put(node);
+                       return;
+               }
        }
 
        res = hfs_bmap_clear_bit(node, nidx);
index 7c8667d5a49c1dae4cbc5b0a1d41b5e1df1d7f00..1e5b58e6a13fc3bfbba86b7349b2b10c191edf8e 100644 (file)
@@ -593,6 +593,85 @@ bool is_bnode_offset_valid(struct hfs_bnode *node, u32 off)
        return is_valid;
 }
 
+static inline
+bool hfs_bnode_num_recs_invalid(struct hfs_bnode *node)
+{
+       u32 node_size;
+       u32 table_size;
+       u32 area_size;
+       u32 rec_size = sizeof(__be16);
+       u32 desc_size = sizeof(struct hfs_bnode_desc);
+
+       if (!node || !node->tree)
+               return true;
+
+       node_size = node->tree->node_size;
+       if (node_size < desc_size)
+               return true;
+
+       area_size = node_size - desc_size;
+       table_size = ((u32)node->num_recs + 1) * rec_size;
+
+       return table_size > area_size;
+}
+
+static inline
+bool hfs_brec_record_invalid(struct hfs_bnode *node, int record)
+{
+       if (hfs_bnode_num_recs_invalid(node))
+               return true;
+       if (record < 0)
+               return true;
+
+       return record >= node->num_recs;
+}
+
+static inline
+bool hfs_brec_offsets_invalid(struct hfs_bnode *node, u16 off, u16 next_off)
+{
+       u32 table_size;
+       u32 table_start;
+       u32 rec_size = sizeof(__be16);
+       u32 desc_size = sizeof(struct hfs_bnode_desc);
+
+       if (!node || !node->tree)
+               return true;
+
+       if (off < desc_size || (off & 1))
+               return true;
+
+       if (next_off <= off ||
+           next_off > node->tree->node_size ||
+           (next_off & 1))
+               return true;
+
+       table_size = ((u32)node->num_recs + 1) * rec_size;
+       table_start = node->tree->node_size - table_size;
+       if (next_off > table_start)
+               return true;
+
+       return false;
+}
+
+static inline
+bool hfs_brec_len_invalid(struct hfs_bnode *node, u16 len)
+{
+       if (!node || !node->tree)
+               return true;
+
+       return len == 0 || len > node->tree->node_size;
+}
+
+static inline
+void hfs_find_result_init(struct hfs_find_data *fd)
+{
+       fd->record = -1;
+       fd->keyoffset = -1;
+       fd->keylength = -1;
+       fd->entryoffset = -1;
+       fd->entrylength = -1;
+}
+
 static inline
 u32 check_and_correct_requested_length(struct hfs_bnode *node, u32 off, u32 len)
 {