Back to Blog
medium SEVERITY9 min read

How buffer overflow happens in C kernel driver (qcom_usbnet_main.c) and how to fix it

A series of unsafe `sprintf()` calls in the Qualcomm USB network kernel driver (`qcom_usbnet_main.c`) created buffer overflow conditions that, when combined with other memory corruption primitives in the same file, could allow an attacker with physical USB access to escalate privileges to root. The fix replaces unbounded `sprintf()` and `snprintf()` misuse with properly bounded `snprintf()` and `scnprintf()` calls that respect actual buffer sizes. This is a textbook example of how a seemingly mi

O
By Orbis AppSec
Published June 22, 2026Reviewed June 22, 2026

Answer Summary

This vulnerability (CWE-120, buffer overflow) exists in the Qualcomm USB network Linux kernel driver (`qcom_usbnet_main.c`) where multiple `sprintf()` calls write formatted output into fixed-size buffers without enforcing size limits. In kernel mode, such overflows can corrupt adjacent memory structures, and when chained with other bugs in the same file, can enable privilege escalation to root. The fix replaces every offending `sprintf()` with `snprintf()` (for general buffers) or `scnprintf()` (for sysfs/procfs output buffers), passing the actual buffer size as the second argument to prevent any write beyond the allocated region.

Vulnerability at a Glance

cweCWE-120
fixReplace sprintf() with snprintf(buf, sizeof(buf), ...) and sprintf() in sysfs/procfs handlers with scnprintf(buf, PAGE_SIZE, ...)
riskKernel memory corruption leading to privilege escalation via physical USB access
languageC (Linux Kernel Driver)
root causesprintf() and misused snprintf() calls write formatted strings into fixed-size buffers without enforcing the buffer's actual size limit
vulnerabilityBuffer Overflow via unbounded sprintf()

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:

  1. Use the sprintf() overflow to corrupt a heap object adjacent to commonDevName.
  2. Use the use-after-free to obtain a dangling pointer to a controlled region.
  3. Spray the kernel heap to place a fake object with a controlled function pointer at the target address.
  4. 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 at size - 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() in GobiAcquireDevice() wrote attacker-controlled USB descriptor data (devpath) into a 255-byte buffer with no size check — the manual commonDevName[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, not snprintf(), 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) at qcom_usbnet_main.c:163, and three additional sprintf() calls at lines 2481, 2576, and 3129
  • Missing control: No size argument was passed to sprintf(), and the post-write null termination at commonDevName[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 with snprintf(buf, sizeof(buf), ...) for the device name buffer and scnprintf(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.


References

Frequently Asked Questions

What is a buffer overflow in a kernel driver?

A buffer overflow occurs when a write operation puts more data into a fixed-size memory region than it can hold, overwriting adjacent memory. In kernel drivers, adjacent memory often contains critical data structures like function pointers or security tokens, making the impact far more severe than in userspace code.

How do you prevent buffer overflows with sprintf() in C kernel code?

Replace sprintf() with snprintf() for general buffers, passing sizeof(buffer) as the size argument. For sysfs and procfs output handlers that receive a kernel-managed PAGE_SIZE buffer, use scnprintf(buf, PAGE_SIZE, ...) instead, which also returns the number of bytes actually written rather than what would have been written.

What CWE is a sprintf buffer overflow?

CWE-120, "Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')". Related identifiers include CWE-787 (Out-of-bounds Write) and CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer).

Is manually null-terminating the buffer (e.g., buf[254] = '\0') enough to prevent a buffer overflow?

No. The original code did exactly this — it called sprintf() and then set commonDevName[254] = '\0'. But sprintf() had already written past the end of the buffer before the null termination, corrupting adjacent memory. You must constrain the write at the point of the sprintf() call itself using snprintf().

Can static analysis detect sprintf buffer overflows in kernel drivers?

Yes. Tools like Semgrep, Coccinelle (used by the Linux kernel community), Coverity, and clang's static analyzer can flag unbounded sprintf() calls. The Linux kernel's own checkpatch.pl script warns about sprintf() usage in certain contexts.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #42

Related Articles

critical

How buffer overflow happens in C tar header parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `microtar/microtar.c` where the `raw_to_header()` and `header_to_raw()` functions used unbounded `strcpy()` and `sprintf()` calls to copy tar header fields. Malicious tar files with non-null-terminated name fields could overflow destination buffers, potentially leading to code execution. The fix replaces all unsafe string operations with bounded alternatives: `memcpy()` with explicit null-termination and `snprintf()` instead of `sprintf(

critical

How buffer overflow happens in C ieee80211_input() and how to fix it

A critical buffer overflow vulnerability was discovered in `src/firmware/src/net/ieee80211.c` at line 1584, where the `ieee80211_input()` function processed raw 802.11 data frames without verifying that the incoming frame was large enough to contain a valid `ieee80211_frame` header. An attacker within wireless range could craft undersized or malformed frames to trigger memory corruption, potentially leading to remote code execution on the firmware. The fix adds a single, targeted bounds check th

high

How buffer overflow from unsafe string copy functions happens in C network interface code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `generic/eth-impl.c`, where unsafe `strncpy()` and `sprintf()` calls could write beyond buffer boundaries when handling network interface names and device filenames. The fix replaced these dangerous functions with bounded `snprintf()` calls that guarantee null-termination and prevent memory corruption.

critical

How buffer overflow in FuzzIxml.c sprintf() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `fuzzer/FuzzIxml.c` where `sprintf()` wrote a PID-formatted filename into a fixed 256-byte stack buffer without any bounds checking. The fix replaces `sprintf()` with `snprintf()`, explicitly passing the buffer size to prevent any overflow. While exploitation in this specific fuzzer context requires local access, the pattern is a textbook example of CWE-120 that developers should recognize and eliminate everywhere it appears.

critical

How buffer overflow happens in C HTML parsing and how to fix it

A critical buffer overflow vulnerability in `include/html_parse.h` allowed attackers to overflow buffers by providing malicious HTML input exceeding buffer capacity. The fix adds proper bounds checking before memcpy() operations to prevent memory corruption and potential code execution.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.