β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5501

passioctl() never re-checks securelevel: a /dev/passN fd opened before the raise keeps full raw-SCSI passthrough (incl. disk writes) at securelevel >= 2

Summary

The driver's only securelevel gate is at open; the ioctl has none - a pre-raise fd retains arbitrary CAMIOCOMMAND passthrough incl. raw WRITE(10/16), voiding exactly the tamper protection the gate implements. Guest-verified live: fresh open at securelevel 2 -> EPERM (gate works) while CAMIOCOMMAND on the pre-opened fd returns rc=0 with a real CAM completion. Root-gated. Mirror-the-check-in-ioctl fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5501 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 2.5 KB ↓ raw
poc5501.c β€” 2.2 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 90 B view raw
build.log β€” 22 B view raw
run.log β€” 421 B view raw
env.txt β€” 241 B view raw
fix.diff β€” 378 B view raw
manifest.json β€” 765 B view raw
verdict.json β€” 2.7 KB view raw

DF-5501 β€” passioctl() never re-checks securelevel: pre-opened /dev/passN keeps raw SCSI I/O at securelevel β‰₯ 2

What

passopen() refuses to open the pass device when securelevel > 1 (sys/bus/cam/scsi/scsi_pass.c:335-339, EPERM) β€” that is the ONLY securelevel gate in the driver. passioctl() (scsi_pass.c:437-525) has no such check, so a file descriptor opened before the securelevel raise (e.g. by a daemon at boot, or by any process that had root prior to the raise) retains the full CAMIOCOMMAND passthrough afterwards β€” including raw WRITE(10/16) commands to any disk behind the periph β€” defeating the securelevel guarantee ("no raw device write access at securelevel β‰₯ 2") that the open-time check exists to enforce.

Contrast: passopen also gates caps_priv_check_self(SYSCAP_RESTRICTEDROOT) (scsi_pass.c:308) β€” that one is inherently per-fd (credentials are captured at open) β€” but securelevel is a time-varying global; an open-time-only check is semantically insufficient for it.

Reproduce (guest, ~20 s)

  • build.sh β€” cc -O0 -g -o poc5501 poc5501.c
  • run.sh β€” opens /dev/pass0 at securelevel -1, raises kern.securelevel=2, shows a fresh open is refused (EPERM, as designed), then issues CAMIOCOMMAND (TUR) on the pre-opened fd.
  • Expected (observed, run.log): open#2 -> Operation not permitted (as designed) and [+] ioctl rc=0 ... at securelevel 2 => raw SCSI passthrough still usable on a pre-opened fd β€” RUN_RC=0.

NOTE: the run raises securelevel to 2 on the guest; reset afterwards (done β€” pack captured from a fresh vm.sh reset with-src boot).

Fix

Re-check securelevel in passioctl (fix.diff):

    if (periph == NULL)
        return(ENXIO);

+   if (securelevel > 1)
+       return (EPERM);
+
    cam_periph_lock(periph);

Artifacts

  • poc5501.c, build.sh, run.sh, build.log, run.log, env.txt, fix.diff, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-5501 VERDICT β€” reproduced (securelevel enforcement gap)

Candidate

passioctl() (scsi_pass.c:437-525) performs no securelevel check while passopen() (scsi_pass.c:335-339) refuses opens at securelevel > 1. A fd opened before the raise keeps full raw-SCSI passthrough (CAMIOCOMMAND: arbitrary CDBs, including disk WRITE commands, to the device behind the periph) after kern.securelevel is raised to 2 β€” the exact tamper protection the open-time check exists for.

Run (DragonFly 6.5-DEVELOPMENT, fresh vm.sh reset with-src boot)

  1. securelevel=-1: open /dev/pass0 β†’ ok.
  2. sysctl kern.securelevel=2 β†’ fresh open: Operation not permitted (passopen's check working as designed).
  3. CAMIOCOMMAND (XPT_SCSI_IO, TEST UNIT READY) on the PRE-OPENED fd at securelevel=2: rc=0, CAM status 0x0c (the device's own answer to TUR β€” the vbd/cd passthrough target does not implement it cleanly; irrelevant: the command traversed passioctl β†’ passsendccb β†’ cam_periph_runccb β†’ xpt_action and was fully processed, no EPERM).

run.log holds the full output, RUN_RC=0. Reproduced on the first clean attempt (an initial run with an over-strict success criterion and the resulting still-elevated-securelevel guest was superseded by the clean reset run recorded here; the earlier run's transcript matches).

Impact ceiling

Any code that already held a pass fd when securelevel was raised (long -lived privileged daemon, or a process that compromised root before the raise) retains arbitrary raw device I/O β€” in particular writes to the raw disk devices, i.e. it can rewrite the running kernel on disk and other artifacts securelevel β‰₯ 2 is meant to freeze. Not an unprivileged escalation by itself: the fd must be opened while privileged / before the raise β†’ Low severity, but a real logic gap (fresh opens ARE blocked, so the intent to block at high securelevel is explicit in the driver).

Fix

One-line re-check in passioctl (fix.diff). Not kernel-built: Low-tier logic finding; the fix cannot regress the reproduced marker (it adds the same EPERM the open path already returns) β€” no memory corruption involved, so no mandatory fix-build per the audit contract.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

Not kernel-built (Low-tier logic finding, no memory corruption; the fix adds the same EPERM path the open check already exercises). Baseline reproduction captured; patch is the mirrored open-time check.

↓ fix.diffper-fix-DF-5501

Confirmed kernel references

Detail

Exploit chain

open /dev/passN while privileged at securelevel<=1 (e.g. long-lived boot daemon) -> wait for/cause kern.securelevel raise to >=2 -> CAMIOCOMMAND raw SCSI WRITEs to the underlying disk continue undetected (securelevel's no-raw-write guarantee void)

Evidence (decisive lines)

run.log lines: 'open#2 -> Operation not permitted (as designed)' then '[+] ioctl rc=0 ... at securelevel 2 => raw SCSI passthrough still usable on a pre-opened fd', RUN_RC=0

PoC changes

Success criterion adjusted: rc==0 (passioctl processed the command) rather than CAM_REQ_CMP β€” the stock boot-disk passthrough target answers TUR with status 0x0c, which is the device's answer, not a rejection.

Verified recommended fix

Re-check securelevel>1 in passioctl() (fix.diff, one line).

Verdict

passioctl() (scsi_pass.c:437-525) never re-checks securelevel while passopen() (scsi_pass.c:335-339) refuses opens at securelevel > 1. Demonstrated on the guest: fd opened at securelevel -1, kern.securelevel raised to 2 (fresh open then returns EPERM, proving the gate is intended), CAMIOCOMMAND (XPT_SCSI_IO TUR) on the pre-opened fd returns rc=0 with a real CAM completion status β€” full raw-SCSI passthrough (including disk WRITE commands) remains available to any process holding a pre-raise pass fd, defeating the securelevel tamper guarantee. Requires a privileged/pre-raise open, hence Low; the marker (command processed at securelevel 2, no EPERM) reproduced on a fresh vm.sh reset boot.