Windows: is_executable returns true for a directory named git.exe (exists() should be is_file())
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 2/5
- Geschätzter Aufwand
- 1-3 Stunden
- Anfängerfreundlichkeit
- 88/100
- Issue-Typ
- Bug
- Klarheit
- Klar beschrieben
- Aktivitätsstatus
- Aktiv
- Tech-Stack
- rust
- Bereich
- operating-systems
Rechercherichtung
Beginne mit der Windows-Implementierung in src/lib.rs und überprüfe die vorhandenen Plattformtests in tests/tests.rs. Führe die Windows-Testsuite aus, einschließlich der Fälle für ein Verzeichnis mit einer nach PATHEXT benannten Erweiterung und für den Pfad des aktuellen Verzeichnisses. Als abgeschlossen gilt die Aufgabe, wenn Verzeichnisse abgelehnt werden, während reguläre Dateien und Symlinks das dokumentierte Verhalten beibehalten.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Reported by Asymptotic Tech: a company focusing on scalable formal verification for securing the world's infrastructure software.
Contact: [email protected].
Summary
On Windows, is_executable returns true for a directory whose name ends in .exe, .bat, .cmd or any other PATHEXT extension.
The Windows code first checks that the path exists (src/lib.rs#L60-L63). Then, if the path has an extension, it only checks whether that extension is in PATHEXT (src/lib.rs#L66-L82). It never checks that the path is a file. Path::exists is true for a directory, and Path::extension just looks at the name. So a directory named git.exe passes both checks.
The Unix code does not have this problem. It checks metadata.is_file() (src/lib.rs#L37-L46). That check was added in 56fd034 for #3, with the test !is_executable(".").
The Windows code almost got the same check. #14 (for #13) added if !self.is_file() in 441c1c7, then changed it to exists() twenty minutes later in 82f9c68 "because symlinks are ok". But Path::is_file already follows symlinks. So the change did nothing for symlinks, and it let directories through.
We checked this at commit 5c04d86, which is the current main and the published 1.0.6.
Impact
Tools use this crate to find programs on PATH. This bug makes them treat a folder as a program.
Example: a folder named git.exe sits in a PATH folder before the real git.exe. A tool that uses this crate picks the folder, tries to run it, and fails. The real git never runs.
Anyone who can create a folder in a PATH folder can cause this. That is often easier than adding a program there.
This bug does not run any code. It only gives a wrong answer and makes a program fail to start.
Affected versions
All releases that have the PATHEXT check. 1.0.2 to 1.0.6 have the exists() check from #14. 1.0.1 has the PATHEXT check with no existence check at all, so it has the same problem. We checked the tagged sources at commit 5c04d86 (main, 2026-06-15).
Reproduction
We did not have a Windows machine. Instead, we copied the Windows check from src/lib.rs at 5c04d86 into a small Linux program, without changing it (harness.rs.in and splice.sh below). The only part left out is the GetBinaryTypeW call, which the crate only reaches for paths with no extension. Path::exists, Path::is_file and Path::extension work the same on Linux and Windows, so the copied code gives the same answers. With PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.MSC the program prints:
directory named git.exe -> true (expected false)
regular file real-program.exe -> true (expected true)
symlink link.exe -> real-program.exe -> true (expected true)
missing.exe (does not exist) -> false (expected false)
With the fix below, the directory gives false, and the file and the symlink still give true.
Suggested fix
Use the check that 441c1c7 had:
// First, ensure that the file exists and is a regular file.
if !self.is_file() {
return false;
}
fix.patch below makes that change. test.patch adds two Windows tests to tests/tests.rs: a directory named *.exe is not executable, and . is not executable. With both patches, the crate compiles for x86_64-pc-windows-msvc and the Unix tests pass. We could not run the Windows tests, but your Windows CI can.
One thing we did not change: for a path with an extension, the answer still depends on the name only. An empty file named notes.exe is still "executable". That is how the crate documents PATHEXT, so we left it alone.
fix.patch
diff --git a/src/lib.rs b/src/lib.rs
index 1aa7767..3559005 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -57,8 +57,10 @@ mod windows {
impl IsExecutable for Path {
fn is_executable(&self) -> bool {
- // First, ensure that the file exists
- if !self.exists() {
+ // First, ensure that the file exists and is a regular file.
+ // `is_file` follows symlinks, so a symlink to an executable still
+ // passes; `exists` would also accept a directory named like one.
+ if !self.is_file() {
return false;
}
test.patch — the Windows regression tests (not executed here)
diff --git a/tests/tests.rs b/tests/tests.rs
index fd0a39a..41abd74 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -46,6 +46,20 @@ mod windows {
fn non_existent_correct_extension() {
assert!(!is_executable("./tests/non_existent.exe"));
}
+
+ #[test]
+ fn not_executable_directory_with_extension() {
+ let dir = std::env::temp_dir().join(format!("is_executable-dir-{}.exe", std::process::id()));
+ std::fs::create_dir_all(&dir).unwrap();
+ let verdict = is_executable(&dir);
+ std::fs::remove_dir(&dir).unwrap();
+ assert!(!verdict, "a directory named *.exe must not be executable");
+ }
+
+ #[test]
+ fn not_executable_directory() {
+ assert!(!is_executable("."));
+ }
}
#[cfg(any(target_os = "wasi", target_family = "wasm"))]
harness.rs.in — the Linux harness (the crate's Windows block goes at the marker)
// Linux harness for the Windows implementation of `is_executable`.
//
// The body of `windows_is_executable` between the markers is NOT written
// here: run.sh splices it in from the crate's own src/lib.rs (the guard and
// the PATHEXT block of `impl IsExecutable for Path`, `self` renamed to
// `path`), so what runs is the crate's text at the checked commit. The only
// thing left out is the `GetBinaryTypeW` fallback after it, which the crate
// reaches only for paths without an extension; the harness returns false
// there. `Path::exists`, `Path::is_file`, `Path::extension` and the string
// code behave the same on every platform.
//
// Exit 0 when the directory case returns true (defect present), 1 otherwise.
use std::fs;
use std::path::Path;
fn windows_is_executable(path: &Path) -> bool {
// @@CRATE_BODY@@
false
}
fn main() {
let dir = std::env::temp_dir().join(format!("is_executable-harness-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
// A typical Windows value.
std::env::set_var("PATHEXT", ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.MSC");
let evil_dir = dir.join("git.exe");
fs::create_dir(&evil_dir).unwrap();
let real = dir.join("real-program.exe");
fs::write(&real, b"MZ").unwrap();
let link = dir.join("link.exe");
std::os::unix::fs::symlink(&real, &link).unwrap();
let text = dir.join("notes.exe");
fs::write(&text, b"just text").unwrap();
let missing = dir.join("missing.exe");
let no_ext = dir.join("noext");
fs::write(&no_ext, b"MZ").unwrap();
let cases: [(&str, &Path, bool); 6] = [
("directory named git.exe", &evil_dir, false),
("regular file real-program.exe", &real, true),
("symlink link.exe -> real-program.exe", &link, true),
("empty-ish text file notes.exe (extension-only verdict, not fixed here)", &text, true),
("missing.exe (does not exist)", &missing, false),
("noext (no extension; falls through to the stubbed GetBinaryTypeW)", &no_ext, false),
];
let mut dir_verdict = None;
for (name, path, expected) in cases.iter() {
let got = windows_is_executable(path);
println!(" {:<72} -> {:<5} (expected {})", name, got, expected);
if *name == "directory named git.exe" {
dir_verdict = Some(got);
}
}
let _ = fs::remove_dir_all(&dir);
match dir_verdict {
Some(true) => println!("REPRODUCED: a directory named git.exe is judged executable by the Windows logic"),
_ => {
println!("NOT REPRODUCED: the directory is not judged executable");
std::process::exit(1);
}
}
}
splice.sh — splices the block out of src/lib.rs into the harness
#!/usr/bin/env bash
# splice.sh <lib.rs> <harness.rs.in> <out.rs>
# Extract the Windows guard + PATHEXT block from the crate's src/lib.rs (from
# the "First, ensure that the file exists" comment up to the line before the
# "Check using file properties" comment), rename `self` to `path`, and put it
# at the @@CRATE_BODY@@ marker. Fails if the markers are not found exactly once.
set -euo pipefail
LIB=$1; IN=$2; OUT=$3
START=$(grep -n "// First, ensure that the file exists" "$LIB" | cut -d: -f1)
END=$(grep -n "// Check using file properties" "$LIB" | cut -d: -f1)
[[ $(echo "$START" | wc -l) -eq 1 && $(echo "$END" | wc -l) -eq 1 && -n "$START" && -n "$END" ]] || { echo "splice: markers not found exactly once in $LIB" >&2; exit 2; }
BODY=$(sed -n "$((START)),$((END - 1))p" "$LIB")
python3 - "$IN" "$OUT" "$BODY" <<'PY'
import re
import sys
src, out, body = sys.argv[1], sys.argv[2], sys.argv[3]
# `self.` -> `path.` (done here rather than in sed: `\b` is GNU-only)
body = re.sub(r"\bself\.", "path.", body)
s = open(src).read()
assert s.count("// @@CRATE_BODY@@") == 1
open(out, "w").write(s.replace(" // @@CRATE_BODY@@", body))
PY
echo "spliced lines $START-$((END - 1)) of $LIB into $OUT"
- Vorherrschende Sprache
- Rust
- Sterne
- 24
- Forks
- 13
- PR-Merge-Kennzahlen
- Keine gemergten PRs in 30 T.
Entwicklungsumgebung
Dieses Projekt bietet weder Dev-Container noch Dockerfile noch Beitragsleitfaden – die Einrichtung liegt bei Ihnen. Beginnen Sie mit der README; die allgemeinen Schritte stehen in unserem Leitfaden für den ersten Beitrag.
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus fitzgen/is_executable
-
Schwierigkeit 3/5 1-2 Tage Anfängerfreundlichkeit 35/100
fitzgen/is_executable#12 · 2 Kommentare ·
-
Schwierigkeit 3/5 1-2 Tage Anfängerfreundlichkeit 35/100
fitzgen/is_executable#9 · 4 Kommentare · 1 Reaktion ·
-
Check ownership too?Offen
Schwierigkeit 5/5 Über eine Woche Anfängerfreundlichkeit 28/100
fitzgen/is_executable#2 · 1 Kommentar ·
Alle Issues in fitzgen/is_executable
Ähnliche Issues
-
bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
stellar/stellar-cli#2773 ·
Maintainer antworten meist innerhalb von 2 Tagen
-
bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 85/100
voidzero-dev/oxc-angular-compiler#511 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 1/5 1-3 Stunden Anfängerfreundlichkeit 86/100
yantrikos/yantrik-os#539 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 82/100
Maintainer antworten meist innerhalb von 1 Tag
-
documentation station:mac ui-dashboard
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 86/100
rolter-ai/rolter#2490 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag