Security#
Completion candidates flow into shell interpreters as part of command lines, so conda-completion applies defense-in-depth against injection and data corruption.
Trust boundaries#
Three boundaries matter:
Manifest generation (Python, runs as the user) introspects conda’s argparse tree and writes the manifest. Input is trusted (conda’s own parser), but the output path needs symlink protection.
File I/O (Python and Rust) reads and writes cache files, manifests, and version data. Files on disk could be tampered with by other processes.
Shell output (Rust, runs on TAB press) produces text consumed by the shell’s completion system. Malicious data in the manifest or project files could attempt to inject shell commands.
Output sanitization#
The Rust binary sanitizes all completion candidates before emitting them.
sanitize() uses an allowlist of permitted characters:
Alphanumerics (
a-z,A-Z,0-9)Hyphens (
-), dots (.), underscores (_)Slashes (
/), equals (=), at signs (@)Spaces, colons (
:), plus (+), tilde (~), backslash (\)
Everything else is stripped, blocking shell metacharacters like $,
backticks, semicolons, pipes, and parentheses from reaching the shell
interpreter.
The function uses Cow<str> to skip allocation when the input is
already clean (the common case).
Per-shell escaping#
After sanitization, shell-specific escaping is applied:
Zsh: colons and backslashes are escaped (
\:,\\) because zsh uses colons as delimiters in_describe.Fish and PowerShell: tab-separated
candidate\tdescriptionformat.Bash: one candidate per line, no descriptions (
compgen/completedoes not support them).
Symlink protection#
Both Python and Rust refuse cache-file symlinks:
Python (atomic_write in manifest.py) checks path.is_symlink()
before and after creating the temp file. If the target is a symlink at
either point, the write is aborted and the temp file is removed. The
double-check narrows the TOCTOU window, though it does not eliminate it
entirely on all filesystems.
Python manifest and version readers also reject symlinked
completion.msgpack, versions.index, and versions.store files.
Rust (StatCache.save in cache.rs) checks symlink_metadata(path)
before writing. Uses tempfile::NamedTempFile with O_EXCL semantics
to create the temp file, then calls persist() to atomically rename it.
Rust (file_stat, read_to_string_limited, read_to_bytes_limited
in cache.rs) uses symlink_metadata() on all file reads to verify
the path is a regular file. Symlinked files are silently skipped.
Atomic file operations#
All file writes use a temp-file-then-rename pattern:
Create a temporary file in the same directory as the target.
Write all data to the temp file.
Rename the temp file to the target path.
Readers never see a partially-written file. If the process crashes
mid-write, only an orphaned temp file remains. On POSIX systems,
rename(2) is atomic within a single filesystem.
File size limits#
Both Python and Rust enforce size limits on file reads:
Manifest: 50 MB (
MAX_MANIFEST_SIZEin Python)Project files: 10 MB (
MAX_FILE_SIZEin Rust)Collection sizes: 2,000,000 entries for arrays and maps
The Python side passes these to msgpack’s deserialization functions
(max_str_len, max_bin_len, max_array_len, max_map_len). The
Rust side uses rmp_serde with post-load length checks.
No shell execution#
The Rust binary never executes shell commands, subprocesses, or
eval-like operations. It reads files, deserializes data, matches
strings, and writes text to stdout. The shell’s own completion machinery
interprets the output.
Startup hooks may use shell-native evaluation (eval, source, or
Invoke-Expression) to load the generated completion function. During
completion, that function executes the Rust binary directly and consumes
its stdout; the binary itself is a pure data processor.
Summary#
Threat |
Mitigation |
|---|---|
Shell injection via completion output |
Allowlist sanitization + per-shell escaping |
Symlink attack on cache/manifest/version files |
Symlink rejection + atomic rename |
Partial file reads (crash mid-write) |
Temp-file-then-rename for all writes |
Oversized input files |
Size limits on all file reads (10-50 MB) |
Malformed msgpack data |
Size-bounded deserialization + type validation |
Stale data after plugin changes |
Automatic hash-based regeneration |