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)
PoC verification
Evidence pack
findings/poc/DF-5501 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
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)
- securelevel=-1: open /dev/pass0 β ok.
- sysctl kern.securelevel=2 β fresh open:
Operation not permitted(passopen's check working as designed). - 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_testableNot 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.
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.
No comments yet.