[BUG] freevxfs: divide-by-zero in vxfs_bmap_ext4() causes kernel oops on mount

From: Đức Cảnh Nguyễn

Date: Sat Aug 01 2026 - 06:21:09 EST


Subject: freevxfs: divide-by-zero in vxfs_bmap_ext4() causes kernel oops on mount

To: Christoph Hellwig <hch@xxxxxxxxxxxxx>
Cc: linux-kernel@xxxxxxxxxxxxxxx

Hi Christoph,

I found a divide-by-zero in vxfs_bmap_ext4() (fs/freevxfs/vxfs_bmap.c).
Mounting a crafted VxFS image triggers a kernel oops ("divide error")
right at mount time -- no read or ioctl needed. A full repro (image,
initramfs, QEMU script, logs) is here:

https://drive.google.com/file/d/1UKsDOxX8aUHeG2AVsk2VIcTLobWAORl9/view?usp=sharing

Summary of the bug:

- File: fs/freevxfs/vxfs_bmap.c, vxfs_bmap_ext4()
- Type: integer divide-by-zero -> kernel oops (DoS), local, needs
  CAP_SYS_ADMIN to mount a crafted image
- Present since: Linux-2.6.12-rc2 (1da177e4c3f4); never fixed

Two paths hit the divide-by-zero:

1. line 62: ve4_indsize == 0 -> denominator (indsize * indsize * bsize / 4)
   is 0. The existing guard (line 52) only rejects indsize > s_blocksize,
   so 0 gets through.

2. line 73: if block 0 isn't covered by the first direct extent (size == 0),
   bn stays 0 after the direct loop, and with indsize > 0 the _expression_
   (bn / indsize) % (indsize * bn) becomes 0 % 0.

The recent freevxfs fix (704d48d81dc4) only touched vxfs_bmap_typed(),
not vxfs_bmap_ext4(), so the bug is still present in mainline.

