hfs: rework MDB locking scheme

hfs_mdb_commit() used to hold the primary MDB buffer_head (mdb_bh)
locked for the whole commit, including writing the alternate MDB and
the volume bitmap. A corrupted image can set drVBMSt so the bitmap
block aliases mdb_bh. The bitmap writeback path then calls
lock_buffer() on that same buffer_head while it is already locked by
hfs_mdb_commit(). Finally, we have a deadlock during flushing the MDB
to the file system volume. However, even for valid images, the locking
scheme of holding the mdb_bh locked across this much unrelated I/O is
fragile anyway.

This patch adds a dedicated sbi->mdb_lock and take it around every
hfs_mdb_commit() caller (hfs_sync_fs(), flush_mdb(), and the initial
hfs_mdb_get() in hfs_fill_super()).

Additionally, this patch makes sbi->mdb/sbi->alt_mdb independent
in-memory copies (allocated with kmemdup()) instead of pointers into
mdb_bh's/alt_mdb_bh's page cache data. Every MDB field read or write in
hfs_mdb_commit(), hfs_mdb_close() and hfs_mdb_get() now operates on
these copies. Also, hfs_mdb_publish() and hfs_alt_mdb_publish() are
the only places left that touch the buffer_heads. The hfs_mdb_put()
frees the two copies.

Reported-by: Yue Sun <samsun1006219@gmail.com>
Link: https://lore.kernel.org/all/CAEkJfYMB47v1yOWHB8q2dc8kf=uj-rLO=+yMyudwPguJ8Kd3jA@mail.gmail.com/
Signed-off-by: Yue Sun <samsun1006219@gmail.com>
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/20260720184414.195213-2-slava@dubeyko.com
Signed-off-by: Viacheslav Dubeyko <slava@dubeyko.com>
This commit is contained in:
Viacheslav Dubeyko 2026-07-20 11:44:15 -07:00
parent c86df5cbba
commit f26da7e038
3 changed files with 87 additions and 23 deletions

View File

