Fix: Calls to compression tools need to separate file names from options

Fixed BurntSushi/ripgrep#3222 — 2 line bug-fix.

The Bug

Repo: BurntSushi/ripgrep Issue: #3222 Status: submission-failed PR: https://github.com/BurntSushi/ripgrep/pull/3222

Description: Calls to compression tools need to separate file names from options

Fix scope: 2 lines changed in crates/cli/src/decompress.rs

When ripgrep encounters a compressed file, it does not perform decompression internally. Instead, it delegates to external tools installed on the system — gzip, bzip2, xz, lz4, brotli, zstd, and uncompress — by spawning a child process with arguments like gzip -d -c <file>. This design keeps ripgrep’s binary small and avoids linking against native compression libraries, but it introduces a subtle class of bugs: file names that look like options.

A file named --help.gz or -v.gz would be passed directly as a positional argument to the compression tool. Without a -- separator, the tool interprets it as a flag, producing wrong output or failing entirely.

Code Analysis

The core of the decompression delegation lives in crates/cli/src/decompress.rs. The DecompressionMatcher::command() method (at line 179) looks up a pre-configured std::process::Command for a given file path by matching its extension against a set of globs (e.g., *.gz maps to gzip, *.bz2 maps to bzip2). The command already carries the decompression flags (-d -c etc.) from default_decompression_commands().

The actual invocation happens in DecompressionReaderBuilder::build() (line 221). After obtaining the command via self.matcher.command(path), the builder does:

cmd.arg(path);

This appends the file path as the final argument to the child process. The problem is that cmd.arg() does not insert a -- terminator — the path is placed verbatim into the argument vector. If the path starts with -, the decompression tool will parse it as an option rather than a file name.

The default_decompression_commands() function (line 490) defines the full set of supported tools: gzip, bzip2, xz (also used for LZMA via --format=lzma), lz4, brotli, zstd, and BSD uncompress. Each entry stores the binary name and its decompression flags. The glob-to-command mapping covers .gz, .tgz, .bz2, .tbz2, .xz, .txz, .lz4, .zst, .zstd, .br, and .Z files.

Root Cause

The root cause is a missing POSIX -- separator before the file path argument at line 229 of crates/cli/src/decompress.rs. In POSIX convention, -- signals the end of option parsing — everything that follows is a positional argument regardless of whether it starts with -. Many decompression tools (for example gzip, xz, and bzip2) support this convention, but ripgrep was not utilizing it.

This edge case is easy to miss because standard test suites rarely exercise file names that collide with option syntax. A user searching files in an untrusted or automatically generated directory could encounter files with leading dashes, causing the decompression subprocess to fail silently or produce corrupted output.

Impact: For ripgrep users who rely on the -z / --search-zip flag to search compressed archives, encountering a file named with a leading dash would cause the decompression tool to reject the argument, emit an error, and produce no output for that file. Because ripgrep logs the error at debug level by default, the failure could appear as a silent skip.

The Fix

The fix is a two-line addition in DecompressionReaderBuilder::build() that inserts "--" before the file path argument:

cmd.arg("--");
cmd.arg(path);

This is a textbook application of the POSIX end-of-options idiom. The "--" argument tells every POSIX-compliant tool that subsequent arguments are file names, not options. The change is entirely additive — it does not alter any existing behavior for normal file names (those without a leading -) because -- is simply absorbed when there are no further options to consume.

Pattern & Takeaways

Pattern: Unvalidated argument injection in subprocess argument vectors — when user-controlled data (a file path) is passed to an external command without a -- separator, the file name can be misinterpreted as an option. This is a recurring class of bug in tools that delegate work to external programs.

Key insight: Any time a program constructs a subprocess command with user-influenced arguments, a -- terminator should be placed before the positional arguments. This is well understood for shell commands but is just as relevant when using std::process::Command in Rust, subprocess in Python, or exec/spawn in any language.

Transfer Potential

High — the -- separator pattern is universal. Every tool that delegates to external programs is susceptible to this bug. Examples include:

  • Code formatters that invoke language-specific linters
  • Build systems that call compilers with generated file names
  • File managers that open documents with associated applications
  • Archive tools that pass member names to decompressors

The same two-character fix — inserting "--" before the variable argument — applies in every case.


Auto-generated from PR #3222. View all patches on GitHub.

References

[1] BurntSushi/ripgrep [2] #3222 [3] https://github.com/BurntSushi/ripgrep/pull/3222