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

high

How buffer overflow via insecure strcpy/strncpy happens in C textbox widgets and how to fix it

A high-severity buffer overflow vulnerability was discovered in the Aroma UI framework's textbox widget where `strncpy()` was used to copy user-provided text without guaranteed null-termination safety. The fix replaces the dangerous `strncpy()` pattern with `snprintf()`, which automatically handles buffer boundaries and null-termination in a single, safer operation.

critical

How buffer overflow via sprintf happens in C++ fuzzer code and how to fix it

A critical buffer overflow vulnerability was discovered in `prog/fuzzing/recog_basic_fuzzer.cc` where `sprintf` writes to a fixed 256-byte buffer without bounds checking. An attacker providing crafted fuzzer input could exploit this to corrupt memory. The fix replaces `sprintf` with `snprintf`, enforcing the buffer size limit and preventing overflow.

critical

How buffer overflow in memcpy happens in C bios_disk.h and how to fix it

A critical buffer overflow vulnerability was discovered in `include/bios_disk.h` at line 474, where a `memcpy` operation copies 512 bytes from a source buffer without properly validating that the calculated offset from the `sectnum` parameter stays within bounds. An attacker controlling the `sectnum` parameter could trigger an out-of-bounds read, potentially leaking sensitive memory contents or causing a crash. The fix adds a proper bounds check before the memcpy call to ensure the source offset

critical

How buffer overflow happens in C RTSPSession.h and how to fix it

A critical buffer overflow vulnerability in `src/AudioTools/Communication/RTSP/RTSPSession.h` allowed an attacker to send a crafted RTSP request with an oversized payload, triggering a heap overflow via an unchecked `memcpy()` call at line 408. The fix adds a single bounds check before the copy and replaces several unsafe `strcpy`/`strncpy` calls with `snprintf`, closing multiple paths to memory corruption and potential remote code execution.

critical

How buffer overflow happens in C sprintf/strcpy and how to fix it

A critical buffer overflow vulnerability was discovered in `mupen64plus-rsp-cxd4/module.c`, where unsafe `sprintf()` and `strcpy()` calls at lines 294–298 could be exploited by a crafted N64 ROM file to corrupt memory and achieve code execution. The fix replaces these unbounded string functions with `snprintf()`, which enforces strict buffer size limits and eliminates the overflow risk. This is a textbook example of how legacy C string functions can silently introduce critical security flaws in

medium

How integer overflow in tensor shape validation happens in C++ with OpenVINO and how to fix it

A medium-severity integer overflow vulnerability was discovered in the OpenVINO noise suppression plugin where model input tensor shapes were loaded without dimension validation. An attacker could supply a crafted `.xml/.bin` model file with extremely large or zero-sized dimensions, causing integer overflow during memory allocation or zero-size allocations followed by out-of-bounds writes. The fix introduces a `NS_MAX_SHAPE_DIM` constant that validates each dimension against a safe upper bound b