Off-by-one bounds checks in SES config-page walkers allow a 1-byte heap over-read past the 8192-byte sdata allocation
| Field | Value |
|---|---|
| ID | DF-2613 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:N |
| CWE | CWE-125 Out-of-bounds Read |
| File | sys/bus/cam/scsi/scsi_ses.c |
| Lines | 1311-1365 |
| 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_enchdr(), ses_encdesc() and ses_getthdr() guard their reads of
buffer[off+3] with if (off + 3 > amt) return (-1), which admits
off+3 == amt, so the loop-advance and header reads touch buffer[amt] β
one byte past the valid data. In ses_getconfig() the buffer is an exact
kmalloc(SCSZ=8192) (scsi_ses.c:1086) and amt = SCSZ - resid, so a device
that returns exactly 8192 bytes with resid 0 lets a crafted subenclosure walk
(VEnclen chain landing on off == 8189) read sdata[8192], one byte beyond
the heap allocation.
Root cause
scsi_ses.c:1311-1321 (ses_enchdr): if (off + 3 > amt) return (-1);
followed by off += buffer[off+3] + 4; and the post-loop check plus
gget8(buffer, off+3, chp->VEnclen) β all permit index == amt. Identical
predicates at scsi_ses.c:1329-1337 (ses_encdesc) and
scsi_ses.c:1358-1365 (ses_getthdr). The correct form is already used later
in ses_getthdr itself (if (amt < (off + 4)) return (-1); at 1367) and in
ses_decode/ses_encode (if (idx+4 > amt) at 1454/1526). The byte read is
used only as a skip length whose effect is re-bounded on the next iteration,
and ses_encdesc's copy length stays min(enclen<=255, amt-off) into the
304-byte stack buffer, so no length amplification is possible from the
mis-read byte.
Threat model & preconditions
- Attacker position: crafted SES enclosure processor (or compromised enclosure firmware / SCSI bus peer) supplying a maximal-length Configuration Page whose subenclosure descriptor lengths walk the parser offset exactly onto the 8192-byte boundary while resid == 0; parsing runs at first open of /dev/sesN (root-gated, node is 0600).
- Privileges gained or impact: single-byte read of adjacent kernel heap (DFly kmalloc(8192) is a power-of-two zone, adjacent object is mapped, so no fault); the value never reaches userspace and cannot enlarge any later copy. Defense-in-depth.
- Required config or capabilities: root + controllable SES target.
- Reachability: sesopen β ses_getconfig on a crafted Configuration Page.
Proof of concept
Build & run
SCSI target emulator answering RECEIVE DIAGNOSTIC page 1: return exactly 8192 bytes (resid 0), Nsubenc large, and VEnclen chain landing the walker at off == 8189; open /dev/ses0 as root. Observable only under KMALLOC_GUARD / malloc debug zones (guard kernel panics on the touched byte).
Expected output
on a stock kernel the read is silent (adjacent mapped heap); on an instrumented kernel, a guard/redzone panic at scsi_ses.c:1314/1332/1361.
Impact
Defense-in-depth finding; no demonstrated crash, leak, or corruption β the mis-read byte never propagates to a length, pointer, or copyout.
Recommended fix
Tighten the three walkers so reading buffer[off+3] requires off+4 <= amt,
matching the predicate style already used at ses_getthdr:1367 and
ses_decode:1454:
--- a/sys/bus/cam/scsi/scsi_ses.c
+++ b/sys/bus/cam/scsi/scsi_ses.c
@@ -1308,11 +1308,11 @@ ses_enchdr(uint8_t *buffer, int amt, uint8_t SubEncId, SesEncHdr *chp)
{
int s, off = 8;
for (s = 0; s < SubEncId; s++) {
- if (off + 3 > amt)
+ if (off + 4 > amt)
return (-1);
off += buffer[off+3] + 4;
}
- if (off + 3 > amt) {
+ if (off + 4 > amt) {
return (-1);
}
gget8(buffer, off+1, chp->Subencid);
@@ -1326,11 +1326,11 @@ ses_encdesc(uint8_t *buffer, int amt, uint8_t SubEncId, SesEncDesc *cdp)
{
int s, e, enclen, off = 8;
for (s = 0; s < SubEncId; s++) {
- if (off + 3 > amt)
+ if (off + 4 > amt)
return (-1);
off += buffer[off+3] + 4;
}
- if (off + 3 > amt) {
+ if (off + 4 > amt) {
return (-1);
}
gget8(buffer, off+3, enclen);
@@ -1355,11 +1355,11 @@ ses_getthdr(uint8_t *buffer, int amt, int nth, SesThdr *thp)
int s, off = 8;
if (amt < SES_CFGHDR_MINLEN) {
return (-1);
}
for (s = 0; s < buffer[1]; s++) {
- if (off + 3 > amt)
+ if (off + 4 > amt)
return (-1);
off += buffer[off+3] + 4;
}
- if (off + 3 > amt) {
+ if (off + 4 > amt) {
return (-1);
}
off += buffer[off+3] + 4 + (nth * 4);
Each changed predicate still permits every parse that was previously in-bounds
(it only rejects the previously-admitted off+3 == amt edge), so no
legitimate Configuration Page is newly rejected.
References
- ses(4); SES (SCSI Enclosure Services) spec Β§Configuration Diagnostic Page
Timeline
- 2026-08-28 Discovered during automated audit (pass 2, GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2613 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | β | 3.1 KB | β raw | |
| env.txt | β | 512 B | view raw | |
| check_env.sh | β | 748 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-2613 β SES config-page walker off-by-one 1-byte heap over-read (sys/bus/cam/scsi/scsi_ses.c:1311-1365)
CLAIM (as filed)
ses_enchdr/ses_encdesc/ses_getthdr guard buffer[off+3] with
if (off + 3 > amt), which admits off+3 == amt, so the reads at
off += buffer[off+3] + 4 / gget8(buffer, off+3, ...) touch
buffer[amt] β one byte past the valid data. With a maximal
(resid==0) Configuration Page the buffer is the exact 8192-byte
kmalloc(SCSZ) and amt == 8192, so a crafted VEnclen chain landing the
walker on off == 8189 reads sdata[8192].
CODE TRACE β CONFIRMED, line-precise, against sys/ @ git HEAD:
* Allocation: ses_getconfig :1086 sdata = SES_MALLOC(SCSZ);
SES_MALLOC == kmalloc(amt, M_SCSISES, M_INTWAIT) at :127;
SCSZ == 0x2000 == 8192 at :937.
* Transfer size: :1077-1079 CDB requests exactly SCSZ bytes;
:1090-1096 amt = SCSZ; ses_runcmd(...); amt = SCSZ - amt;
ses_runcmd sets *dlenp = ccb->csio.resid on success (:695), so a
device that transfers all 8192 bytes leaves amt == 8192.
* Walkers (buffer bounds are 0..amt-1, i.e. reading index off+3 is safe
only when off+4 <= amt):
ses_enchdr :1311-1321 β loop guard :1312 off + 3 > amt, read
:1314 buffer[off+3]; post-loop guard :1316, reads via
gget8(buffer, off+3, ...) at :1319-1321.
ses_encdesc :1326-1347 β same guards at :1330/:1334, reads
:1332/:1337.
ses_getthdr :1351-1369 β same guards at :1359/:1363, read :1366.
In every case off+3 == amt passes the guard and indexes buffer[amt].
* Entry path: sesopen :428-433 -> ses_vec.softc_init -> ses_getconfig
(call at :988) on first open of the root-gated /dev/sesN (make_dev
0600 at :350-352).
* Blast radius (why Info, not higher): the mis-read byte is used only
as a subenclosure skip length (buffer[off+3]+4) whose effect is
re-bounded on the next iteration or by the following checks
(ses_encdesc's off >= amt at :1339, e > amt clamp at :1342-1345
with the copy staying inside the 304-byte stack buffer; ses_getthdr's
amt < off+4 at :1367). The byte never propagates to a copyout,
pointer, or length that reaches userspace. DFly kmalloc(8192) is a
power-of-2 zone: the adjacent object is mapped, so no fault occurs.
LIVE TESTING β BLOCKED (missing hardware), same blocker as DF-2612: guest has no SES device (camcontrol devlist: only a QEMU DVD-ROM), /dev/sesN absent; QEMU cannot emulate an SES target. A guard-page observation would additionally require a KMALLOC_GUARD-style kernel. verdict = code-confirmed, live-unreachable (missing_setup).
FIX
Tighten the three loop guards and three post-loop guards from
off + 3 > amt to off + 4 > amt (6 changed predicates) β matches the
predicate style already used at ses_getthdr:1367 and ses_decode:1454 /
ses_encode:1526. Only the previously-admitted off+3 == amt edge is
rejected; every previously-in-bounds parse remains accepted.
fix.diff applies cleanly with patch -p1 from /usr/src (dry-run
verified against the read-only audit tree).
Artifacts: fix.diff, trace_notes.txt (this file), env.txt, verdict.json, manifest.json.
Fix verification
not_testablefix.diff applies cleanly (patch -p1 dry-run against pristine sys/) and is provably tightening-only; cannot be behavior-validated without an SES target presenting a maximal crafted page
findings/poc/DF-2613/fix.diff
Confirmed kernel references
- sys/bus/cam/scsi/scsi_ses.c:1311
- sys/bus/cam/scsi/scsi_ses.c:1314
- sys/bus/cam/scsi/scsi_ses.c:1316
- sys/bus/cam/scsi/scsi_ses.c:1321
- sys/bus/cam/scsi/scsi_ses.c:1330
- sys/bus/cam/scsi/scsi_ses.c:1334
- sys/bus/cam/scsi/scsi_ses.c:1359
- sys/bus/cam/scsi/scsi_ses.c:1363
- sys/bus/cam/scsi/scsi_ses.c:1366
- sys/bus/cam/scsi/scsi_ses.c:1086
- sys/bus/cam/scsi/scsi_ses.c:695
Detail
Evidence (decisive lines)
['findings/poc/DF-2613/VERDICT.md β line-precise walker trace incl. allocation size, resid semantics, and why the byte cannot propagate', 'findings/poc/DF-2613/env.txt β guest inventory proving the missing-setup blocker', 'findings/poc/DF-2613/fix.diff β off+3>amt -> off+4>amt in all three walkers (6 predicates), dry-run verified']
PoC changes
verify mode, no seed: no PoC was runnable β an availability-check script (check_env.sh) documents the blocker
Verified recommended fix
change the six walker predicates from 'off + 3 > amt' to 'off + 4 > amt' in ses_enchdr/ses_encdesc/ses_getthdr (scsi_ses.c:1312,1316,1330,1334,1359,1363)
Verdict
CODE-CONFIRMED, LIVE-UNREACHABLE (missing_setup): the off-by-one is certain from source β the three config-page walkers guard buffer[off+3] reads with 'if (off + 3 > amt)' (ses_enchdr scsi_ses.c:1312/1316, ses_encdesc :1330/1334, ses_getthdr :1359/1363), which admits off+3 == amt and then reads buffer[amt] at :1314/:1321, :1332/:1337, :1366. In ses_getconfig the buffer is an exact kmalloc(SCSZ=8192) (:1086, SES_MALLOC=:127, SCSZ=:937) and amt = SCSZ - resid can equal 8192 when the device transfers the full page (ses_runcmd sets *dlenp = ccb->csio.resid at :695; :1090-1096), so a crafted VEnclen chain landing on off == 8189 reads sdata[8192], one byte past the heap allocation, at first open of the root-gated /dev/sesN (sesopen :428-433 -> softc_init -> ses_getconfig :988). The mis-read byte only feeds a skip length that is re-bounded by the following checks (:1339, :1342-1345, :1367) and never reaches userspace β defense-in-depth, exactly as filed. Live reproduction requires an SES target returning a maximal crafted Configuration Page: the QEMU guest has no SES device (camcontrol devlist shows only a QEMU DVD-ROM, /dev/sesN absent) and none can be attached, so no runtime observation (a guard-page/KMALLOC_GUARD kernel would additionally be needed to observe the touch). The 6-predicate bounds fix is mechanical and verified to apply.
No comments yet.