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

ses_getputstat swallows all decode/encode/device errors and returns 0 (fail-open enclosure status)

Field Value
ID DF-2612
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
CWE CWE-252 Unchecked Return Value
File sys/bus/cam/scsi/scsi_ses.c
Lines 1268-1287
Area bus
Confidence certain
Discovered 2026-08-28
Pass 2 (GLM 5.3 second pass)
Bucket base:bus
Reported pending
Known CVE none
CVE match novel

Summary

ses_getputstat() assigns ENODEV to err when ses_decode()/ses_encode() rejects device-returned data, and assigns the transport error of the SEND_DIAGNOSTIC write, but then unconditionally executes return (0) so every error is discarded. All four SES vector entry points consequently report success on malformed or short device data: enclosure CRITICAL/UNRECOVERABLE states are cached as all-zero "valid" status and control writes silently no-op while the ioctl returns 0.

Root cause

scsi_ses.c:1268-1287: after the RECEIVE_DIAGNOSTIC read succeeds, the function does if (ses_decode(...)) err = ENODEV; (1269-1271), if (ses_encode(...)) err = ENODEV; (1273-1274), and err = ses_runcmd(ssc, cdb, 6, sdata, &amt); for the SEND_DIAGNOSTIC write (1283) β€” then falls through to SES_FREE(sdata, bufsiz); return (0); (1286-1287). err is dead. Downstream: ses_get_objstat (1029-1044) gets 0 back, writes the bzero'd ComStat (zeroed at 1222) into ses_objmap[i].encstat and sets svalid=1, so SESIOC_GETOBJSTAT returns all-zero status cached as valid; ses_get_encstat (998-1008) sets ses_encstat = 0 | ENCI_SVALID; ses_set_objstat (1062-1064) and ses_set_encstat (1017-1021) return 0 even though nothing was written to the enclosure. This also masks the ses_ntypes=(uint8_t)ntype truncation (1190) and the 16-bit CDB allocation-length truncation (1250-1251): for >255-type enclosures the status page read is short, ses_decode fails, and the swallowed ENODEV turns a detectable failure into "all OK".

Threat model & preconditions

  • Attacker position: root (/dev/sesN is 0600 root:operator at scsi_ses.c:350-352) plus a malfunctioning or firmware-compromised SES / SAF-TE enclosure processor returning short or malformed diagnostic pages.
  • Privileges gained or impact: fail-open integrity β€” the kernel tells the monitoring daemon an enclosure is healthy when the device reported (or failed to report) critical conditions (fan/power/temperature/slot faults), and confirms control operations that never reached the hardware.
  • Required config or capabilities: root-gated device node; crafted or broken enclosure.
  • Reachability: any SESIOC_GETOBJSTAT / GETENCSTAT / SETOBJSTAT / SETENCSTAT against a device returning short/malformed pages.

Proof of concept

Build & run

host with a real or emulated SES enclosure (e.g. SCSI target emulator
returning a short RECEIVE DIAGNOSTIC status page); as root:
ioctl(open("/dev/ses0", O_RDWR), SESIOC_GETOBJSTAT, &os) with os.obj_id in
range, while the target truncates the Enclosure Status page below the offset
of that object (ses_decode hits 'idx+4 > amt' at scsi_ses.c:1454).

Expected output

ioctl returns 0 and os.cstat[] is all zeros even though the object's true
status was never delivered. Conversely SESIOC_SETOBJSTAT with cstat[0]=
SESCTL_CSEL while the SEND_DIAGNOSTIC fails (CHECK CONDITION): ioctl still
returns 0.

Impact

Monitoring daemons (sesutil et al.) receive fabricated "healthy" state during real enclosure emergencies; control operations are confirmed without effect. No memory-safety impact; no unprivileged trigger.

Return the computed error instead of a constant 0:

--- a/sys/bus/cam/scsi/scsi_ses.c
+++ b/sys/bus/cam/scsi/scsi_ses.c
@@ -1281,7 +1281,7 @@ ses_getputstat(ses_softc_t *ssc, int objid, SesComStat *sp, int slp, int in)
            amt = -bufsiz;
            err = ses_runcmd(ssc, cdb, 6, sdata, &amt);   
        }
    }
    SES_FREE(sdata, bufsiz);
-   return (0);
+   return (err);
 }

On error the callers already do the right thing: ses_get_objstat returns the error before caching status, and ses_set_objstat/ses_set_encstat propagate it to the ioctl, so the SESIOC_* handlers now fail closed.

References

  • SCSI ENC-SER / SAF-TE specs (RECEIVE/SEND DIAGNOSTIC)
  • ses(4), sesutil(8)

