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/sesNis 0600 root:operator atscsi_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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2612 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_testablefix.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
Confirmed kernel references
- sys/bus/cam/scsi/scsi_ses.c:1268
- sys/bus/cam/scsi/scsi_ses.c:1270
- sys/bus/cam/scsi/scsi_ses.c:1274
- sys/bus/cam/scsi/scsi_ses.c:1283
- sys/bus/cam/scsi/scsi_ses.c:1287
- sys/bus/cam/scsi/scsi_ses.c:998
- sys/bus/cam/scsi/scsi_ses.c:1025
- sys/bus/cam/scsi/scsi_ses.c:1010
- sys/bus/cam/scsi/scsi_ses.c:1047
- sys/bus/cam/scsi/scsi_ses.c:350
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).
No comments yet.