# DF-0931 — PoC: `setuid`-bit bypass in `ffs_write`

## Summary

`ffs_write()` (`sys/vfs/ufs/ufs_readwrite.c`) fails to clear the `ISUID`/
`ISGID` bits when a non-root user writes to a `setuid`-root binary on an FFS
filesystem and the write faults partway through (`EFAULT` after a partial
`copyin`). This allows a non-root attacker to replace the content of a
`setuid`-root binary while keeping the `setuid` bit, then `execv()` the
modified binary to obtain `uid=0`.

**Full unprivileged→root escalation demonstrated** on the default
`X86_64_GENERIC` (`#0`) kernel. See `VERDICT.md` for the detailed analysis.

## Root cause

The post-write ISUID-clearing check at `ufs_readwrite.c:400` uses
`resid > uio->uio_resid` to detect "were any bytes written to disk?"
When `uiomove()` returns `EFAULT` after a partial `copyin` (some bytes
copied, then unmapped page hit), it breaks **without** decrementing
`uio->uio_resid` (`kern_subr.c:148-149`). The dirty buffer (with attacker
bytes) is still `bdwrite()`'d to disk (`ufs_readwrite.c:389`), but the
`uio_resid` accounting doesn't reflect it, so the check evaluates to false
and `ISUID` is preserved.

The finding's proposed root cause (`int resid` truncation) is a real
type-safety defect but is **not sufficient** to explain the bypass — the
same ISUID preservation occurs with `size_t resid` and even with a 100-byte
write. The truncation is a contributing factor; the fundamental issue is
the `uiomove`-on-`EFAULT` accounting gap.

## Build & run

### Lab setup (simulates the realistic precondition)

```
# As root on the guest — create an FFS filesystem with a writable
# setuid-root binary (the realistic admin-misconfiguration scenario):
dd if=/dev/zero of=/var/tmp/ffs.img bs=1m count=64
vnconfig -c vn0 /var/tmp/ffs.img
newfs /dev/vn0
mkdir -p /mnt/ffs
mount -t ufs /dev/vn0 /mnt/ffs
cp /bin/sh /mnt/ffs/target
chown root:<attacker-gid> /mnt/ffs/target
chmod 04775 /mnt/ffs/target
```

### Build

```
./build.sh    # or: cc -O2 -o suid_bypass suid_bypass.c && cc -O2 -o exploit exploit.c
```

### Run (full escalation chain)

```
# As the unprivileged attacker:
./exploit /mnt/ffs/target          # writes ELF payload, ISUID preserved
echo id | /mnt/ffs/target          # exec: setuid(0) -> /bin/sh -> runs id
# Expected on vulnerable kernel: uid=0(root) gid=0(wheel)
```

### Minimal trigger (no escalation, just demonstrates ISUID preservation)

```
./suid_bypass /mnt/ffs/target
# Expected: "target mode=4775 ISUID=PRESERVED (BUG)"
```

## Notes

- The guest's root filesystem is HAMMER2 and `/tmp` is tmpfs — neither
  uses `ffs_write`. An FFS mount must be created explicitly.
- The precondition (non-root user with write access to a `setuid`-root
  binary on FFS) is not the default DragonFlyBSD configuration; system
  `setuid` binaries are `root:wheel 4755`. The scenario arises with
  custom/admin `setuid` tools, group-writable `setuid` binaries, or
  permissive container/jail images.
- `small_eFault.c` demonstrates the same bypass with only a 100-byte
  write (no 4 GiB needed), proving the `int` truncation is not required.
