From: Andrew Price anprice@redhat.com
[ Upstream commit 557c024ca7250bb65ae60f16c02074106c2f197b ]
A fuzzer test introduced corruption that ends up with a depth of 0 in dir_e_read(), causing an undefined shift by 32 at:
index = hash >> (32 - dip->i_depth);
As calculated in an open-coded way in dir_make_exhash(), the minimum depth for an exhash directory is ilog2(sdp->sd_hash_ptrs) and 0 is invalid as sdp->sd_hash_ptrs is fixed as sdp->bsize / 16 at mount time.
So we can avoid the undefined behaviour by checking for depth values lower than the minimum in gfs2_dinode_in(). Values greater than the maximum are already being checked for there.
Also switch the calculation in dir_make_exhash() to use ilog2() to clarify how the depth is calculated.
Tested with the syzkaller repro.c and xfstests '-g quick'.
Reported-by: syzbot+4708579bb230a0582a57@syzkaller.appspotmail.com Signed-off-by: Andrew Price anprice@redhat.com Signed-off-by: Andreas Gruenbacher agruenba@redhat.com Signed-off-by: Sasha Levin sashal@kernel.org ---
LLM Generated explanations, may be completely bogus:
**Backport Status: YES**
This commit should be backported to stable kernel trees for the following reasons:
1. **Fixes a Critical Security Bug**: The commit fixes an undefined behavior caused by an invalid shift operation (`index = hash >> (32 - dip->i_depth)`) when `i_depth` is 0. This results in a shift by 32, which is undefined behavior in C and can lead to unpredictable results or crashes.
2. **Reported by Syzbot**: The bug was discovered by syzbot (Google's kernel fuzzer), indicating it's a real issue that can be triggered, not just theoretical. The commit message includes `Reported-by: syzbot+4708579bb230a0582a57@syzkaller.appspotmail.com`.
3. **Small and Contained Fix**: The changes are minimal and focused: - In `fs/gfs2/dir.c`: Simply replaces an open-coded calculation with `ilog2()` for clarity - In `fs/gfs2/glops.c`: Adds a validation check for `i_depth` to ensure it's not less than the minimum valid value
4. **Clear Boundary Checking**: The fix adds proper validation in `gfs2_dinode_in()`: ```c if ((ip->i_diskflags & GFS2_DIF_EXHASH) && depth < ilog2(sdp->sd_hash_ptrs)) { gfs2_consist_inode(ip); return -EIO; } ``` This ensures exhash directories have a valid minimum depth based on the hash pointer count.
5. **No Feature Changes**: The commit doesn't introduce new features or change the filesystem behavior - it only adds validation to prevent corruption from causing undefined behavior.
7. **Tested**: The commit message indicates it was tested with both the syzkaller reproducer and xfstests, providing confidence in the fix.
The fix prevents filesystem corruption from causing undefined behavior that could lead to crashes or security issues, making it an excellent candidate for stable backporting according to stable tree rules.
fs/gfs2/dir.c | 6 ++---- fs/gfs2/glops.c | 6 ++++++ 2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/fs/gfs2/dir.c b/fs/gfs2/dir.c index dbf1aede744c..509e2f0d97e7 100644 --- a/fs/gfs2/dir.c +++ b/fs/gfs2/dir.c @@ -60,6 +60,7 @@ #include <linux/crc32.h> #include <linux/vmalloc.h> #include <linux/bio.h> +#include <linux/log2.h>
#include "gfs2.h" #include "incore.h" @@ -912,7 +913,6 @@ static int dir_make_exhash(struct inode *inode) struct qstr args; struct buffer_head *bh, *dibh; struct gfs2_leaf *leaf; - int y; u32 x; __be64 *lp; u64 bn; @@ -979,9 +979,7 @@ static int dir_make_exhash(struct inode *inode) i_size_write(inode, sdp->sd_sb.sb_bsize / 2); gfs2_add_inode_blocks(&dip->i_inode, 1); dip->i_diskflags |= GFS2_DIF_EXHASH; - - for (x = sdp->sd_hash_ptrs, y = -1; x; x >>= 1, y++) ; - dip->i_depth = y; + dip->i_depth = ilog2(sdp->sd_hash_ptrs);
gfs2_dinode_out(dip, dibh->b_data);
diff --git a/fs/gfs2/glops.c b/fs/gfs2/glops.c index 4b6b23c638e2..1ed42f0e6ec7 100644 --- a/fs/gfs2/glops.c +++ b/fs/gfs2/glops.c @@ -11,6 +11,7 @@ #include <linux/bio.h> #include <linux/posix_acl.h> #include <linux/security.h> +#include <linux/log2.h>
#include "gfs2.h" #include "incore.h" @@ -450,6 +451,11 @@ static int gfs2_dinode_in(struct gfs2_inode *ip, const void *buf) gfs2_consist_inode(ip); return -EIO; } + if ((ip->i_diskflags & GFS2_DIF_EXHASH) && + depth < ilog2(sdp->sd_hash_ptrs)) { + gfs2_consist_inode(ip); + return -EIO; + } ip->i_depth = (u8)depth; ip->i_entries = be32_to_cpu(str->di_entries);