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

Wrong loop bound (USB_DEV_QUIRKS_MAX vs USB_QUIRK_MAX) in USB_DEV_QUIRK_ADD/REMOVE quirk-name lookup

Field Value
ID DF-1047
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-704 Incorrect Type Conversion or Cast (logic: wrong constant used as loop bound)
File sys/bus/u4b/quirk/usb_quirk.c
Lines 883-888 (USB_DEV_QUIRK_ADD), 921-926 (USB_DEV_QUIRK_REMOVE)
Area bus/u4b/quirk (USB device quirk table)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

In USB_DEV_QUIRK_ADD and USB_DEV_QUIRK_REMOVE, the quirk-name-to-number lookup loop iterates y != USB_DEV_QUIRKS_MAX (384) instead of y != USB_QUIRK_MAX (96). Because usb_quirkstr() returns the string "UQ_UNKNOWN" for every y >= USB_QUIRK_MAX, a privileged caller supplying quirkname="UQ_UNKNOWN" matches at y == USB_QUIRK_MAX and the code then proceeds to store or remove quirk value 96 (== USB_QUIRK_MAX) in the table. There is no memory-corruption or privilege-escalation impact: the stored uint16 is in-bounds for pqe->quirks[], the value is never queried by any kernel caller (all UQ_* lookups pass values < USB_QUIRK_MAX), and it reads back harmlessly as "UQ_UNKNOWN" via USB_DEV_QUIRK_GET. It is a genuine logic bug (wrong constant) in a privileged code path and is reported only for correctness / defense-in-depth.

Root cause

usb_quirk.c:883-887 (ADD) and usb_quirk.c:921-927 (REMOVE) use for (y = 0; y != USB_DEV_QUIRKS_MAX; y++) to scan the quirk-name table, but the quirk-name enum/table only has USB_QUIRK_MAX (96) valid entries.

/* usb_quirk.c:883-887 β€” USB_DEV_QUIRK_ADD */
for (y = 0; y != USB_DEV_QUIRKS_MAX; y++) {     /* WRONG bound */
    if (strcmp(pgq->quirkname, usb_quirkstr(y)) == 0) {
        break;
    }
}
if (y == USB_DEV_QUIRKS_MAX) {                  /* never triggers via sentinel */
    return (EINVAL);
}

The subsequent not-found test (y == USB_DEV_QUIRKS_MAX) can therefore never trigger via the sentinel string, because usb_quirkstr(y) for any y in [USB_QUIRK_MAX, USB_DEV_QUIRKS_MAX) returns "UQ_UNKNOWN", so input "UQ_UNKNOWN" matches at y == USB_QUIRK_MAX and falls through both the not-found test and the if (y == UQ_NONE) test (usb_quirk.c:891, :929). The correct bound is USB_QUIRK_MAX, matching usb_strquirk() at usb_quirk.c:718.

Threat model & preconditions

  • Attacker position: Local user holding SYSCAP_NODRIVER (caps_priv_check_self at usb_quirk.c:878/:916) β€” effectively uid 0.
  • Privileges gained or impact: None. The only observable effect is that a root user can store a quirk entry whose quirk code equals USB_QUIRK_MAX (96), outside the valid UQ_* enum range. This value is inert: usb_test_quirk_by_info is only called by kernel USB drivers with valid UQ_* constants (< 96), so the bogus entry never matches; usb_quirkstr() renders it as "UQ_UNKNOWN". No memory unsafety, no info leak, no DoS.
  • Required config or capabilities: Default kernel with usb configured; root.
  • Reachability: ioctl(/dev/usb, USB_DEV_QUIRK_ADD, {.vid=0x1234, .pid=0x5678, .bcdDeviceLow=0, .bcdDeviceHigh=0xffff, .quirkname="UQ_UNKNOWN"}) returns 0 (success) instead of EINVAL.

Proof of concept

/* qadd.c β€” exercises the wrong-bound logic bug. No security payload. */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <bus/u4b/usb_freebsd.h>
#include <bus/u4b/quirk/usb_quirk.h>

