Back to Blog
critical SEVERITY5 min read

How buffer overflow happens in C xxd utility and how to fix it

A critical buffer overflow vulnerability was discovered in the xxd utility's `xxdline()` function where `strcpy()` was used without bounds checking on file input. An attacker could craft a malicious hex dump file with oversized lines to trigger memory corruption. The fix replaces the unsafe `strcpy()` with `snprintf()` to enforce buffer size limits.

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

Answer Summary

This is a buffer overflow vulnerability (CWE-120) in C code within the xxd hex dump utility. The vulnerable `strcpy(z, l)` call at line 576 of `xxd.c` copied user-controlled file input without checking buffer bounds. The fix replaces `strcpy()` with `snprintf(z, sizeof(z), "%s", l)` to enforce the destination buffer's size limit, preventing memory corruption from oversized input lines.

Vulnerability at a Glance

cweCWE-120
fixReplace strcpy() with snprintf() using sizeof(z) as the size limit
riskRemote code execution or denial of service via crafted input file
languageC
root causestrcpy() used without bounds checking on file-derived input
vulnerabilityBuffer Overflow (Classic)

Introduction

In the xxd hex dump utility, we discovered a critical buffer overflow vulnerability in src/xxd/xxd.c at line 576. The xxdline() function, which processes hex dump file input for the revert mode (xxd -r), used strcpy(z, l) to copy line data without any bounds checking. Since the source buffer l is populated directly from user-provided file input, an attacker could craft a hex dump file with lines exceeding the destination buffer z's size, triggering a stack buffer overflow.

This vulnerability is particularly dangerous because xxd is a command-line utility that processes arbitrary user-provided files, making the attack vector directly accessible to anyone who can supply input to the tool.

The Vulnerability Explained

The vulnerable code resided in the xxdline() function, which handles line-by-line processing of hex dump files during revert operations. Here's the problematic code:

static void xxdline(FILE *fp, char *l, char *colors, int nz)
{
  static signed char zero_seen = 0;

  if (!nz && zero_seen == 1) {
    strcpy(z, l);  // VULNERABLE: No bounds checking!
    if (colors) {
      memcpy(z_colors, colors, strlen(z));
    }

The issue is straightforward but severe:

  1. l is file-derived input: The buffer l contains data read from a hex dump file provided by the user
  2. z is a fixed-size buffer: The destination buffer z has a predetermined size
  3. strcpy() has no length limit: This function copies bytes until it encounters a null terminator, regardless of the destination buffer's capacity

Attack Scenario

An attacker could exploit this vulnerability with these steps:

  1. Create a malicious hex dump file with a line containing more than 256 bytes of hex characters
  2. Run xxd -r malicious_file.hex > output.bin
  3. When xxdline() processes the oversized line, strcpy(z, l) writes beyond z's boundary
  4. This corrupts the stack, potentially overwriting the return address
  5. With a carefully crafted payload, the attacker achieves arbitrary code execution

A simple proof-of-concept payload would be a hex dump file containing a single line with 512+ hex characters:

444444444444444444444444444444444444444444444444... (512+ '4' characters)

The regression test in the PR demonstrates this exact attack vector:

/* Generate oversized hex payloads */
char payload_256[513];
memset(payload_256, '4', 512);
payload_256[512] = '\0';

The Fix

The fix is elegant and follows C security best practices—replacing the unbounded strcpy() with the bounds-checked snprintf():

Before (Vulnerable)

strcpy(z, l);

After (Fixed)

snprintf(z, sizeof(z), "%s", l);

This change provides several security guarantees:

  1. sizeof(z) enforces the buffer limit: The second argument explicitly tells snprintf() the maximum number of bytes to write
  2. Automatic null-termination: Unlike strncpy(), snprintf() always null-terminates the output (if size > 0)
  3. Truncation over corruption: If l exceeds the buffer size, the data is truncated rather than overflowing

The PR also added a functional test to prevent regression:

it('handles long lines in revert mode', function()
  t.skip(t.is_arch('s390x'), 'FIXME: xxd not built correctly on s390x with QEMU?')
  local long_line = ('4'):rep(512) .. '\n'
  fn.system({ testprg('xxd'), '-r' }, long_line)
  eq(0, eval('v:shell_error'))
end)

This test ensures xxd gracefully handles 512-character lines without crashing—a direct verification that the buffer overflow is mitigated.

Note on Additional Vulnerable Locations

The PR notes that line 1115 in the same file uses a similar pattern and may need review. This highlights an important principle: when fixing one instance of an unsafe pattern, always search for similar patterns throughout the codebase.

Key Takeaways

  • Never use strcpy() with file-derived input: The xxdline() function processed user-provided hex dump files, making strcpy(z, l) a direct attack vector
  • snprintf() is the safe replacement for strcpy() in C: It enforces bounds and always null-terminates
  • Command-line utilities processing user files are high-risk: xxd's -r mode reads arbitrary files, requiring defensive coding throughout
  • Search for pattern siblings: The PR notes line 1115 may have the same issue—one vulnerability often indicates more
  • Regression tests prevent security fixes from being undone: The Lua test with 512-character input ensures this specific attack vector stays closed

How Orbis AppSec Detected This

  • Source: File input processed by the xxd utility's revert mode (xxd -r), specifically the line buffer l populated from user-provided hex dump files
  • Sink: strcpy(z, l) at src/xxd/xxd.c:576 in the xxdline() function
  • Missing control: No bounds checking between the file-derived input length and the destination buffer z's capacity
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Replaced strcpy(z, l) with snprintf(z, sizeof(z), "%s", l) to enforce the buffer size limit

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

Buffer overflows remain one of the most dangerous vulnerability classes in C programming, and this xxd vulnerability demonstrates why. A single strcpy() call without bounds checking created a critical security flaw that could be exploited through a crafted input file. The fix—replacing strcpy() with snprintf()—is simple but essential.

When working with C code that processes external input, always assume the input is malicious. Use bounds-checked functions, enable compiler protections, and implement regression tests for security fixes. The few extra characters of code for snprintf(z, sizeof(z), "%s", l) versus strcpy(z, l) are the difference between secure software and a critical vulnerability.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #40236

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.