Windows: is_executable returns true for a directory named git.exe (exists() should be is_file())
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 88/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- rust
- Lĩnh vực
- operating-systems
Hướng nghiên cứu
Bắt đầu trong phần triển khai Windows ở src/lib.rs và xem xét các bài kiểm thử nền tảng hiện có trong tests/tests.rs. Chạy bộ kiểm thử Windows, bao gồm các trường hợp cho một thư mục được đặt tên với phần mở rộng PATHEXT và đường dẫn thư mục hiện tại. Hoàn tất có nghĩa là các thư mục bị từ chối, trong khi các tệp thông thường và symlink vẫn giữ nguyên hành vi được ghi chép.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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"
- Ngôn ngữ chính
- Rust
- Star
- 24
- Fork
- 13
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của fitzgen/is_executable
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 35/100
fitzgen/is_executable#12 · 2 bình luận ·
-
Missing `is_symlink` check?Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 35/100
fitzgen/is_executable#9 · 4 bình luận · 1 reaction ·
-
Check ownership too?Đang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 28/100
fitzgen/is_executable#2 · 1 bình luận ·
Tất cả issue của fitzgen/is_executable
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 69/100
router-for-me/EasyCLIProxyAPI#406 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 83/100
Maintainer thường phản hồi trong vòng 1 ngày
-
ai-priority/P2 bug db/oracle user-priority/P1
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Sweep stale notesĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 63/100