int main(void) {
    int fd = open("/dev/usb", O_RDONLY);
    if (fd < 0) { perror("open /dev/usb"); return 1; }
    struct usb_quirk_add_generic pgq = {
        .vid = 0x1234, .pid = 0x5678,
        .bcdDeviceLow = 0, .bcdDeviceHigh = 0xffff,
        .mode = 0, .index = 0,
    };
    strlcpy(pgq.quirkname, "UQ_UNKNOWN", sizeof(pgq.quirkname));
    int rc = ioctl(fd, USB_DEV_QUIRK_ADD, &pgq);
    printf("USB_DEV_QUIRK_ADD returned %d (errno=%d) β€” expected EINVAL\n",
        rc, rc ? 0 : -1);
    return 0;
}

Build & run

cc -o qadd qadd.c
sudo ./qadd
# Expected: "USB_DEV_QUIRK_ADD returned 0 (errno=0) β€” expected EINVAL"
sudo usbconfig dump_device_quirks
# Expected: an entry with vid=0x1234 pid=0x5678 quirkname=UQ_UNKNOWN

No panic, no corruption. The PoC only proves the logic defect; there is no security payload.

Impact

None beyond a polluted quirk-table entry that confuses tooling (e.g. usbconfig dump_device_quirks). Cannot affect device probe/attach behavior. Filed as Info per the AGENT.md rubric ("Hardening opportunity, defense-in-depth, no demonstrated impact").

Change the loop bound and the not-found test in both USB_DEV_QUIRK_ADD and USB_DEV_QUIRK_REMOVE from USB_DEV_QUIRKS_MAX to USB_QUIRK_MAX.

--- a/sys/bus/u4b/quirk/usb_quirk.c
+++ b/sys/bus/u4b/quirk/usb_quirk.c
@@ -880,7 +880,7 @@
        }
        /* convert quirk string into numerical */
-       for (y = 0; y != USB_DEV_QUIRKS_MAX; y++) {
+       for (y = 0; y != USB_QUIRK_MAX; y++) {
            if (strcmp(pgq->quirkname, usb_quirkstr(y)) == 0) {
                break;
            }
        }
-       if (y == USB_DEV_QUIRKS_MAX) {
+       if (y == USB_QUIRK_MAX) {
            return (EINVAL);
        }
        if (y == UQ_NONE) {
@@ -918,7 +918,7 @@
        /* check privileges */
        err = caps_priv_check_self(SYSCAP_NODRIVER);
        if (err) {
            return (err);
        }
        /* convert quirk string into numerical */
-       for (y = 0; y != USB_DEV_QUIRKS_MAX; y++) {
+       for (y = 0; y != USB_QUIRK_MAX; y++) {
            if (strcmp(pgq->quirkname, usb_quirkstr(y)) == 0) {
                break;
            }
        }
-       if (y == USB_DEV_QUIRKS_MAX) {
+       if (y == USB_QUIRK_MAX) {
            return (EINVAL);
        }
        if (y == UQ_NONE) {

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1047 Β· 1 files
FileTypeDescriptionSize
fix.diff suggested-fix Wrong loop bound (USB_DEV_QUIRKS_MAX vs USB_QUIRK_MAX) in USB_DEV_QUIRK_ADD/REMO 382 B view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied + combined nativekernel build rc=0 (-Werror)

fix.diff applied + combined nativekernel build rc=0 (-Werror)
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none (Info severity)

Evidence (decisive lines)

Source-confirmed at sys/bus/u4b/quirk/usb_quirk.c:883: wrong loop bound (USB_DEV_QUIRKS_MAX vs USB_QUIRK_MAX) in quirk-name lookup

Verified recommended fix

Source-confirmed at sys/bus/u4b/quirk/usb_quirk.c:883: wrong loop bound (USB_DEV_QUIRKS_MAX vs USB_QUIRK_MAX) in quirk-name lookup

Verdict

Source-confirmed at sys/bus/u4b/quirk/usb_quirk.c:883: wrong loop bound (USB_DEV_QUIRKS_MAX vs USB_QUIRK_MAX) in quirk-name lookup