Hacktoberfest 2026: die Issues, die Maintainer für den Oktober markiert haben – offen und einsteigerfreundlich. Hacktoberfest-Issues durchsuchen

Windows: is_executable returns true for a directory named git.exe (exists() should be is_file())

Offen Anfängerfreundlich
#24 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

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

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

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus fitzgen/is_executable

Alle Issues in fitzgen/is_executable

Ähnliche Issues

Weitere Issues zu Rust

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.