How Buffer Overflow Happens in C Kernel Driver (qcom_usbnet_main.c) and How to Fix It
Introduction
The qcom_usbnet_main.c file is the heart of the Qualcomm USB network kernel driver — it handles device acquisition, sysfs attribute exposure, and procfs diagnostics for Qualcomm USB modems on Linux. Buried inside this driver, across four separate call sites, a deceptively simple function — sprintf() — was writing formatted strings into fixed-size buffers with no regard for how large those buffers actually were.
In userspace, a sprintf() overflow might crash a process. In a kernel driver, the same mistake can corrupt the kernel heap, overwrite function pointers, and hand an attacker a direct path to root.
This post walks through exactly what was wrong, why it mattered in this specific context, and how the fix closes the vulnerability.
The Vulnerability Explained
Unbounded Writes into Fixed-Size Kernel Buffers
The most structurally dangerous instance of the vulnerability was in GobiAcquireDevice() at line 163:
// VULNERABLE — before the fix
sprintf(commonDevName, "%s:%d-%s", mpKey, pDev->udev->bus->busnum, pDev->udev->devpath);
commonDevName[254] = '\0';
commonDevName is a fixed-size character array (255 bytes, as implied by the [254] index). The code's author clearly knew there was a size concern — hence the manual null-termination at index 254. But this is a critical misunderstanding of how sprintf() works: the overflow has already happened by the time you set the null byte.
sprintf() does not know the size of commonDevName. It will write as many bytes as the format string produces, regardless of the buffer boundary. The three format arguments — mpKey (a caller-controlled string), pDev->udev->bus->busnum (an integer), and pDev->udev->devpath (a path string from the USB device descriptor) — can together easily exceed 255 bytes when a malicious USB device is connected.
The other three vulnerable call sites appear in sysfs and procfs output handlers:
// VULNERABLE — debug_show() at line 2481
return sprintf(buf, "%04x\n", qmi_sys);
// VULNERABLE — gobiQMITimer_show() at line 2576
return sprintf(buf, "%llu\n", ctx->timer_interval / (u32)NSEC_PER_MSEC);
// VULNERABLE — GobiUSBprocRead() at line 3129
len += sprintf(buf, "%d %ld ms\n", debug_g, gtimer/NSEC_PER_USEC);
In sysfs show callbacks and procfs read handlers, the kernel passes a buf pointer backed by exactly one page (PAGE_SIZE, typically 4096 bytes). The sprintf() return value is supposed to tell the kernel how many bytes were written. If sprintf() overflows the page, it writes into the next kernel page — which could be anything.
Why This Is Exploitable via USB
The vulnerability description is specific: an attacker with physical USB access can connect a programmable USB device (a Facedancer board, for instance) that presents crafted USB descriptors. The devpath field written into commonDevName in GobiAcquireDevice() comes directly from the USB device's descriptor data — it is attacker-controlled.
By crafting a devpath value long enough to overflow commonDevName, an attacker can overwrite whatever sits adjacent to that buffer on the kernel heap. In a driver that also contains use-after-free and integer underflow bugs (V-003, V-005 in the same file), these primitives can be chained:
- Use the
sprintf()overflow to corrupt a heap object adjacent tocommonDevName. - Use the use-after-free to obtain a dangling pointer to a controlled region.
- Spray the kernel heap to place a fake object with a controlled function pointer at the target address.
- Trigger the function pointer call → arbitrary kernel code execution → root.
This is not a theoretical chain. Kernel heap grooming via USB descriptor manipulation is a well-documented technique, and the Qualcomm USB driver is loaded automatically on many Linux distributions when a matching device is inserted.
The Fix
The fix is precise and minimal: every sprintf() call is replaced with a size-bounded equivalent, and the redundant manual null-termination is removed.
Fix 1: GobiAcquireDevice() — Line 163
// BEFORE
sprintf(commonDevName, "%s:%d-%s", mpKey, pDev->udev->bus->busnum, pDev->udev->devpath);
commonDevName[254] = '\0';
// AFTER
snprintf(commonDevName, sizeof(commonDevName), "%s:%d-%s", mpKey, pDev->udev->bus->busnum, pDev->udev->devpath);
snprintf() takes the buffer size as its second argument and guarantees it will never write more than sizeof(commonDevName) - 1 bytes, always null-terminating within the buffer. The manual [254] = '\0' line is removed — it was both redundant (snprintf null-terminates) and misleading (it implied the preceding sprintf was safe).
Using sizeof(commonDevName) rather than a hardcoded 255 is important: if the array is ever resized, the bound automatically tracks the new size.
Fix 2: debug_show() — Line 2481
// BEFORE
return sprintf(buf, "%04x\n", qmi_sys);
// AFTER
return scnprintf(buf, PAGE_SIZE, "%04x\n", qmi_sys);
Fix 3: gobiQMITimer_show() — Line 2576
// BEFORE
return sprintf(buf, "%llu\n", ctx->timer_interval / (u32)NSEC_PER_MSEC);
// AFTER
return scnprintf(buf, PAGE_SIZE, "%llu\n", ctx->timer_interval / (u32)NSEC_PER_MSEC);
Fix 4: GobiUSBprocRead() — Line 3129
// BEFORE
len += sprintf(buf, "%d %ld ms\n", debug_g, gtimer/NSEC_PER_USEC);
// AFTER
len += scnprintf(buf, BUFSIZE, "%d %ld ms\n", debug_g, gtimer/NSEC_PER_USEC);
For the sysfs and procfs handlers (fixes 2, 3, and 4), the fix uses scnprintf() rather than snprintf(). This is the idiomatic Linux kernel choice for these contexts for a subtle but important reason:
snprintf()returns the number of bytes that would have been written if the buffer were large enough (even if truncation occurred).scnprintf()returns the number of bytes actually written (capped atsize - 1).
Sysfs show callbacks return this value to the VFS layer to indicate how many bytes to send to userspace. If snprintf() returns a value larger than PAGE_SIZE due to truncation, the kernel would attempt to copy more bytes than actually exist in the buffer — a second-order bug. scnprintf() closes this gap.
Prevention & Best Practices
1. Ban sprintf() in Kernel Code
The Linux kernel's own coding style documentation discourages sprintf() in favor of snprintf() and scnprintf(). Consider adding a Coccinelle semantic patch to your CI pipeline to flag any new sprintf() introduction:
// Coccinelle rule to flag sprintf in kernel drivers
@@
expression buf, fmt;
expression list args;
@@
- sprintf(buf, fmt, args)
+ snprintf(buf, sizeof(buf), fmt, args)
2. Use sizeof(), Not Magic Numbers
The original code used commonDevName[254] = '\0', which hardcodes the buffer size. When the fix uses sizeof(commonDevName), the bound is automatically correct even if the array declaration changes. Never hardcode buffer sizes in format function calls.
3. Choose the Right Bounded Function for the Context
| Context | Use | Why |
|---|---|---|
| General kernel buffers | snprintf(buf, sizeof(buf), ...) |
Standard bounded write |
sysfs show callbacks |
scnprintf(buf, PAGE_SIZE, ...) |
Returns actual bytes written |
procfs read handlers |
scnprintf(buf, BUFSIZE, ...) |
Same — avoids over-reporting |
| seq_file handlers | seq_printf(m, ...) |
Handles buffering automatically |
4. Treat USB Descriptor Data as Attacker-Controlled
Any field sourced from a USB device — devpath, manufacturer, product, serial — must be treated as hostile input. Apply the same scrutiny you would to network input: bound all copies, validate lengths before use, and never assume the kernel's USB subsystem has already sanitized the data for your driver's specific use.
5. Run Kernel-Specific Static Analysis
- Coccinelle (
scripts/coccinelle/in the kernel tree): semantic patch engine designed for kernel C patterns - Sparse (
make C=2): the kernel's own type-checker, catches some buffer issues - clang
--analyze: good at tracking buffer sizes through assignments - Semgrep with C rules: https://semgrep.dev/r?q=sprintf
6. CWE and OWASP Alignment
This vulnerability maps to:
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-787: Out-of-bounds Write
- OWASP A05:2021 – Security Misconfiguration (insufficient input handling in infrastructure components)
Key Takeaways
sprintf()inGobiAcquireDevice()wrote attacker-controlled USB descriptor data (devpath) into a 255-byte buffer with no size check — the manualcommonDevName[254] = '\0'that followed did not undo the overflow that had already occurred.- The post-write null termination pattern is a false safety net — it signals awareness of the buffer boundary but does nothing to prevent the overwrite that precedes it.
scnprintf()is the correct choice for sysfs and procfs handlers in Linux kernel drivers, notsnprintf(), because it returns actual bytes written rather than bytes that would have been written, preventing a second-order VFS miscommunication.- USB descriptor fields are attacker-controlled input — any driver that formats them into fixed buffers must treat them with the same hostility as network packets.
- In kernel code, buffer overflows are not just crashes — adjacent heap objects are kernel data structures, and overwriting them is a direct path to privilege escalation.
How Orbis AppSec Detected This
- Source: USB device descriptor fields (
pDev->udev->devpath,pDev->udev->bus->busnum) provided by a physical USB device, plus kernel internal state variables (qmi_sys,ctx->timer_interval,debug_g,gtimer) exposed through sysfs/procfs - Sink:
sprintf(commonDevName, "%s:%d-%s", mpKey, pDev->udev->bus->busnum, pDev->udev->devpath)atqcom_usbnet_main.c:163, and three additionalsprintf()calls at lines 2481, 2576, and 3129 - Missing control: No size argument was passed to
sprintf(), and the post-write null termination atcommonDevName[254] = '\0'did not prevent the preceding out-of-bounds write - CWE: CWE-120 — Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
- Fix: Replaced all four
sprintf()calls withsnprintf(buf, sizeof(buf), ...)for the device name buffer andscnprintf(buf, PAGE_SIZE/BUFSIZE, ...)for the sysfs and procfs output handlers
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The sprintf() buffer overflows in qcom_usbnet_main.c are a reminder that some of the oldest vulnerability classes in C remain dangerous precisely because the unsafe functions are so convenient to use. Four lines of code — each a one-character-difference fix — separated a production kernel driver from a physical-access privilege escalation chain.
The lesson is not just "use snprintf()." It's that kernel driver code handling external device data must treat every byte from that device as potentially hostile, and that post-write bounds enforcement (like the [254] = '\0' pattern) creates a false sense of safety that can survive code review for years.
Automated tools that understand data flow from USB descriptors through kernel buffer operations — rather than just pattern-matching on function names — are essential for catching these issues before they ship.