Timeline

  • 2026-08-28 Discovered during automated audit (pass 2, GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2612 Β· 5 files
FileTypeDescriptionSize
VERDICT.md β€” 3.6 KB ↓ raw
env.txt β€” 512 B view raw
check_env.sh β€” 748 B view raw
fix.diff β€” 343 B view raw
verdict.json β€” 3.4 KB view raw
VERDICT.md
↓ download raw

DF-2612 β€” ses_getputstat fail-open (sys/bus/cam/scsi/scsi_ses.c:1268-1287) VERIFICATION RUN (verify mode, code-trace only β€” no live device available)

CLAIM (as filed) ses_getputstat() computes err from ses_decode()/ses_encode() (ENODEV) and from the SEND_DIAGNOSTIC ses_runcmd(), then unconditionally returns 0, so all four SES ioctl vector entries report success on malformed/short device data or failed control writes.

CODE TRACE β€” CONFIRMED, line-precise, against sys/ @ git HEAD: * scsi_ses.c:1216 ses_getputstat() entry; sp bzero'd at :1222 (so a decode failure leaves an all-zero SesComStat). * :1268-1271 if (in) { if (ses_decode(...)) err = ENODEV; } * :1273-1274 if (ses_encode(...)) err = ENODEV; * :1276-1283 SEND_DIAGNOSTIC path, err = ses_runcmd(...) at :1283 (transport failure of the control write lands in err). * :1286-1287 SES_FREE(sdata, bufsiz); return (0); <-- err is DEAD. * Downstream (all four SESIOC_* vector entries): - ses_get_encstat :998-1008 β€” 0 return => ses_encstat = 0|ENCI_SVALID ("valid" all-OK overall status cached from device data that failed to decode). - ses_get_objstat :1025-1044 β€” 0 return => zeroed ComStat copied into ses_objmap[i].encstat and svalid=1 (cached-as-valid). - ses_set_encstat :1010-1022 β€” returns 0 even when nothing reached the enclosure (or the write errored). - ses_set_objstat :1047-1065 β€” returns 0 after a failed SEND_DIAGNOSTIC; the caller's view (SESIOC_SETOBJSTAT dispatch at :621-627) reports success to userland. Dispatch verified at sesioctl :541-551 (write ioctls need FWRITE) and :553-640 (GETENCSTAT/SETENCSTAT/GETOBJSTAT/SETOBJSTAT handlers). * Reachability: /dev/sesN is created make_dev(UID_ROOT, GID_OPERATOR, 0600) at :350-352 β€” root-gated device node; sesopen :386-435 requires the periph; ioctls reach ses_getputstat via the vector at :541+. Precondition beyond root: a SES/SAF-TE enclosure processor returning short or malformed diagnostic pages (firmware-compromised or broken enclosure, or a hostile SCSI peer).

LIVE TESTING β€” BLOCKED (missing hardware), precisely: * Guest: DragonFly 6.5-DEVELOPMENT #0..#5, QEMU/KVM, virtio storage + a single QEMU QEMU DVD-ROM at scbus1 target 0 lun 0 (sg0,pass0,cd0). camcontrol devlist shows NO device of type 0x0D (SES); /dev/sesN does not exist. QEMU cannot present an emulated SES enclosure target, and fabricating one (hacked kernel + fake enc device) was ruled out of scope for this run. * Therefore: no live reproduction attempted beyond the availability check; verdict = code-confirmed, live-unreachable (missing_setup). * The one-line fix is mechanical and its caller behavior is traced above; fix validation on real hardware remains open (fix_status=not_testable).

CORRECTNESS OF THE FIX (reasoned, not runtime-validated) return (err) is exactly the value the function already computed: - decode/encode failure => ENODEV (fail closed: callers stop caching / stop reporting success), - SEND_DIAGNOSTIC transport error => propagated, - success path => err == 0 (unchanged behavior for healthy devices). Callers already handle a non-zero return correctly (get_objstat returns before caching at :1032-1033; set_encstat/set_objstat propagate at :1017-1019/:1062-1064), so no caller adjustments are needed.

Artifacts: fix.diff (git-apply-able, -p1 from /usr/src), trace_notes.txt (this file), env.txt (guest inventory + blocker), verdict.json, manifest.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff is the mechanical one-liner (return err); applies cleanly (patch -p1 dry-run) but cannot be behavior-validated without an SES device

findings/poc/DF-2612/fix.diff
↓ fix.diffper-fix-DF-2612

Confirmed kernel references

Detail

Evidence (decisive lines)

['findings/poc/DF-2612/VERDICT.md β€” full line-precise trace of the dead err and all four downstream vector entries', 'findings/poc/DF-2612/env.txt β€” guest inventory proving the missing-setup blocker (no ses(4) device)', "findings/poc/DF-2612/fix.diff β€” 'return (err);' at scsi_ses.c:1287, dry-run verified against pristine sys/"]

PoC changes

verify mode, no seed: no PoC was runnable β€” an availability-check script (check_env.sh) documents the blocker

Verified recommended fix

scsi_ses.c:1287: return the computed error instead of 0 (return (err);) so SESIOC_GET/SET{ENCSTAT,OBJSTAT} fail closed

Verdict

CODE-CONFIRMED, LIVE-UNREACHABLE (missing_setup): the fail-open is certain from source β€” ses_getputstat assigns ENODEV to err at scsi_ses.c:1270/1274 and the SEND_DIAGNOSTIC transport error at :1283, then unconditionally 'return (0)' at :1287, so all four SESIOC vector entries (get_encstat :998-1008 caches 0|ENCI_SVALID, get_objstat :1025-1044 caches zeroed status as valid, set_encstat :1010-1022 and set_objstat :1047-1065 confirm unwritten control ops) report success on malformed/short device data. Live execution requires a SES/SAF-TE enclosure device: the QEMU guest has none (camcontrol devlist shows only a QEMU DVD-ROM; /dev/sesN absent) and QEMU cannot emulate an SES target, so no runtime reproduction was possible. The defect, its root cause, and the one-line fix are verified line-precisely; live impact on real hardware remains unexercised (root-gated 0600 node + crafted enclosure preconditions unchanged).