Handle win32 separator for cygwin paths
This PR handles a issue that cygwin actually supports Win32 path, so we need to handle the Win32 prefix and separaters.
r? @mati865
cc @jeremyd2019
~~Not sure if I should handle the prefix like the windows target... Cygwin does support win32 paths directly going through the APIs, but I think it's not the recommended way.~~
Here I just use cygwin_conv_path because it handles both cygwin and win32 paths correctly and convert them into absolute POSIX paths.
UPDATE: Windows path prefix is handled.
The job x86_64-gnu-tools failed! Check out the build log: (web) (plain)
Click to see the possible cause of the failure (guessed by this bot)
tests/pass-dep/tokio/file-io.rs ... FAILED
tests/pass-dep/libc/mmap.rs ... ok
FAILED TEST: tests/pass-dep/tokio/file-io.rs
command: MIRI_ENV_VAR_TEST="0" MIRI_TEMP="/tmp/miri-uitest-A3m2FG" RUST_BACKTRACE="1" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage2/bin/miri" "--error-format=json" "--sysroot=/checkout/obj/build/x86_64-unknown-linux-gnu/miri-sysroot" "-Dwarnings" "-Dunused" "-Ainternal_features" "-Zui-testing" "--out-dir" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/tests/pass-dep/tokio" "tests/pass-dep/tokio/file-io.rs" "-Zmiri-disable-isolation" "--extern" "cfg_if=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libcfg_if-55a32e410325a882.rlib" "--extern" "cfg_if=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libcfg_if-55a32e410325a882.rmeta" "--extern" "getrandom_01=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libgetrandom-77190f9b44a3c009.rlib" "--extern" "getrandom_01=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libgetrandom-77190f9b44a3c009.rmeta" "--extern" "getrandom_02=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libgetrandom-56888fca95a09bf8.rlib" "--extern" "getrandom_02=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libgetrandom-56888fca95a09bf8.rmeta" "--extern" "getrandom_03=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libgetrandom-a8fc577bb44b6d80.rlib" "--extern" "getrandom_03=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libgetrandom-a8fc577bb44b6d80.rmeta" "--extern" "libc=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/liblibc-dd98cac8117d550a.rlib" "--extern" "libc=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/liblibc-dd98cac8117d550a.rmeta" "--extern" "num_cpus=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libnum_cpus-7ab8becb6da559b7.rlib" "--extern" "num_cpus=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libnum_cpus-7ab8becb6da559b7.rmeta" "--extern" "page_size=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libpage_size-c34469ad90c5eb76.rlib" "--extern" "page_size=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libpage_size-c34469ad90c5eb76.rmeta" "--extern" "tempfile=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libtempfile-d40c54a83e8abae9.rlib" "--extern" "tempfile=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libtempfile-d40c54a83e8abae9.rmeta" "--extern" "tokio=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libtokio-539f2d149eae570a.rlib" "--extern" "tokio=/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps/libtokio-539f2d149eae570a.rmeta" "-L" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/debug/deps" "-L" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/tmp/miri_ui/0/miri/x86_64-unknown-linux-gnu/debug/deps" "--edition" "2021"
error: test got exit status: 101, but expected 0
= note: the compiler panicked
error: no output was expected
---
4: test_create_and_write::{closure#0}
at tests/pass-dep/tokio/file-io.rs:24:5
5: main::{closure#0}
at tests/pass-dep/tokio/file-io.rs:14:29
6: tokio::runtime::park::CachedParkThread::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/park.rs:284:60
7: tokio::task::coop::with_budget::<std::task::Poll<()>, {closure@tokio::runtime::park::CachedParkThread::block_on<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/task/coop/mod.rs:167:5
8: tokio::task::coop::budget::<std::task::Poll<()>, {closure@tokio::runtime::park::CachedParkThread::block_on<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/task/coop/mod.rs:133:5
9: tokio::runtime::park::CachedParkThread::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/park.rs:284:31
10: tokio::runtime::context::blocking::BlockingRegionGuard::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/context/blocking.rs:66:9
11: tokio::runtime::scheduler::multi_thread::MultiThread::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/scheduler/multi_thread/mod.rs:87:13
12: tokio::runtime::context::runtime::enter_runtime::<{closure@tokio::runtime::scheduler::multi_thread::MultiThread::block_on<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}}, ()>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/context/runtime.rs:65:16
13: tokio::runtime::scheduler::multi_thread::MultiThread::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/scheduler/multi_thread/mod.rs:86:9
14: tokio::runtime::Runtime::block_on_inner::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/runtime.rs:370:45
15: tokio::runtime::Runtime::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/runtime.rs:342:13
16: main
at tests/pass-dep/tokio/file-io.rs:15:5
17: <fn() as std::ops::FnOnce<()>>::call_once - shim(fn())
at /checkout/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
---
4: test_create_and_write::{closure#0}
at tests/pass-dep/tokio/file-io.rs:24:5
5: main::{closure#0}
at tests/pass-dep/tokio/file-io.rs:14:29
6: tokio::runtime::park::CachedParkThread::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/park.rs:284:60
7: tokio::task::coop::with_budget::<std::task::Poll<()>, {closure@tokio::runtime::park::CachedParkThread::block_on<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/task/coop/mod.rs:167:5
8: tokio::task::coop::budget::<std::task::Poll<()>, {closure@tokio::runtime::park::CachedParkThread::block_on<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/task/coop/mod.rs:133:5
9: tokio::runtime::park::CachedParkThread::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/park.rs:284:31
10: tokio::runtime::context::blocking::BlockingRegionGuard::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/context/blocking.rs:66:9
11: tokio::runtime::scheduler::multi_thread::MultiThread::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/scheduler/multi_thread/mod.rs:87:13
12: tokio::runtime::context::runtime::enter_runtime::<{closure@tokio::runtime::scheduler::multi_thread::MultiThread::block_on<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>::{closure#0}}, ()>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/context/runtime.rs:65:16
13: tokio::runtime::scheduler::multi_thread::MultiThread::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/scheduler/multi_thread/mod.rs:86:9
14: tokio::runtime::Runtime::block_on_inner::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/runtime.rs:370:45
15: tokio::runtime::Runtime::block_on::<{async block@tests/pass-dep/tokio/file-io.rs:12:1: 12:15}>
at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/runtime/runtime.rs:342:13
16: main
at tests/pass-dep/tokio/file-io.rs:15:5
17: <fn() as std::ops::FnOnce<()>>::call_once - shim(fn())
at /checkout/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
---
Location:
/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ui_test-0.29.2/src/lib.rs:369
Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
error: test failed, to rerun pass `--test ui`
Caused by:
process didn't exit successfully: `/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/x86_64-unknown-linux-gnu/release/deps/ui-f58affc77002becb` (exit status: 1)
Command has failed. Rerun with -v to see more details.
Build completed unsuccessfully in 0:05:02
local time: Sun Jun 1 16:00:41 UTC 2025
network time: Sun, 01 Jun 2025 16:00:41 GMT
##[error]Process completed with exit code 1.
Post job cleanup.
Finally I reuse the prefix parser from Windows. Now the cygwin Path can handle Win32 path correctly. It should behave a little different as it allows / as verbatim separater. Here is a simple test code:
use std::path::{absolute, Path};
fn main() {
test_path("C:\\msys64");
test_path("/dev/sda1");
test_path("./sda1");
test_path("/c/Users");
test_path("\\\\?\\UNC/localhost/share");
}
fn test_path(p: impl AsRef<Path>) {
let p = p.as_ref();
let abs = absolute(p).unwrap();
if p.is_absolute() {
println!("{} == {}", p.display(), abs.display());
} else {
println!("{} -> {}", p.display(), abs.display());
}
println!("{:?}", p.components().collect::<Vec<_>>());
}
On Windows, it should output (the current drive is D:)
C:\msys64 == C:\msys64
[Prefix(PrefixComponent { raw: "C:", parsed: Disk(67) }), RootDir, Normal("msys64")]
/dev/sda1 -> D:\dev\sda1
[RootDir, Normal("dev"), Normal("sda1")]
./sda1 -> <<<current dir>>>\sda1
[CurDir, Normal("sda1")]
/c/Users -> D:\c\Users
[RootDir, Normal("c"), Normal("Users")]
\\?\UNC/localhost/share == \\?\UNC/localhost/share
[Prefix(PrefixComponent { raw: "\\\\?\\UNC/localhost/share", parsed: VerbatimUNC("localhost/share", "") })]
While on Cygwin, it should output
C:\msys64 == /c/msys64
[Prefix(PrefixComponent { raw: "C:", parsed: Disk(67) }), RootDir, Normal("msys64")]
/dev/sda1 == /dev/sda1
[RootDir, Normal("dev"), Normal("sda1")]
./sda1 -> <<<current dir>>>/sda1
[CurDir, Normal("sda1")]
/c/Users == /c/Users
[RootDir, Normal("c"), Normal("Users")]
\\?\UNC/localhost/share == //?/UNC/localhost/share
[Prefix(PrefixComponent { raw: "\\\\?\\UNC/localhost/share", parsed: VerbatimUNC("localhost", "share") })]
@jeremyd2019
I was going to raise the issue of encoding, but looking at the code it doesn't seem to deal with WIN_A as a different encoding than POSIX paths, so I guess that's not an issue. I guess I need to have a conversation on the Cygwin list to better understand this.
Actually rust doesn't handle very well for non-UTF-8 encoding on Unix. OsStr and Path are all UTF-8 (because they could be constructed from a UTF-8 str without copy) and they are passed to OS APIs directly without encoding conversion. The only concerns, I think, should be command line input and console input. Do you know that whether Cygwin handles the encoding conversion of argv and stdin? I hope it does:)
OK, seems that the command line is converted from UTF-16: https://github.com/cygwin/cygwin/blob/972872c0d396dbf0ce296b8280cee08ce7727b51/winsup/cygwin/dcrt0.cc#L902
And stdin handles the encoding conversion: https://github.com/cygwin/cygwin/blob/972872c0d396dbf0ce296b8280cee08ce7727b51/winsup/cygwin/fhandler/pty.cc#L3935
So I think all inputs are UTF-8 for a Cygwin program, and the developers could not construct a non-UTF-8 Win32 path if they aren't aware of encodings. If they are, they should know that Rust codes are UTF-8:)
Now I'm a little worried about cygwin_conv_path then. If the maintainers someday find that it should handle code page encodings, the code here will be messed up:(
This was originally reported by @Ry0tak through Rust's security disclosure process, and since this affects a still-in-development tier 3 target (with very few users right now) we agreed to let the development of the fix happen in the open.
This change is T-libs area, so it will require their review.
r? rust-lang/libs
I'm not familiar with Cygwin API at all, but I see what you want to achieve here, and I think it's sensible.
Now I'm a little worried about
cygwin_conv_paththen. If the maintainers someday find that it should handle code page encodings, the code here will be messed up:(
They wouldn't want to break that API as a normal release, right? I'd expected a documented API to remain backwards compatible.
@pietroalbini I'm out of the loop, could you shed some light on your comment?
Also, there is a typo in both the title and the first commit 😉
So I think all inputs are UTF-8 for a Cygwin program, and the developers could not construct a non-UTF-8 Win32 path if they aren't aware of encodings. If they are, they should know that Rust codes are UTF-8:)
Cygwin's encoding may not always be UTF-8 - one can set the locale environment variables like they can on Unix to set the encoding, and then the conversions of paths and terminal input will use that encoding instead of UTF-8.
In such locale, Rust doesn't even work on Linux. That's fair:)
@pietroalbini I'm out of the loop, could you shed some light on your comment?
@mati865 this was originally reported to [email protected] as it could defeat path traversal mitigations like this:
if p.components().into_iter().any(|x| x == Component::ParentDir) {
panic!("path traversal");
}
By only parsing unix-like paths, it would be possible to sneak in a path traversal using win32 paths. Given the current status of the target we agreed it would be ok to develop the fix in the open.
@tgross35 ping?
@pietroalbini I'm out of the loop, could you shed some light on your comment?
@mati865 this was originally reported to [email protected] as it could defeat path traversal mitigations like this:
if p.components().into_iter().any(|x| x == Component::ParentDir) { panic!("path traversal"); }By only parsing unix-like paths, it would be possible to sneak in a path traversal using win32 paths. Given the current status of the target we agreed it would be ok to develop the fix in the open.
@Berrysoft could you add a regression test to this effect that fails without this change? Probably in library/std/tests/path.rs.
Should there be some testing of // prefixes, like //?/C:/foo/bar and //server/share/foo.txt?
Should there be some testing of
//prefixes, like//?/C:/foo/barand//server/share/foo.txt?
I'll add some immediately.
@jeremyd2019 mind confirming/approving when this looks good to you? Considering you're one of the resident cygwin experts.
Added tests for // prefixes and mixed separater verbatim paths.
It looks like Cygwin will take //. as well, but only //.\C: not //./C:. Go figure
Are these tests actually run in CI? I was concerned that the prefix parsing had some hard-coded backslashes that may result in the forward-slash // prefixes not working as desired. It'd be good to get confirmation that these tests all pass before merging.
No. I ran the tests on my devices...
The prefix parser converts all / to \ in the beginning so it's OK to hardcode here.
It looks like Cygwin will take
//.as well, but only//.\C:not//./C:. Go figure
Well... You're right then. So when does Cygwin doesn't take / in a path?
P.S. cygwin_conv_path converts //.\C:/msys64 to //./C:/msys64 and I think it might be a bug?
It looks like Cygwin will take
//.as well, but only//.\C:not//./C:. Go figureWell... You're right then. So when does Cygwin doesn't take
/in a path?P.S.
cygwin_conv_pathconverts//.\C:/msys64to//./C:/msys64and I think it might be a bug?
I don't know, probably have to ask Corinna when she's online
@jeremyd2019 mind confirming/approving when this looks good to you? Considering you're one of the resident cygwin experts.
I think the tests cover the cases I thought of, with the exception of //. prefix which seems kind of questionable how that should work.
Seems that Cygwin treats //./ and //../ as //. Wierd but I think I can handle that...
I think I need more time to think how to handle the //././../././../ prefix as //... If there's no better way, I think I should remove the prefix parser for Cygwin.
Now they are handled. Based on my experiments, only //./ (together with //../) is special and should be dealt with. \\.\, \\./, //.\ are valid device paths. I have to add a new field to Components to record the extra length of ./ and ../.
The job mingw-check-1 failed! Check out the build log: (web) (plain)
Click to see the possible cause of the failure (guessed by this bot)
Compiling addr2line v0.24.2
[RUSTC-TIMING] addr2line test:false 0.867
[RUSTC-TIMING] gimli test:false 8.377
[RUSTC-TIMING] object test:false 11.905
error[E0599]: no method named `is_verbatim` found for tuple `(usize, Prefix<'_>)` in the current scope
--> library/std/src/sys/path/windows.rs:193:25
|
193 | if prefix.map(|x| x.is_verbatim()).unwrap_or(false) {
| ^^^^^^^^^^^ method not found in `(usize, Prefix<'_>)`
For more information about this error, try `rustc --explain E0599`.
[RUSTC-TIMING] std test:false 8.828
error: could not compile `std` (lib) due to 1 previous error
Build completed unsuccessfully in 0:12:54
My only concern is will this work out well if the current handling of //./ is determined to be a bug and is fixed? It may not be, // being a distinct root from / is apparently allowed by POSIX, and perhaps they require 'ordinary' handling of . and .. elements in that case (for current and parent directory) with the presence of \ used to allow handling of the Windows device namespace.
I posted this question to https://inbox.sourceware.org/cygwin/[email protected]/T/#u
I don't think it's a bug now. It's only a little wierd. Due to the design of rust std, I choose to identify the whole //././?/C: as a prefix.
Tested on Cygwin and I found that Cygwin even treats //./?/C:/msys64/../../../?/D:/msys64 equal to D:\msys64. So Cygwin really thinks that's a POSIX path. I think I should follow it. The path without driver letter or //, and not containing \, is a POSIX path even it contains /?/ and could be recognized finally.