Reproduced in QEMU on Ubuntu 7.0.0-28-generic:

  Oops: divide error: 0000 [#1] SMP NOPTI
  RIP: 0010:vxfs_bmap_ext4+0xfc/0x1c0 [freevxfs]
  RAX: 0000000000000000 RBX: 0000000000000000 RDX: 0000000000000000
  Call Trace:
    vxfs_bmap_ext4+0xfc/0x1c0 [freevxfs]
    vxfs_bmap1+0x42/0x70 [freevxfs]
    vxfs_getblk+0x17/0x70 [freevxfs]
    block_read_full_folio+0x109/0x270
    vxfs_read_folio+0x18/0x30 [freevxfs]
    vxfs_get_page+0x13/0x40 [freevxfs]
    __vxfs_iget+0x3a/0xd0 [freevxfs]
    vxfs_iget+0x5b/0x1a0 [freevxfs]
    vxfs_fill_super+0x159/0x340 [freevxfs]
    get_tree_bdev_flags+0x141/0x1e0
    ...
    __x64_sys_mount+0x12b/0x160

The crash happens inside vxfs_fill_super(), i.e. during mount itself.

Suggested fix (also in the Drive zip):

diff --git a/fs/freevxfs/vxfs_bmap.c b/fs/freevxfs/vxfs_bmap.c
--- a/fs/freevxfs/vxfs_bmap.c
+++ b/fs/freevxfs/vxfs_bmap.c
@@ -49,7 +49,7 @@ vxfs_bmap_ext4(struct inode *ip, long bn)
  unsigned long bsize = sb->s_blocksize;
  u32 indsize = fs32_to_cpu(sbi, vip->vii_ext4.ve4_indsize);
  int i;
 
- if (indsize > sb->s_blocksize)
+ if (indsize == 0 || indsize > sb->s_blocksize)
  goto fail_size;
 
  for (i = 0; i < VXFS_NDADDR; i++) {
@@ -62,7 +62,8 @@ vxfs_bmap_ext4(struct inode *ip, long bn)
  bn -= fs32_to_cpu(sbi, d->size);
  }
 
- if ((bn / (indsize * indsize * bsize / 4)) == 0) {
+ if (bn == 0)
+ goto fail_buf;
+ if ((bn / (indsize * indsize * bsize / 4)) == 0) {
  struct buffer_head *buf;
  daddr_t bno;
  __fs32 *indir;

Environment note about the repro: it was built and tested on a machine
whose /boot already ships vmlinuz-7.0.0-28-generic (I did not build a
kernel from source). The image targets the Ubuntu struct vxfs_sb layout
(verified via module BTF: vs_bsize@32, vs_oltext[0]@368, vs_oltsize@376);
mainline offsets differ (vs_bsize@24, vs_oltext[0]@356, vs_oltsize@364)
and the PoC script documents both. The bug itself is in shared mainline
code and is independent of these offsets.

Thanks,
canhnguyen26 (Nguyen Duc Canh) 
# freevxfs: divide-by-zero in `vxfs_bmap_ext4()` — kernel oops on mount

**Component:** `fs/freevxfs/vxfs_bmap.c`
**Impact:** Local denial of service (kernel oops / `divide error`); reproducible by mounting a crafted VxFS image. Requires CAP_SYS_ADMIN.
**Introduced:** Linux-2.6.12-rc2 (`1da177e4c3f4`), never fixed.
**Affected kernel (tested):** Ubuntu `7.0.0-28-generic` (vmlinuz from `/boot`, booted in QEMU). Bug present in mainline up to at least `v7.2-rc5`.

## 1. Summary

`vxfs_bmap_ext4()` performs integer divisions whose divisor is fully
controlled by attacker-supplied on-disk inode fields and does not guard
against zero:

- Line 62 divides `bn` by `indsize * indsize * bsize / 4`.
- Line 73 evaluates `(bn / indsize) % (indsize * bn)`.

Two independent paths yield a divisor of zero, raising `#DE` in the kernel:

| Path | Trigger | Faulting expression |
|------|---------|---------------------|
| A | `ve4_indsize == 0` | `bn / (0 * 0 * bsize / 4)` → `bn / 0` |
| B | `bn == 0` (block 0 not covered by a direct extent) with `indsize > 0` | `(0 / indsize) % (indsize * 0)` → `0 % 0` |

The only guard is at line 52, which rejects `indsize > s_blocksize` but
allows `indsize == 0`.

## 2. Vulnerable code (mainline @ `02dc699f83d0`)

- Function: [fs/freevxfs/vxfs_bmap.c#L42-L86](https://github.com/torvalds/linux/blob/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_bmap.c#L42-L86)
- Guard (too weak): [L52](https://github.com/torvalds/linux/blob/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_bmap.c#L52)
- Direct-extent loop (can leave `bn` unchanged): [L55-L60](https://github.com/torvalds/linux/blob/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_bmap.c#L55-L60)
- Div-by-zero #1: [L62](https://github.com/torvalds/linux/blob/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_bmap.c#L62)
- Div-by-zero #2: [L73](https://github.com/torvalds/linux/blob/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_bmap.c#L73)

```c
static daddr_t
vxfs_bmap_ext4(struct inode *ip, long bn)
{
...
u32 indsize = fs32_to_cpu(sbi, vip->vii_ext4.ve4_indsize); /* L49: disk-controlled */
...
if (indsize > sb->s_blocksize) /* L52: only rejects "too big" */
goto fail_size;

for (i = 0; i < VXFS_NDADDR; i++) { /* L55-60 */
struct direct *d = vip->vii_ext4.ve4_direct + i;
if (bn >= 0 && bn < fs32_to_cpu(sbi, d->size))
return (bn + fs32_to_cpu(sbi, d->extent));
bn -= fs32_to_cpu(sbi, d->size);
}

if ((bn / (indsize * indsize * bsize / 4)) == 0) { /* L62: DIV-0 #1 */
...
bno = fs32_to_cpu(sbi, indir[(bn / indsize) % (indsize * bn)]) + /* L73: DIV-0 #2 */
(bn % indsize);
...
```

### Path A — `ve4_indsize == 0`

`ve4_indsize` is copied verbatim from the on-disk inode in
[dip2vip_cpy()](https://github.com/torvalds/linux/blob/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_inode.c#L76-L116)
(`vdi_ext4.ve4_indsize`, offset 84 in the inode). With `indsize == 0` the
denominator `0 * 0 * bsize / 4 == 0`, so `bn / 0` faults for any `bn`
that escapes the direct loop (e.g. all `ve4_direct[].size == 0`).

### Path B — `bn == 0` with `indsize > 0`

If block 0 is not covered by `ve4_direct[0]` (its `size` field is 0), the
loop at L55-60 leaves `bn == 0`. Then `0 / positive == 0` enters the `if`
block and line 73 computes `(0 / indsize) % (0)` = `0 % 0`.

## 3. Root cause

`vxfs_bmap_ext4()` was added in [1da177e4c3f4 ("Linux-2.6.12-rc2")](https://github.com/torvalds/linux/commit/1da177e4c3f415f28ad2e1298974ff1c7a7c9280)
and never audited for zero divisors:

- [git blame of L62](https://github.com/torvalds/linux/blame/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_bmap.c#L62) — unchanged since 2005.
- [git blame of L73](https://github.com/torvalds/linux/blame/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_bmap.c#L73) — only endian-swap churn (`0d83f7fc83f7`) since.

The June 2026 fix by Farhad Alemi
([704d48d81dc4 "freevxfs: don't BUG() on unknown typed-extent type"](https://github.com/torvalds/linux/commit/704d48d81dc41470e108811c32c577ada66192d4))
only touched `vxfs_bmap_typed()` (default case); `vxfs_bmap_ext4()` was
not addressed. The bug is therefore still present in mainline.

## 4. Reproducer

A complete, self-contained repro environment lives in this folder:

```
repro/vxfs-bmap-ext4-div0/
├── build_initramfs.sh # builds qemu/initramfs.img.gz (busybox + module + PoC image)
├── run.sh # boots kernel in QEMU and triggers the bug
├── poc/
│ ├── make_vxfs_poc.py # crafts the malicious VxFS image
│ └── vxfs_poc.img # pre-built image (16 KB, 1024-byte blocks)
├── initramfs/
│ ├── init # init script (insmod + mount)
│ └── freevxfs.ko # module from Ubuntu 7.0.0-28-generic
├── qemu/initramfs.img.gz # (generated)
├── logs/
│ ├── boot_console.log # full boot + oops console capture
│ └── oops_host_7.0.0-28.txt # extracted oops
└── patches/freevxfs-fix-divide-by-zero.patch
```

### How the image triggers the bug

`make_vxfs_poc.py` builds a minimal VxFS filesystem:

- Superblock @blk1 with valid magic `0xa501FCF5`, `vs_bsize = 1024`,
OLT pointer and OLT size — enough for `vxfs_fill_super()` to proceed
([vxfs_super.c#L228-L262](https://github.com/torvalds/linux/blob/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs_super.c#L228-L262)).
- OLT @blk2 with FSHEAD + ILIST entries.
- Inode list @blk3; structural FSH @blk6; primary FSH @blk7; STILIST @blk8;
ilist @blk9 containing the root inode (ino 2).
- Root inode (ino 2): `vdi_orgtype = VXFS_ORG_EXT4 (1)`,
`ve4_indsize = 0`, all `ve4_direct[].size = 0`, `vdi_size > 0`,
mode `S_IFDIR | 0x1ff` — this is **Path A**.

During mount, `vxfs_fill_super()` calls `__vxfs_iget()` on the root inode,
which reads inode metadata through `vxfs_get_page()` → `vxfs_read_folio()`
→ `block_read_full_folio()` → `vxfs_getblk()` → `vxfs_bmap1()` →
`vxfs_bmap_ext4()` → **#DE**.

### Steps to reproduce

```bash
# 0. prerequisites (host)
sudo apt install qemu-system-x86 busybox-static cpio gzip # already installed on the dev box

# 1. (re)build the initramfs
./build_initramfs.sh

# 2. run QEMU using the host kernel image
sudo bash run.sh /boot/vmlinuz-$(uname -r) # or: sudo VMLINUZ=/tmp/vmlinuz-host bash run.sh

# 3. expected output (grep of run_console.log)
# Oops: divide error: 0000 [#1] SMP NOPTI
# RIP: 0010:vxfs_bmap_ext4+0xfc/0x1c0 [freevxfs]
```

### Environment limitations (important)

The PoC was developed and verified on a **single Ubuntu 24.04 machine**
whose boot loader already ships `/boot/vmlinuz-7.0.0-28-generic`. We did
**not** build a kernel from source, so reproducibility depends on:

1. **Kernel image** — `run.sh` boots the machine's own
`/boot/vmlinuz-$(uname -r)` (fallback: explicit path). Verified run:
`7.0.0-28-generic`.
2. **Module version match** — `initramfs/freevxfs.ko` was extracted from
the 7.0.0-28-generic module set (`/lib/modules/7.0.0-28-generic/.../freevxfs.ko.zst`)
and is `insmod`-ed inside the initramfs. `insmod` requires vermagic to
match the booted kernel. For another kernel, replace the module with
that kernel's freevxfs.ko (or build it: `make M=fs/freevxfs` after
building that kernel).
3. **On-disk struct layout** — `poc/vxfs_poc.img` targets the *Ubuntu*
`struct vxfs_sb` (verified via module BTF: `vs_bsize@32`,
`vs_oltext[0]@368`, `vs_oltsize@376`). Mainline offsets differ
(`vs_bsize@24`, `vs_oltext[0]@356`, `vs_oltsize@364`); the PoC script
documents both sets and can be flipped for mainline.
4. **Root / CAP_SYS_ADMIN** is required to mount a crafted filesystem.

The divide-by-zero in `vxfs_bmap_ext4()` itself (shared mainline code,
`vxfs_bmap.c:62`/`:73`) is independent of these offsets -- only the
triggering image is layout-specific.

## 5. Observed crash (tested on Ubuntu 7.0.0-28-generic)

Full console capture: [`logs/boot_console.log`](logs/boot_console.log)
Extracted oops: [`logs/oops_host_7.0.0-28.txt`](logs/oops_host_7.0.0-28.txt)

```
Oops: divide error: 0000 [#1] SMP NOPTI
RIP: 0010:vxfs_bmap_ext4+0xfc/0x1c0 [freevxfs]
RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
RDX: 0000000000000000 <-- divisor == 0
Call Trace:
vxfs_bmap_ext4+0xfc/0x1c0 [freevxfs]
vxfs_bmap1+0x42/0x70 [freevxfs]
vxfs_getblk+0x17/0x70 [freevxfs]
block_read_full_folio+0x109/0x270
vxfs_read_folio+0x18/0x30 [freevxfs]
vxfs_get_page+0x13/0x40 [freevxfs]
__vxfs_iget+0x3a/0xd0 [freevxfs]
vxfs_iget+0x5b/0x1a0 [freevxfs]
vxfs_fill_super+0x159/0x340 [freevxfs]
get_tree_bdev_flags+0x141/0x1e0
get_tree_bdev+0x10/0x20
vxfs_get_tree+0x15/0x20 [freevxfs]
vfs_get_tree+0x2a/0x100
fc_mount+0x14/0xa0
do_new_mount+0x1bb/0x370
path_mount+0x1e0/0x6f0
__x64_sys_mount+0x12b/0x160
...
```

Key observations:

- Faulting instruction is the second `div` in `vxfs_bmap_ext4`
(bytes `48 99 49 f7 fc ... <49 f7 fc>` — see the `Code:` line in the log),
matching line 73 (`0 % 0`).
- All three registers involved (`RAX`, `RBX`, `RDX`) are zero.
- The crash happens **inside `vxfs_fill_super()`, i.e. at `mount` time** —
no `ls`/`read`/`FIBMAP` needed. This makes the bug trivially reachable:
any user allowed to mount a crafted VxFS image (root or CAP_SYS_ADMIN)
crashes the kernel.

## 6. Proposed fix

Patch: [`patches/freevxfs-fix-divide-by-zero.patch`](patches/freevxfs-fix-divide-by-zero.patch)

```c
- if (indsize > sb->s_blocksize)
+ if (indsize == 0 || indsize > sb->s_blocksize)
goto fail_size;
...
- if ((bn / (indsize * indsize * bsize / 4)) == 0) {
+ if (bn == 0)
+ goto fail_buf;
+ if ((bn / (indsize * indsize * bsize / 4)) == 0) {
```

- `indsize == 0` closes **Path A**.
- `bn == 0` (after the direct loop) closes **Path B**: for a valid EXT4
inode, block 0 must always be covered by the first direct extent, so
reaching line 62 with `bn == 0` only happens for corrupted images —
bailing out with `fail_buf` (returns 0) is safe.

## 7. Notes / follow-ups

- The index expression `(bn / indsize) % (indsize * bn)` itself is also
suspicious (it can index past `buf->b_data` for large `bn`) but that is
outside the scope of this report.
- An older, weaker repro path (mount succeeds, then `ls`/`FIBMAP` triggers
the bug) is documented in the analysis notes; the current PoC triggers at
mount time, which is stronger.
- On mainline the struct offsets are `vs_bsize@24`, `vs_oltext[0]@356`,
`vs_oltsize@364` ([vxfs.h#L44-L132](https://github.com/torvalds/linux/blob/02dc699f83d04069fdabc996fc22d47cda47a4a9/fs/freevxfs/vxfs.h#L44-L132)).
Ubuntu's `7.0.0-28` kernel inserts `__unused1/__unused2/__unused3`
(verified via module BTF), shifting offsets to `vs_bsize@32`,
`vs_oltext[0]@368`, `vs_oltsize@376`; the bundled PoC already uses the
Ubuntu layout since it was tested there. `make_vxfs_poc.py` notes both.

## 8. Disclosure timeline

- 2026-08-01: bug identified; crafted PoC image; reproduced oops in QEMU on
Ubuntu 7.0.0-28-generic.
- 2026-08-01: report + patch prepared. Pending disclosure decision.

---

**Reporter:** canhnguyen26 (Nguyen Duc Canh) <0206canh@xxxxxxxxx>
**Component maintainer:** Christoph Hellwig <hch@xxxxxxxxxxxxx>
**Draft submission email:** [`patches/email-draft.txt`](patches/email-draft.txt)