Skip to content

Commit

Permalink
Support Windows Device Paths in WASI
Browse files Browse the repository at this point in the history
Turns out that Windows paths are very complicated. There's all sorts of
relative paths and various device paths and UNC paths. Here's a TL;DR of
how it all works:

1. Win32 paths may be in one of various relative forms that depend on
   hidden environment variables, such as `C:\Windows` (absolute on C
   drive), `Windows\System32\user32.dll` (relative to current dir on
   current drive), `C:Windows` (relative to current dir on C drive), or
   `\Windows` (absolute on current drive). There's also `\\server\share`
   paths that are called UNC paths.
2. These paths then get converted into a form that's rooted in a device
   (called device path) starting with `\\.\` followed by a device name.
   UNC paths get mapped to `\\.\UNC\server\share`. If a reserved device
   is part of the path, it takes precedence and becomes the device path
   (`C:\some\dir\COM1.txt` -> `\\.\COM1`).
3. The paths then get normalized / canonicalized (forward slashes
   converted, .. and . resolved, some spaces and dots get removed) into
   the `\\?\` form (normalized device path). Because this is the step
   that replaces forward slashes by backward slashes, all previous forms
   mentioned may use forward slashes instead. `Path::canonicalize`
   handles all three steps, meaning a path returned by it starts with
   `\\?\` and skips all these three steps when used.
4. The `\\?\` form gets passed almost directly to NT, though it gets
   replaced with `\??\`. At this point it is an NT path. The NT path
   `\??\` matches `\GLOBAL??\` where the devices are then looked up. The
   device names may actually be "symbolic links" in the NT object
   namespace to other devices (so a symbolic link from one NT path to
   another). So for example `C:` is actually a symbolic link at
   `\GLOBAL??\C:` to `\Device\HarddiskVolume1` (or any other number).
   Various other forms of NT paths are also possible, but you can't get
   to them from a Win32 path (except via the device symlink called
   `GLOBALROOT`). The driver is then chosen based on the device that it
   resolves to.

Depending on what kind of Win32 path you have, you may skip some of the
steps on the way.

References:
 - https://chrisdenton.github.io/omnipath/
 - https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
 - https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
 - https://stackoverflow.com/a/46019856
 - https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
 - https://medium.com/walmartglobaltech/dos-file-path-magic-tricks-5eda7a7a85fa
 - https://reverseengineering.stackexchange.com/a/3799

We now map the `\\?\` paths, which are the lowest level before the NT
paths directly to `/mnt/device` in WASI. This should allow you to access
everything that's currently not accessible.
  • Loading branch information
CryZe committed Jan 29, 2024
1 parent f803c68 commit 86e4459
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 40 deletions.
6 changes: 3 additions & 3 deletions crates/livesplit-auto-splitting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ sysinfo = { version = "0.30.0", default-features = false, features = [
"multithread",
] }
time = { version = "0.3.3", default-features = false }
wasmtime = { version = "16.0.0", default-features = false, features = [
wasmtime = { version = "17.0.0", default-features = false, features = [
"cranelift",
"parallel-compilation",
] }
wasmtime-wasi = { version = "16.0.0", default-features = false, features = [
wasmtime-wasi = { version = "17.0.0", default-features = false, features = [
"sync",
] }
wasi-common = "16.0.0"
wasi-common = "17.0.0"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.52.0", features = ["Win32_Storage_FileSystem"] }
Expand Down
7 changes: 4 additions & 3 deletions crates/livesplit-auto-splitting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,9 +532,10 @@ support:

- `stdout` / `stderr` / `stdin` are unbound. Those streams currently do
nothing.
- The file system is currently almost entirely empty. The host's file system
is accessible through `/mnt`. It is entirely read-only. Windows paths are
mapped to `/mnt/c`, `/mnt/d`, etc. to match WSL.
- The file system is currently almost entirely empty. The host's file system is
accessible through `/mnt`. It is entirely read-only. Windows paths are mapped
to `/mnt/c`, `/mnt/d`, etc. to match WSL. Additionally `/mnt/device` maps to
`\\?\` on Windows to access additional paths.
- There are no environment variables.
- There are no command line arguments.
- There is no networking.
Expand Down
3 changes: 2 additions & 1 deletion crates/livesplit-auto-splitting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@
//! nothing.
//! - The file system is currently almost entirely empty. The host's file system
//! is accessible through `/mnt`. It is entirely read-only. Windows paths are
//! mapped to `/mnt/c`, `/mnt/d`, etc. to match WSL.
//! mapped to `/mnt/c`, `/mnt/d`, etc. to match WSL. Additionally
//! `/mnt/device` maps to `\\?\` on Windows to access additional paths.
//! - There are no environment variables.
//! - There are no command line arguments.
//! - There is no networking.
Expand Down
67 changes: 66 additions & 1 deletion crates/livesplit-auto-splitting/src/runtime/api/wasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn build(script_path: Option<&Path>) -> WasiCtx {
drives &= !(1 << drive_idx);
let drive = drive_idx as u8 + b'a';
if let Ok(path) = wasmtime_wasi::Dir::open_ambient_dir(
str::from_utf8(&[drive, b':', b'\\']).unwrap(),
str::from_utf8(&[b'\\', b'\\', b'?', b'\\', drive, b':', b'\\']).unwrap(),
ambient_authority(),
) {
wasi.push_dir(
Expand All @@ -43,6 +43,9 @@ pub fn build(script_path: Option<&Path>) -> WasiCtx {
.unwrap();
}
}

wasi.push_dir(Box::new(DeviceDir), PathBuf::from("/mnt/device"))
.unwrap();
}
#[cfg(not(windows))]
{
Expand Down Expand Up @@ -128,3 +131,65 @@ impl WasiDir for ReadOnlyDir {
self.0.get_path_filestat(path, follow_symlinks).await
}
}

#[cfg(windows)]
struct DeviceDir;

#[cfg(windows)]
#[async_trait::async_trait]
impl WasiDir for DeviceDir {
fn as_any(&self) -> &dyn std::any::Any {
self
}

async fn open_file(
&self,
symlink_follow: bool,
path: &str,
oflags: OFlags,
read: bool,
write: bool,
fdflags: FdFlags,
) -> Result<OpenResult, wasi_common::Error> {
let (dir, file) = device_path(path)?;
dir.open_file(symlink_follow, file, oflags, read, write, fdflags)
.await
}

// FIXME: cap-primitives/src/windows/fs/get_path tries to strip `\\?\`,
// which breaks paths that aren't valid without it, such as UNC paths:
// https://github.com/bytecodealliance/cap-std/issues/348

async fn read_link(&self, path: &str) -> Result<PathBuf, wasi_common::Error> {
let (dir, file) = device_path(path)?;
dir.read_link(file).await
}

async fn get_path_filestat(
&self,
path: &str,
follow_symlinks: bool,
) -> Result<Filestat, wasi_common::Error> {
let (dir, file) = device_path(path)?;
dir.get_path_filestat(file, follow_symlinks).await
}
}

#[cfg(windows)]
fn device_path(path: &str) -> Result<(ReadOnlyDir, &str), wasi_common::Error> {
let (parent, file) = path
.strip_suffix('/')
.unwrap_or(path)
.rsplit_once('/')
.ok_or_else(wasi_common::Error::not_supported)?;

let parent = wasi_path::to_native(&format!("/mnt/device/{parent}"), true)
.ok_or_else(wasi_common::Error::not_supported)?;

let dir = wasmtime_wasi::dir::Dir::from_cap_std(
wasmtime_wasi::Dir::open_ambient_dir(parent, ambient_authority())
.map_err(|_| wasi_common::Error::not_supported())?,
);

Ok((ReadOnlyDir(dir), file))
}
Loading

0 comments on commit 86e4459

Please sign in to comment.