August 3, 2023·6 min read·engineeringrustsecuritycargopermissions

Cargo's permission fix also invalidates old extracted sources

Rust 1.71.1 makes Cargo respect the Unix umask when extracting crate archives. It also replaces cached extractions made by older versions, because fixing future writes would leave existing writable source exposed.

A correctly downloaded dependency can become untrusted after extraction if another local user can modify its source files. Cargo's August 3 advisory describes exactly that failure. On Unix-like systems, extraction preserved archive permission bits without applying the user's umask. An archive could consequently leave dependency source writable by other local users.

Rust 1.71.1 corrects CVE-2023-38497 by applying the mask during extraction. The release also addresses previously extracted sources, because Cargo caches those files for later builds. The repair has to change both the writer and the policy for reusing output written by older versions.

I value Rust's control over memory and ownership, but those guarantees begin with the program actually compiled. A writable dependency cache can change that program before the compiler sees it. Local filesystem permissions are part of the source integrity model, even when the application itself contains no unsafe code.

Extraction crossed a local-user boundary

The advisory explains Unix permissions in terms of the file owner, owning group and other local users. A umask removes selected permissions during creation. Under common defaults described in the disclosure, only the owner should receive write permission for an ordinary newly created file.

Cargo instead propagated permissions stored in the crate archive. If those permissions allowed other users to write, and no additional system protection prevented access, another local user could change the dependency source. A later compilation could then compile and execute code under the victim user's authority.

The attack requires a path from archive permissions to local access. A broadly writable file is not necessarily reachable by another user if enclosing directory permissions prevent traversal. The advisory explicitly qualifies exploitability by system-dependent protections, so it does not establish identical exposure on every Unix installation.

That qualification does not make the extraction behavior acceptable. The user configured a creation policy through the umask, and Cargo failed to apply it. A dependency archive should not silently override that local restriction merely because it includes more permissive mode bits.

The affected scope includes Rust versions before 1.71.1 on Unix-like systems such as macOS and Linux. Windows and other non-Unix systems are excluded by the advisory. The relevant failure is the handling of Unix permission semantics, rather than a cross-platform claim that every Cargo cache has the same permissions.

A cached extraction persists beyond the fetch

Cargo extracts dependency source so the compiler can read it. The advisory says extraction normally occurs the first time a dependency is used, with the expanded files retained for future invocations. That saves repeated decompression and filesystem work.

It also lengthens the period during which source integrity depends on local permissions. The attacker need not control the registry connection during each build. If they can alter a cached extraction, the changed bytes can become an input to a later invocation that trusts the cache.

This makes the writable directory more than disposable intermediate output. It is a reusable source input. An operation that was safe when the archive arrived can become unsafe later if another principal can write the expanded representation. The trust claim must hold over the cache's lifetime, not just at download time.

I prefer to describe this as a local source-integrity failure because that identifies the authority involved. The attacker is another local user with access permitted by the effective filesystem configuration. The victim is the account that later compiles or executes the resulting code. No network interception is required for that disclosed path.

The patch applies the mask to archive extraction

The published extraction patch creates the archive reader, obtains the current umask on Unix and supplies it to the archive extraction mechanism. The accompanying tests expect extracted permission bits to exclude the bits removed by that mask.

That preserves a useful relationship between archive metadata and local policy. An archive can request permissions, but the user's mask constrains what extraction grants. The test covers ordinary source permissions as well as executable-file permissions, so applying the mask does not mean treating every file as an identical nonexecutable object.

The patch makes the mask operation a no-op on non-Unix platforms. Its comments explain that Windows archive handling uses the user-permission information for the read-only attribute. This keeps the Unix-specific correction tied to the platform semantics that motivated it.

Obtaining the mask itself requires care because the underlying operation changes the mask while returning its previous value. The patch caches the result and restores the old value after reading it. That detail illustrates that applying a familiar process setting still requires an implementation that handles its actual system-call interface.

Old cache entries need a migration signal

Fixing future extraction would leave existing cached directories unchanged. A build using an old expanded dependency might never call the new extraction path. The cache invalidation patch therefore changes the completion metadata Cargo uses to recognize an acceptable extraction.

The new .cargo-ok contents encode a version in JSON. Cargo accepts the recognized new version as evidence that it can reuse the extraction. An old marker containing the earlier ok text triggers removal of the expanded directory and extraction again. Unrecognized marker content also causes the directory to be rebuilt.

The version is not an integrity signature over every source byte. Its role is to distinguish cached output produced under the relevant extraction policy. Treating it as a general tamper detector would claim more than the patch implements. Local permissions still need to prevent unauthorized modification after a correct extraction.

The advisory describes this operationally as purging caches that the new Cargo tries to access when they were generated by older versions. That is an access-time correction, rather than a claim that installing the toolchain immediately scans every cache directory on the machine. The next relevant operation encounters the old completion state and replaces it.

The patch includes a test that simulates an old completion marker and verifies another extraction occurs. It also verifies that the new marker preserves reuse when no invalidation condition exists. Those are different requirements. The fix needs to invalidate old output without eliminating the cache's ordinary performance benefit for new output.

The completion file already had a history before this security change. Comments in the patch explain its earlier use for detecting interrupted extraction and changes in when Cargo created it. The new format extends that existing decision point instead of assuming that every directory with source files is reusable. A directory can contain plausible source while lacking evidence that the required extraction procedure completed.

That distinction gives the correction a defined migration path. Old state is recognized by the consumer at the moment it would otherwise be trusted. The consumer discards that state and uses the corrected writer to produce a replacement, preserving the ordinary workflow after the extra extraction.

Directory isolation is the documented interim mitigation

Users unable to update receive a concrete mitigation in the advisory. It recommends preventing other local users from accessing the Cargo directory, normally under the user's home directory. The supplied command removes group and other permissions from that directory.

chmod go= ~/.cargo

This changes the access path to cached files. Even when an extracted file has permissive bits, another user needs access through the surrounding directory to reach it. The mitigation therefore addresses a different layer from archive extraction, which explains how it can help without changing the older Cargo binary.

The documented directory is the usual location. The relevant principle is restricting access to the Cargo directory actually used by the build. A configuration that puts caches somewhere else needs its own account of who can traverse and modify that location. The advisory's command should be understood in relation to the stated path.

The repaired cache still has an owner

The correction does not make one user's writable source directory safe to share indiscriminately with other principals. It restores the local permission policy during extraction and replaces older cached output as Cargo accesses it. The account that owns those files still has authority to change them.

My judgment is that the cache invalidation is as important as the new extraction code. The old output outlives the process that wrote it, and a later build can reuse it without repeating that process. Rust 1.71.1 handles that persistence explicitly by giving corrected extractions a recognizable completion format and replacing entries that lack it.