# DF-4958 VERDICT — led_parse() sbuf leak on empty spec

**Status: REPRODUCED (live on guest, both trigger paths, causality
control included). Fix VALIDATED live (module-level).**

## Root cause (source trace)

- `led_write` (sys/dev/misc/led/led.c:222-253) and `led_set`
  (led.c:255-278) both call `led_parse(s, &sb, &state)`.
- `led_parse` (led.c:114-208) allocates `*sb = sbuf_new_auto()`
  (led.c:128). For spec strings starting with 's', it appends only chars
  passing the filter at led.c:169-174. For `"s"` alone or `"s" + chars
  the filter drops` (e.g. `"sz"`, since 'z' > 'u' fails every filter
  clause), nothing is appended.
- led.c:202-206:

```c
	error = sbuf_finish(*sb);
	if (error != 0 || sbuf_len(*sb) == 0) {
		*sb = NULL;		/* <-- sbuf deleted NOWHERE: leak */
		return (error);		/* error == 0 for the empty case */
	}
```

  The caller's cleanup `if (sb != NULL) sbuf_delete(sb)` (led.c:250-251,
  275-276) now sees NULL, and the sbuf allocated at led.c:128 —
  `sizeof(struct sbuf)` (48 B) plus the initial 16-byte auto-extend
  buffer (subr_sbuf.c:53-54 SBMALLOC/M_SBUF; subr_sbuf.c:79
  SBUF_MINEXTENDSIZE=16; subr_sbuf.c:137) — is orphaned: **64 bytes of
  M_SBUF leaked per call, two kmallocs, zero frees**.
- The `default:` case of the same switch DOES `sbuf_delete(*sb)`
  (led.c:198-200), confirming the omission at 202-206 is an oversight.
- Both leaks (empty-spec and sbuf_finish-error) are one root cause: the
  `*sb = NULL` branch frees nothing.
- Inherited verbatim from FreeBSD's dev/led/led.c (same branch, no
  sbuf_delete as of FreeBSD head today) — equivalent upstream, worth an
  upstream report too.

## Threat model / reachability

- `/dev/led/<name>` nodes are created 0600 root:wheel (led.c:305-306);
  in-tree creators: acpi_asus(4) (bled/dled/gled/mled/tled/wled),
  acpi_thinkpad(4) (led/thinklight). On such hardware any root process
  can loop `write(fd, "sz", 2)` → unbounded kernel heap exhaustion
  (memory-pressure DoS; kmem is finite).
- Kernel-side: the exported `led_set(name, cmd)` API (led.h) hits the
  identical path — any kernel/module caller passing a spec that parses
  empty (e.g. `"s"`) leaks. (No in-tree led_set callers today, so the
  write path is the realistic trigger; severity stays Low because it
  requires root.)

## Verification on the guest (DragonFly 6.5-DEVELOPMENT, cc 8.3)

`trig.ko` compiles the unmodified tree file (SRCS=led.c,
.PATH=/usr/src/sys/dev/misc/led) + a trigger module. Measured with
`vmstat -m | grep -w sbuf` (columns: InUse, MemUse):

| step | InUse | MemUse |
|---|---|---|
| baseline | 0 | 0 |
| +20000 × `write("/dev/led/leaktest", "sz", 2)` (cdev path, stock) | 39.1K | 1.53M |
| +20000 × `led_set("leaktest","s")` via sysctl (kernel API, stock) | 78.1K | 3.05M |
| +20000 × `write("sA")` — VALID spec control | 78.1K (flat) | 3.05M (flat) |
| swap module to trig_fixed.ko, +20000 × `write("sz")` | 78.1K (flat) | 3.05M (flat) |
| +20000 × `led_set` on fixed module | 78.1K (flat) | 3.05M (flat) |

- 20000 leaked calls × 2 allocations = ~39.1K InUse — exactly as
  predicted; MemUse 1.53M ≈ 20000 × 64 B + allocator overhead/rounding.
- The control (valid "sA" spec) allocates and frees in steady state
  (Requests counter +39.1K, InUse flat) — proves the leak is caused by
  the empty-spec branch, not by write(2) traffic itself.
- The leaked allocations remain InUse after `kldunload` (leaked is
  leaked).

## Fix

`fix.diff` (one line): `sbuf_delete(*sb);` before `*sb = NULL;` at
led.c:204 — mirrors the `default:` case at led.c:199. Validated live:
`trig_fixed.ko` = same tree led.c + this one line; both trigger paths
re-run → **zero growth** (InUse/MemUse flat). Module-level validation is
the deployment unit here: led.c ships as `optional led`
(/boot/kernel/led.ko), and the fix changes only led.c.

## Note on the interaction with the 's'-filter typo (DF-4959)

Because of the `*s <= 'u'` typo (led.c:171), control characters and
bytes ≤ 'u' PASS the filter; the leak's write-path trigger must use a
char > 'u' (e.g. "sz"). With FreeBSD-intended semantics (`== 'u'`),
`echo s > /dev/led/x` (writing "s\n") would leak too. The two bugs are
independent; both are inherited from FreeBSD.

## 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.