@ -64,14 +64,22 @@ struct hfs_inode_info {
* The HFS-specific part of a Linux (struct super_block)
*/
struct hfs_sb_info {
struct mutex mdb_lock; /* MDB operations lock */
struct buffer_head *mdb_bh; /* The hfs_buffer
holding the real
superblock (aka VIB
or MDB) */
struct hfs_mdb *mdb;
unsigned int mdb_offset; /* byte offset of the MDB
sector within mdb_bh's
data */
struct hfs_mdb *mdb; /* in-memory copy of the MDB */
struct buffer_head *alt_mdb_bh; /* The hfs_buffer holding
the alternate superblock */
struct hfs_mdb *alt_mdb;
unsigned int alt_mdb_offset; /* byte offset of the alternate
MDB sector within
alt_mdb_bh's data */
struct hfs_mdb *alt_mdb; /* in-memory copy of the
alternate MDB */
__be32 *bitmap; /* The page holding the
allocation bitmap */
struct hfs_btree *ext_tree; /* Information about

View File

@ -85,6 +85,39 @@ bool is_hfs_cnid_counts_valid(struct super_block *sb)
return !corrupted;
}
/*
* hfs_sect_offset() - get byte offset within the buffer_head.
*/
static unsigned int hfs_sect_offset(struct super_block *sb, sector_t sec)
{
loff_t start = (loff_t)sec << HFS_SECTOR_SIZE_BITS;
return start & (sb->s_blocksize - 1);
}
/*
* hfs_mdb_publish() - copy the in-memory primary MDB to the on-disk buffer.
*/
static void hfs_mdb_publish(struct hfs_sb_info *sbi)
{
lock_buffer(sbi->mdb_bh);
memcpy(sbi->mdb_bh->b_data + sbi->mdb_offset, sbi->mdb, HFS_SECTOR_SIZE);
mark_buffer_dirty(sbi->mdb_bh);
unlock_buffer(sbi->mdb_bh);
}
/*
* hfs_alt_mdb_publish() - copy the in-memory alternate MDB to its buffer.
*/
static void hfs_alt_mdb_publish(struct hfs_sb_info *sbi)
{
lock_buffer(sbi->alt_mdb_bh);
memcpy(sbi->alt_mdb_bh->b_data + sbi->alt_mdb_offset, sbi->alt_mdb,
HFS_SECTOR_SIZE);
mark_buffer_dirty(sbi->alt_mdb_bh);
unlock_buffer(sbi->alt_mdb_bh);
}
/*
* hfs_mdb_get()
*
@ -94,7 +127,7 @@ bool is_hfs_cnid_counts_valid(struct super_block *sb)
int hfs_mdb_get(struct super_block *sb)
{
struct buffer_head *bh;
struct hfs_mdb *mdb, *mdb2;
struct hfs_mdb *mdb, *alt_mdb;
unsigned int block;
char *ptr;
int off2, len, size, sect;
@ -158,7 +191,14 @@ int hfs_mdb_get(struct super_block *sb)
return -EIO;
}
mdb = kmemdup(mdb, HFS_SECTOR_SIZE, GFP_KERNEL);
if (!mdb) {
brelse(bh);
return -ENOMEM;
}
HFS_SB(sb)->mdb_bh = bh;
HFS_SB(sb)->mdb_offset = hfs_sect_offset(sb, part_start + HFS_MDB_BLK);
HFS_SB(sb)->mdb = mdb;
/* These parameters are read from the MDB, and never written */
@ -187,11 +227,18 @@ int hfs_mdb_get(struct super_block *sb)
/* TRY to get the alternate (backup) MDB. */
sect = part_start + part_size - 2;
bh = sb_bread512(sb, sect, mdb2);
bh = sb_bread512(sb, sect, alt_mdb);
if (bh) {
if (mdb2->drSigWord == cpu_to_be16(HFS_SUPER_MAGIC)) {
HFS_SB(sb)->alt_mdb_bh = bh;
HFS_SB(sb)->alt_mdb = mdb2;
if (alt_mdb->drSigWord == cpu_to_be16(HFS_SUPER_MAGIC)) {
alt_mdb = kmemdup(alt_mdb, HFS_SECTOR_SIZE, GFP_KERNEL);
if (alt_mdb) {
HFS_SB(sb)->alt_mdb_bh = bh;
HFS_SB(sb)->alt_mdb_offset =
hfs_sect_offset(sb, sect);
HFS_SB(sb)->alt_mdb = alt_mdb;
} else {
brelse(bh);
}
} else
brelse(bh);
}
@ -253,7 +300,7 @@ int hfs_mdb_get(struct super_block *sb)
be32_add_cpu(&mdb->drWrCnt, 1);
mdb->drLsMod = hfs_mtime();
mark_buffer_dirty(HFS_SB(sb)->mdb_bh);
hfs_mdb_publish(HFS_SB(sb));
sync_dirty_buffer(HFS_SB(sb)->mdb_bh);
}
@ -300,7 +347,6 @@ int hfs_mdb_commit(struct super_block *sb)
return -EIO;
}
lock_buffer(HFS_SB(sb)->mdb_bh);
if (test_and_clear_bit(HFS_FLG_MDB_DIRTY, &HFS_SB(sb)->flags)) {
/* These parameters may have been modified, so write them back */
mdb->drLsMod = hfs_mtime();
@ -314,8 +360,14 @@ int hfs_mdb_commit(struct super_block *sb)
mdb->drDirCnt =
cpu_to_be32((u32)atomic64_read(&HFS_SB(sb)->folder_count));
hfs_inode_write_fork(HFS_SB(sb)->ext_tree->inode, mdb->drXTExtRec,
&mdb->drXTFlSize, NULL);
hfs_inode_write_fork(HFS_SB(sb)->cat_tree->inode, mdb->drCTExtRec,
&mdb->drCTFlSize, NULL);
/* write MDB to disk */
mark_buffer_dirty(HFS_SB(sb)->mdb_bh);
hfs_mdb_publish(HFS_SB(sb));
sync_dirty_buffer(HFS_SB(sb)->mdb_bh);
}
/* write the backup MDB, not returning until it is written.
@ -330,18 +382,11 @@ int hfs_mdb_commit(struct super_block *sb)
goto out;
}
hfs_inode_write_fork(HFS_SB(sb)->ext_tree->inode, mdb->drXTExtRec,
&mdb->drXTFlSize, NULL);
hfs_inode_write_fork(HFS_SB(sb)->cat_tree->inode, mdb->drCTExtRec,
&mdb->drCTFlSize, NULL);
lock_buffer(HFS_SB(sb)->alt_mdb_bh);
memcpy(HFS_SB(sb)->alt_mdb, HFS_SB(sb)->mdb, HFS_SECTOR_SIZE);
memcpy(HFS_SB(sb)->alt_mdb, mdb, HFS_SECTOR_SIZE);
HFS_SB(sb)->alt_mdb->drAtrb |= cpu_to_be16(HFS_SB_ATTRIB_UNMNT);
HFS_SB(sb)->alt_mdb->drAtrb &= cpu_to_be16(~HFS_SB_ATTRIB_INCNSTNT);
unlock_buffer(HFS_SB(sb)->alt_mdb_bh);
mark_buffer_dirty(HFS_SB(sb)->alt_mdb_bh);
hfs_alt_mdb_publish(HFS_SB(sb));
sync_dirty_buffer(HFS_SB(sb)->alt_mdb_bh);
}
@ -377,7 +422,6 @@ int hfs_mdb_commit(struct super_block *sb)
}
}
out:
unlock_buffer(HFS_SB(sb)->mdb_bh);
return ret;
}
@ -392,7 +436,7 @@ void hfs_mdb_close(struct super_block *sb)
HFS_SB(sb)->mdb->drAtrb |= cpu_to_be16(HFS_SB_ATTRIB_UNMNT);
HFS_SB(sb)->mdb->drAtrb &= cpu_to_be16(~HFS_SB_ATTRIB_INCNSTNT);
mark_buffer_dirty(HFS_SB(sb)->mdb_bh);
hfs_mdb_publish(HFS_SB(sb));
}
/*
@ -408,6 +452,8 @@ void hfs_mdb_put(struct super_block *sb)
/* free the buffers holding the primary and alternate MDBs */
brelse(HFS_SB(sb)->mdb_bh);
brelse(HFS_SB(sb)->alt_mdb_bh);
kfree(HFS_SB(sb)->mdb);
kfree(HFS_SB(sb)->alt_mdb);
unload_nls(HFS_SB(sb)->nls_io);
unload_nls(HFS_SB(sb)->nls_disk);

View File

@ -34,8 +34,14 @@ MODULE_LICENSE("GPL");
static int hfs_sync_fs(struct super_block *sb, int wait)
{
int ret;
mutex_lock(&HFS_SB(sb)->mdb_lock);
is_hfs_cnid_counts_valid(sb);
return hfs_mdb_commit(sb);
ret = hfs_mdb_commit(sb);
mutex_unlock(&HFS_SB(sb)->mdb_lock);
return ret;
}
/*
@ -65,9 +71,10 @@ static void flush_mdb(struct work_struct *work)
sbi->work_queued = 0;
spin_unlock(&sbi->work_lock);
mutex_lock(&sbi->mdb_lock);
is_hfs_cnid_counts_valid(sb);
hfs_mdb_commit(sb);
mutex_unlock(&sbi->mdb_lock);
}
void hfs_mark_mdb_dirty(struct super_block *sb)
@ -338,9 +345,12 @@ static int hfs_fill_super(struct super_block *sb, struct fs_context *fc)
sb->s_op = &hfs_super_operations;
sb->s_xattr = hfs_xattr_handlers;
sb->s_flags |= SB_NOATIME | SB_NODIRATIME;
mutex_init(&sbi->mdb_lock);
mutex_init(&sbi->bitmap_lock);
mutex_lock(&sbi->mdb_lock);
res = hfs_mdb_get(sb);
mutex_unlock(&sbi->mdb_lock);
if (res) {
if (!silent)
pr_warn("can't find a HFS filesystem on dev %s\n",