April 9, 2024·6 min read·engineeringrustsecuritywindowsprocesses

Rust fixes argument escaping for Windows batch files

CVE-2024-24576 allowed untrusted arguments to escape into batch-file command syntax. Rust 1.77.2 strengthens encoding and rejects inputs it cannot safely represent.

Passing separate arguments to a process API is supposed to avoid shell-command construction. On Windows, a batch file makes that separation harder to maintain because cmd.exe must interpret a command-line string. Rust's standard library attempted the necessary escaping, but its rules were incomplete. The April 9 security advisory for CVE-2024-24576 describes how an attacker controlling an argument could execute arbitrary shell commands.

Rust 1.77.2, published the same day, contains the fix. The affected case is Windows batch files with .bat or .cmd extensions invoked with untrusted arguments through Command. The advisory explicitly excludes other platforms and other uses. That scope is necessary to understand the vulnerability's severity without implying that every process launch is affected.

I find Rust attractive for systems code because it makes many dangerous operations explicit. This case concerns a safe standard-library abstraction whose implementation crossed an operating-system parsing boundary. Memory safety does not by itself establish that an argument reaches a command interpreter as data.

Windows starts from a command-line string

The advisory explains that Windows process creation supplies a single string containing the arguments. The child process is responsible for splitting that string. Most programs follow the C runtime's conventions, which give the standard library a reasonably consistent target for encoding argument boundaries.

cmd.exe uses different parsing rules. Batch-file execution therefore needs custom handling. A string encoded for an ordinary executable's argument parser cannot simply be assumed to preserve the same arguments through the command interpreter.

This makes the abstraction more demanding than storing each .arg call in a list. The implementation eventually serializes that list into a string that another program interprets. To preserve an argument boundary, its encoding must match the recipient's grammar. To preserve an untrusted argument as data, it must also prevent that grammar from turning part of the value into a command.

The 1.77.2 Command documentation distinguishes ordinary literal arguments from applications with nonstandard decoding. Its Windows warning names cmd.exe and batch files. That documentation puts the platform distinction beside the API that callers use, where the otherwise reasonable expectation of literal argument passing begins.

The vulnerability is not that an application intentionally concatenated user input into a shell command. The affected caller could use Command::arg or Command::args and rely on the standard library to maintain the promised argument semantics. Incomplete batch escaping broke that expectation.

The fix includes refusal, not only more quoting

The security working group says it could not identify an escaping solution correct for every possible input because of cmd.exe's complexity. Rust 1.77.2 therefore strengthens the escaping it can perform and returns InvalidInput when it cannot safely escape an argument. The error occurs when spawning the process.

That is an API-level consequence. Building a Command and adding arguments does not guarantee that spawning it will succeed. A newly rejected argument can now produce an error before the child starts. This is the intended result when the implementation cannot preserve its argument contract.

Returning an error is different from removing suspicious characters or changing their meaning. A library silently rewriting input would leave the application unsure which argument the child received. Refusal preserves a clear distinction between successful encoding and an input the implementation cannot represent safely.

It also avoids promising that a larger collection of quoting rules solves every interpreter behavior. The working group's description is explicit about the limit. The implementation can reject inputs beyond that limit while retaining ordinary behavior for arguments it knows how to encode.

The patch treats the batch grammar separately

The Windows argument implementation at the release tag separates batch argument construction from ordinary argument construction. Its batch builder starts cmd.exe with command extensions enabled and delayed expansion disabled, and constructs an extra pair of quotes around the command.

The comments explain that command extensions are required for the percent-sign encoding used by the fix. That matters because an escaping scheme can depend on interpreter configuration. Choosing the quoting rules without fixing the relevant interpreter mode would leave another source of variation.

The builder validates the script name separately from its arguments. It rejects a name containing a double quote or ending with a backslash. The argument path rejects carriage returns and newlines because those characters can truncate arguments. Those checks return InvalidInput instead of attempting to continue with an altered command.

Batch argument quoting also accounts for an empty argument. Without quotes, an empty string can disappear when the recipient parses the command line. Preserving an empty argument is part of the normal argument-list contract, even though it is not itself an attack.

The code comments identify another edge case involving a trailing backslash. A batch script may force quotes around an expanded argument, and a trailing slash can then interact with the closing quote. The encoder treats that case explicitly. The correct representation depends on how batch processing will interpret the composed text.

Environment expansion survives naive quoting assumptions

The percent-sign handling is a particularly revealing part of the patch. The comments describe inserting an expression using an empty substring of the interpreter's current-directory variable. Its purpose is to prevent an argument's percent-delimited text from being expanded as an environment variable.

The details show why a general instruction to surround an argument with quotes is insufficient. Command interpreters can perform substitutions inside text that appears quoted. Argument separation and suppression of expansion are distinct properties, and the implementation needs both for untrusted data.

The batch encoder also doubles internal double quotes and handles preceding backslashes. Its ordinary executable encoder has different rules for these characters. Keeping the implementations separate makes the downstream parser distinction visible in the code instead of assuming one universal Windows quoting convention.

This is a useful mechanism to understand even without copying the escaping implementation into application code. The standard library is maintaining a translation between two interfaces. Its source side is a list of literal arguments with boundaries already chosen by the caller. The destination is a command language whose parsing and expansion rules can reinterpret characters within those arguments.

I prefer leaving that translation inside a maintained platform implementation. A local quoting helper would inherit the same grammar obligations while losing the coordinated review and update path of the standard library. Applications still need to know when their target is a batch interpreter, because that choice determines which contract applies.

Raw arguments transfer the encoding responsibility

The advisory mentions Windows CommandExt::raw_arg for callers that implement their own escaping or handle only trusted inputs. The patch's source confirms that raw arguments follow a different path and remain the caller's responsibility.

That is not an equivalent security fix for a program forwarding untrusted arguments. Bypassing the library's encoding also bypasses the protection whose failure prompted the advisory. A caller choosing raw arguments must establish the appropriate encoding contract itself.

The distinction matters when responding to a new InvalidInput error. Automatically retrying the same value through the raw interface would remove the rejection without showing that the interpreter can receive it safely. The new error communicates that the ordinary encoder did not establish that property.

The advisory also includes dependency behavior in its scope. An application's own source may not invoke a batch file, while a dependency does so with input derived from a request or other external source. The relevant path is the actual process invocation and the provenance of its arguments, rather than the location of the .arg call in the dependency tree.

The corrected contract is observable at spawn

All Rust versions before 1.77.2 are affected on Windows for the disclosed batch-file case. The patch changes the standard-library implementation, so the correction concerns programs built with that fixed implementation. The release announcement identifies the security change directly instead of describing it as a general Windows behavior update.

The resulting behavior is explicit. An argument is encoded using strengthened batch rules or spawning returns InvalidInput. The caller can handle that error as a failure to launch the requested operation. It no longer has to receive a nominally successful spawn whose command interpretation may differ from the literal argument it supplied.