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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2613 Β· 5 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-2613

Confirmed kernel references

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.