diff --git a/.cirrus.yml b/.cirrus.yml
index abe8fce4..d6fa71b4 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -2,11 +2,10 @@
 # at revision 7f4774e76bd5cb9ccb7140d71ef9be9c16009cdf.
 
 task:
-  name: stable x86_64-unknown-freebsd-13
+  name: stable x86_64-unknown-freebsd-15-snap
   freebsd_instance:
-    image_family: freebsd-13-0-snap
+    image_family: freebsd-15-0-snap
   setup_script:
-    - pkg install -y curl
     - curl https://sh.rustup.rs -sSf --output rustup.sh
     - sh rustup.sh --default-toolchain stable -y --profile=minimal
     - . $HOME/.cargo/env
@@ -16,11 +15,23 @@ task:
     - cargo test --features=fs_utf8 --workspace
 
 task:
-  name: stable x86_64-unknown-freebsd-12
+  name: stable x86_64-unknown-freebsd-14
+  freebsd_instance:
+    image_family: freebsd-14-0
+  setup_script:
+    - curl https://sh.rustup.rs -sSf --output rustup.sh
+    - sh rustup.sh --default-toolchain stable -y --profile=minimal
+    - . $HOME/.cargo/env
+    - rustup default stable
+  test_script:
+    - . $HOME/.cargo/env
+    - cargo test --features=fs_utf8 --workspace
+
+task:
+  name: stable x86_64-unknown-freebsd-13
   freebsd_instance:
-    image_family: freebsd-12-1
+    image_family: freebsd-13-3
   setup_script:
-    - pkg install -y curl
     - curl https://sh.rustup.rs -sSf --output rustup.sh
     - sh rustup.sh --default-toolchain stable -y --profile=minimal
     - . $HOME/.cargo/env
diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml
index 6afc01d6..11c05760 100644
--- a/.github/actions/install-rust/action.yml
+++ b/.github/actions/install-rust/action.yml
@@ -8,5 +8,5 @@ inputs:
     default: 'stable'
 
 runs:
-  using: node16
+  using: node20
   main: 'main.js'
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 56e593bf..6a255228 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -11,7 +11,7 @@ jobs:
     name: Rustfmt
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -34,7 +34,7 @@ jobs:
             rust: beta
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -88,7 +88,7 @@ jobs:
             rust: beta
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -116,7 +116,7 @@ jobs:
             rust: nightly
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -150,7 +150,7 @@ jobs:
         riscv64gc-unknown-linux-gnu
         arm-unknown-linux-gnueabihf
         aarch64-linux-android
-        wasm32-wasi
+        wasm32-wasip1
     - run: cargo check --workspace --all-targets --all-features --release -vv
     - run: cargo check --workspace --all-targets --all-features --release -vv --target=x86_64-unknown-linux-musl
     - run: cargo check --workspace --all-targets --all-features --release -vv --target=x86_64-unknown-linux-gnux32
@@ -164,7 +164,7 @@ jobs:
     - run: cargo check --workspace --all-targets --all-features --release -vv --target=riscv64gc-unknown-linux-gnu
     - run: cargo check --workspace --all-targets --all-features --release -vv --target=arm-unknown-linux-gnueabihf
     - run: cargo check --workspace --all-targets --all-features --release -vv --target=aarch64-linux-android
-    - run: cd cap-std && cargo check --features=fs_utf8 --release -vv --target=wasm32-wasi
+    - run: cd cap-std && cargo check --features=fs_utf8 --release -vv --target=wasm32-wasip1
 
   check_cross_nightly_windows:
     name: Check Cross-Compilation on Rust nightly on Windows
@@ -178,7 +178,7 @@ jobs:
             rust: nightly
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -201,7 +201,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        build: [stable, windows-latest, windows-2019, macos-latest, macos-10.15, beta, ubuntu-20.04, aarch64-ubuntu]
+        build: [stable, windows-latest, windows-2019, macos-latest, macos-12, beta, ubuntu-20.04, aarch64-ubuntu]
         include:
           - build: stable
             os: ubuntu-latest
@@ -215,8 +215,8 @@ jobs:
           - build: macos-latest
             os: macos-latest
             rust: stable
-          - build: macos-10.15
-            os: macos-10.15
+          - build: macos-12
+            os: macos-12
             rust: stable
           - build: beta
             os: ubuntu-latest
@@ -234,7 +234,7 @@ jobs:
             qemu_target: aarch64-linux-user
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -291,7 +291,7 @@ jobs:
             rust: nightly
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -311,7 +311,7 @@ jobs:
             rust: stable
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -345,7 +345,7 @@ jobs:
       RUSTFLAGS: --cfg linux_raw
       RUSTDOCFLAGS: --cfg linux_raw
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -365,7 +365,7 @@ jobs:
             rust: 1.58
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
@@ -381,7 +381,7 @@ jobs:
     name: Fuzz Targets
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: true
     - uses: ./.github/actions/install-rust
diff --git a/Cargo.toml b/Cargo.toml
index 1916f0bd..f805529d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,7 +32,7 @@ libc = "0.2.100"
 io-lifetimes = "1.0.0"
 
 [target.'cfg(not(windows))'.dev-dependencies]
-rustix = { version = "0.37.0", features = ["fs"] }
+rustix = { version = "0.38.0", features = ["fs"] }
 
 [target.'cfg(windows)'.dev-dependencies]
 # nt_version uses internal Windows APIs, however we're only using it
@@ -40,10 +40,12 @@ rustix = { version = "0.37.0", features = ["fs"] }
 nt_version = "0.1.3"
 
 [target.'cfg(windows)'.dependencies.windows-sys]
-version = "0.45.0"
+version = "0.52.0"
 features = [
     "Win32_Storage_FileSystem",
     "Win32_Foundation",
+    "Win32_System_IO",
+    "Win32_System_Ioctl",
     "Win32_System_SystemServices",
 ]
 
diff --git a/build.rs b/build.rs
index 1a2cead4..c991825c 100644
--- a/build.rs
+++ b/build.rs
@@ -19,6 +19,9 @@ fn main() {
                                                   // https://doc.rust-lang.org/unstable-book/library-features/windows-file-type-ext.html
     use_feature_or_nothing("windows_file_type_ext");
 
+    // Cfgs that users may set.
+    println!("cargo:rustc-check-cfg=cfg(racy_asserts)");
+
     // Don't rerun this on changes other than build.rs, as we only depend on
     // the rustc version.
     println!("cargo:rerun-if-changed=build.rs");
@@ -28,6 +31,7 @@ fn use_feature_or_nothing(feature: &str) {
     if has_feature(feature) {
         use_feature(feature);
     }
+    println!("cargo:rustc-check-cfg=cfg({})", feature);
 }
 
 fn use_feature(feature: &str) {
@@ -36,7 +40,7 @@ fn use_feature(feature: &str) {
 
 /// Test whether the rustc at `var("RUSTC")` supports the given feature.
 fn has_feature(feature: &str) -> bool {
-    can_compile(&format!(
+    can_compile(format!(
         "#![allow(stable_features)]\n#![feature({})]",
         feature
     ))
@@ -46,12 +50,11 @@ fn has_feature(feature: &str) -> bool {
 fn can_compile<T: AsRef<str>>(test: T) -> bool {
     use std::process::Stdio;
 
-    let out_dir = var("OUT_DIR").unwrap();
     let rustc = var("RUSTC").unwrap();
     let target = var("TARGET").unwrap();
 
-    // Use `RUSTC_WRAPPER` if it's set, unless it's set to an empty string,
-    // as documented [here].
+    // Use `RUSTC_WRAPPER` if it's set, unless it's set to an empty string, as
+    // documented [here].
     // [here]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads
     let wrapper = var("RUSTC_WRAPPER")
         .ok()
@@ -70,8 +73,9 @@ fn can_compile<T: AsRef<str>>(test: T) -> bool {
         .arg("--emit=metadata") // Do as little as possible but still parse.
         .arg("--target")
         .arg(target)
-        .arg("--out-dir")
-        .arg(out_dir); // Put the output somewhere inconsequential.
+        .arg("-o")
+        .arg("-")
+        .stdout(Stdio::null()); // We don't care about the output (only whether it builds or not)
 
     // If Cargo wants to set RUSTFLAGS, use that.
     if let Ok(rustflags) = var("CARGO_ENCODED_RUSTFLAGS") {
diff --git a/cap-async-std/Cargo.toml b/cap-async-std/Cargo.toml
index 90048a67..92948372 100644
--- a/cap-async-std/Cargo.toml
+++ b/cap-async-std/Cargo.toml
@@ -23,7 +23,7 @@ io-extras = { version = "0.17.0", features = ["use_async_std"] }
 camino = { version = "1.0.5", optional = true }
 
 [target.'cfg(not(windows))'.dependencies]
-rustix = { version = "0.37.0", features = ["fs"] }
+rustix = { version = "0.38.0", features = ["fs"] }
 
 [features]
 default = []
diff --git a/cap-directories/Cargo.toml b/cap-directories/Cargo.toml
index e3a3804a..583958aa 100644
--- a/cap-directories/Cargo.toml
+++ b/cap-directories/Cargo.toml
@@ -17,7 +17,7 @@ cap-std = { path = "../cap-std", version = "^1.0.9" }
 directories-next = "2.0.0"
 
 [target.'cfg(not(windows))'.dependencies]
-rustix = { version = "0.37.0" }
+rustix = { version = "0.38.0" }
 
 [target.'cfg(windows)'.dependencies.windows-sys]
 version = "0.45.0"
diff --git a/cap-fs-ext/Cargo.toml b/cap-fs-ext/Cargo.toml
index b42fa771..2a4f62a3 100644
--- a/cap-fs-ext/Cargo.toml
+++ b/cap-fs-ext/Cargo.toml
@@ -40,3 +40,9 @@ features = [
 
 [dev-dependencies]
 cap-tempfile = { path = "../cap-tempfile" }
+
+[lints.rust.unexpected_cfgs]
+level = "warn"
+check-cfg = [
+    'cfg(feature, values("async_std"))'
+]
diff --git a/cap-fs-ext/build.rs b/cap-fs-ext/build.rs
index 6e8f4d3f..1716d753 100644
--- a/cap-fs-ext/build.rs
+++ b/cap-fs-ext/build.rs
@@ -13,6 +13,7 @@ fn use_feature_or_nothing(feature: &str) {
     if has_feature(feature) {
         use_feature(feature);
     }
+    println!("cargo:rustc-check-cfg=cfg({})", feature);
 }
 
 fn use_feature(feature: &str) {
@@ -21,20 +22,60 @@ fn use_feature(feature: &str) {
 
 /// Test whether the rustc at `var("RUSTC")` supports the given feature.
 fn has_feature(feature: &str) -> bool {
-    let out_dir = var("OUT_DIR").unwrap();
+    can_compile(&format!(
+        "#![allow(stable_features)]\n#![feature({})]",
+        feature
+    ))
+}
+
+/// Test whether the rustc at `var("RUSTC")` can compile the given code.
+fn can_compile<T: AsRef<str>>(test: T) -> bool {
+    use std::process::Stdio;
+
     let rustc = var("RUSTC").unwrap();
+    let target = var("TARGET").unwrap();
+
+    // Use `RUSTC_WRAPPER` if it's set, unless it's set to an empty string,
+    // as documented [here].
+    // [here]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads
+    let wrapper = var("RUSTC_WRAPPER")
+        .ok()
+        .and_then(|w| if w.is_empty() { None } else { Some(w) });
 
-    let mut child = std::process::Command::new(rustc)
-        .arg("--crate-type=rlib") // Don't require `main`.
+    let mut cmd = if let Some(wrapper) = wrapper {
+        let mut cmd = std::process::Command::new(wrapper);
+        // The wrapper's first argument is supposed to be the path to rustc.
+        cmd.arg(rustc);
+        cmd
+    } else {
+        std::process::Command::new(rustc)
+    };
+
+    cmd.arg("--crate-type=rlib") // Don't require `main`.
         .arg("--emit=metadata") // Do as little as possible but still parse.
-        .arg("--out-dir")
-        .arg(out_dir) // Put the output somewhere inconsequential.
+        .arg("--target")
+        .arg(target)
+        .arg("-o")
+        .arg("-")
+        .stdout(Stdio::null()); // We don't care about the output (only whether it builds or not)
+
+    // If Cargo wants to set RUSTFLAGS, use that.
+    if let Ok(rustflags) = var("CARGO_ENCODED_RUSTFLAGS") {
+        if !rustflags.is_empty() {
+            for arg in rustflags.split('\x1f') {
+                cmd.arg(arg);
+            }
+        }
+    }
+
+    let mut child = cmd
         .arg("-") // Read from stdin.
-        .stdin(std::process::Stdio::piped()) // Stdin is a pipe.
+        .stdin(Stdio::piped()) // Stdin is a pipe.
+        .stderr(Stdio::null()) // Errors from feature detection aren't interesting and can be confusing.
         .spawn()
         .unwrap();
 
-    writeln!(child.stdin.take().unwrap(), "#![feature({})]", feature).unwrap();
+    writeln!(child.stdin.take().unwrap(), "{}", test.as_ref()).unwrap();
 
     child.wait().unwrap().success()
 }
diff --git a/cap-primitives/Cargo.toml b/cap-primitives/Cargo.toml
index 80f11559..69f00e02 100644
--- a/cap-primitives/Cargo.toml
+++ b/cap-primitives/Cargo.toml
@@ -25,7 +25,7 @@ io-lifetimes = { version = "1.0.0", default-features = false }
 cap-tempfile = { path = "../cap-tempfile" }
 
 [target.'cfg(not(windows))'.dependencies]
-rustix = { version = "0.37.0", features = ["fs", "process", "procfs", "termios", "time"] }
+rustix = { version = "0.38.31", features = ["fs", "process", "procfs", "termios", "time"] }
 
 [target.'cfg(windows)'.dependencies]
 winx = "0.35.0"
diff --git a/cap-primitives/build.rs b/cap-primitives/build.rs
index 966799b5..0515658a 100644
--- a/cap-primitives/build.rs
+++ b/cap-primitives/build.rs
@@ -5,9 +5,15 @@ fn main() {
     use_feature_or_nothing("windows_by_handle"); // https://github.com/rust-lang/rust/issues/63010
                                                  // https://doc.rust-lang.org/unstable-book/library-features/windows-file-type-ext.html
     use_feature_or_nothing("windows_file_type_ext");
+    use_feature_or_nothing("windows_change_time");
     use_feature_or_nothing("io_error_more"); // https://github.com/rust-lang/rust/issues/86442
     use_feature_or_nothing("io_error_uncategorized");
 
+    // Cfgs that users may set.
+    println!("cargo:rustc-check-cfg=cfg(racy_asserts)");
+    println!("cargo:rustc-check-cfg=cfg(emulate_second_only_system)");
+    println!("cargo:rustc-check-cfg=cfg(io_lifetimes_use_std)");
+
     // Don't rerun this on changes other than build.rs, as we only depend on
     // the rustc version.
     println!("cargo:rerun-if-changed=build.rs");
@@ -17,6 +23,7 @@ fn use_feature_or_nothing(feature: &str) {
     if has_feature(feature) {
         use_feature(feature);
     }
+    println!("cargo:rustc-check-cfg=cfg({})", feature);
 }
 
 fn use_feature(feature: &str) {
@@ -25,20 +32,60 @@ fn use_feature(feature: &str) {
 
 /// Test whether the rustc at `var("RUSTC")` supports the given feature.
 fn has_feature(feature: &str) -> bool {
-    let out_dir = var("OUT_DIR").unwrap();
+    can_compile(&format!(
+        "#![allow(stable_features)]\n#![feature({})]",
+        feature
+    ))
+}
+
+/// Test whether the rustc at `var("RUSTC")` can compile the given code.
+fn can_compile<T: AsRef<str>>(test: T) -> bool {
+    use std::process::Stdio;
+
     let rustc = var("RUSTC").unwrap();
+    let target = var("TARGET").unwrap();
+
+    // Use `RUSTC_WRAPPER` if it's set, unless it's set to an empty string,
+    // as documented [here].
+    // [here]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads
+    let wrapper = var("RUSTC_WRAPPER")
+        .ok()
+        .and_then(|w| if w.is_empty() { None } else { Some(w) });
 
-    let mut child = std::process::Command::new(rustc)
-        .arg("--crate-type=rlib") // Don't require `main`.
+    let mut cmd = if let Some(wrapper) = wrapper {
+        let mut cmd = std::process::Command::new(wrapper);
+        // The wrapper's first argument is supposed to be the path to rustc.
+        cmd.arg(rustc);
+        cmd
+    } else {
+        std::process::Command::new(rustc)
+    };
+
+    cmd.arg("--crate-type=rlib") // Don't require `main`.
         .arg("--emit=metadata") // Do as little as possible but still parse.
-        .arg("--out-dir")
-        .arg(out_dir) // Put the output somewhere inconsequential.
+        .arg("--target")
+        .arg(target)
+        .arg("-o")
+        .arg("-")
+        .stdout(Stdio::null()); // We don't care about the output (only whether it builds or not)
+
+    // If Cargo wants to set RUSTFLAGS, use that.
+    if let Ok(rustflags) = var("CARGO_ENCODED_RUSTFLAGS") {
+        if !rustflags.is_empty() {
+            for arg in rustflags.split('\x1f') {
+                cmd.arg(arg);
+            }
+        }
+    }
+
+    let mut child = cmd
         .arg("-") // Read from stdin.
-        .stdin(std::process::Stdio::piped()) // Stdin is a pipe.
+        .stdin(Stdio::piped()) // Stdin is a pipe.
+        .stderr(Stdio::null()) // Errors from feature detection aren't interesting and can be confusing.
         .spawn()
         .unwrap();
 
-    writeln!(child.stdin.take().unwrap(), "#![feature({})]", feature).unwrap();
+    writeln!(child.stdin.take().unwrap(), "{}", test.as_ref()).unwrap();
 
     child.wait().unwrap().success()
 }
diff --git a/cap-primitives/src/fs/metadata.rs b/cap-primitives/src/fs/metadata.rs
index afcda83d..fd63d5c4 100644
--- a/cap-primitives/src/fs/metadata.rs
+++ b/cap-primitives/src/fs/metadata.rs
@@ -443,6 +443,11 @@ impl std::os::windows::fs::MetadataExt for Metadata {
     fn file_index(&self) -> Option<u64> {
         self.ext.file_index()
     }
+
+    #[inline]
+    fn change_time(&self) -> Option<u64> {
+        self.ext.change_time()
+    }
 }
 
 /// Extension trait to allow `volume_serial_number` etc. to be exposed by
diff --git a/cap-primitives/src/lib.rs b/cap-primitives/src/lib.rs
index e9943917..03245996 100644
--- a/cap-primitives/src/lib.rs
+++ b/cap-primitives/src/lib.rs
@@ -5,6 +5,7 @@
 #![allow(stable_features)]
 #![cfg_attr(target_os = "wasi", feature(wasi_ext))]
 #![cfg_attr(all(windows, windows_by_handle), feature(windows_by_handle))]
+#![cfg_attr(all(windows, windows_change_time), feature(windows_change_time))]
 #![cfg_attr(all(windows, windows_file_type_ext), feature(windows_file_type_ext))]
 #![doc(
     html_logo_url = "https://raw.githubusercontent.com/bytecodealliance/cap-std/main/media/cap-std.svg"
diff --git a/cap-primitives/src/rustix/fs/metadata_ext.rs b/cap-primitives/src/rustix/fs/metadata_ext.rs
index 9143a65d..61bd006b 100644
--- a/cap-primitives/src/rustix/fs/metadata_ext.rs
+++ b/cap-primitives/src/rustix/fs/metadata_ext.rs
@@ -102,6 +102,9 @@ impl MetadataExt {
     /// Constructs a new instance of `Metadata` from the given `Stat`.
     #[inline]
     pub(crate) fn from_rustix(stat: Stat) -> Metadata {
+        #[cfg(not(target_os = "wasi"))]
+        use rustix::fs::StatExt;
+
         Metadata {
             file_type: ImplFileTypeExt::from_raw_mode(stat.st_mode as RawMode),
             len: u64::try_from(stat.st_size).unwrap(),
@@ -112,12 +115,12 @@ impl MetadataExt {
 
             #[cfg(not(any(target_os = "netbsd", target_os = "wasi")))]
             modified: system_time_from_rustix(
-                stat.st_mtime.try_into().unwrap(),
+                stat.mtime().try_into().unwrap(),
                 stat.st_mtime_nsec as _,
             ),
             #[cfg(not(any(target_os = "netbsd", target_os = "wasi")))]
             accessed: system_time_from_rustix(
-                stat.st_atime.try_into().unwrap(),
+                stat.atime().try_into().unwrap(),
                 stat.st_atime_nsec as _,
             ),
 
@@ -178,19 +181,19 @@ impl MetadataExt {
                 rdev: u64::try_from(stat.st_rdev).unwrap(),
                 size: u64::try_from(stat.st_size).unwrap(),
                 #[cfg(not(target_os = "wasi"))]
-                atime: i64::try_from(stat.st_atime).unwrap(),
+                atime: i64::try_from(stat.atime()).unwrap(),
                 #[cfg(not(any(target_os = "netbsd", target_os = "wasi")))]
                 atime_nsec: stat.st_atime_nsec as _,
                 #[cfg(target_os = "netbsd")]
                 atime_nsec: stat.st_atimensec as _,
                 #[cfg(not(target_os = "wasi"))]
-                mtime: i64::try_from(stat.st_mtime).unwrap(),
+                mtime: i64::try_from(stat.mtime()).unwrap(),
                 #[cfg(not(any(target_os = "netbsd", target_os = "wasi")))]
                 mtime_nsec: stat.st_mtime_nsec as _,
                 #[cfg(target_os = "netbsd")]
                 mtime_nsec: stat.st_mtimensec as _,
                 #[cfg(not(target_os = "wasi"))]
-                ctime: i64::try_from(stat.st_ctime).unwrap(),
+                ctime: i64::try_from(stat.ctime()).unwrap(),
                 #[cfg(not(any(target_os = "netbsd", target_os = "wasi")))]
                 ctime_nsec: stat.st_ctime_nsec as _,
                 #[cfg(target_os = "netbsd")]
diff --git a/cap-primitives/src/rustix/fs/open_unchecked.rs b/cap-primitives/src/rustix/fs/open_unchecked.rs
index 4112a5a3..8ff6b351 100644
--- a/cap-primitives/src/rustix/fs/open_unchecked.rs
+++ b/cap-primitives/src/rustix/fs/open_unchecked.rs
@@ -2,7 +2,7 @@ use super::compute_oflags;
 use crate::fs::{stat_unchecked, OpenOptions, OpenUncheckedError};
 use crate::AmbientAuthority;
 use io_lifetimes::AsFilelike;
-use rustix::fs::{cwd, openat, Mode};
+use rustix::fs::{openat, Mode, CWD};
 use rustix::io;
 use std::fs;
 use std::path::Path;
@@ -67,5 +67,5 @@ pub(crate) fn open_ambient_impl(
     ambient_authority: AmbientAuthority,
 ) -> Result<fs::File, OpenUncheckedError> {
     let _ = ambient_authority;
-    open_unchecked(&cwd().as_filelike_view::<fs::File>(), path, options)
+    open_unchecked(&CWD.as_filelike_view::<fs::File>(), path, options)
 }
diff --git a/cap-primitives/src/rustix/fs/reopen_impl.rs b/cap-primitives/src/rustix/fs/reopen_impl.rs
index 40490a6b..e010a4d3 100644
--- a/cap-primitives/src/rustix/fs/reopen_impl.rs
+++ b/cap-primitives/src/rustix/fs/reopen_impl.rs
@@ -1,14 +1,14 @@
 use crate::fs::{open_unchecked, OpenOptions};
 use crate::rustix::fs::file_path;
 use io_lifetimes::AsFilelike;
-use rustix::fs::cwd;
+use rustix::fs::CWD;
 use std::{fs, io};
 
 /// Implementation of `reopen`.
 pub(crate) fn reopen_impl(file: &fs::File, options: &OpenOptions) -> io::Result<fs::File> {
     if let Some(path) = file_path(file) {
         Ok(open_unchecked(
-            &cwd().as_filelike_view::<fs::File>(),
+            &CWD.as_filelike_view::<fs::File>(),
             &path,
             options,
         )?)
diff --git a/cap-primitives/src/rustix/fs/stat_unchecked.rs b/cap-primitives/src/rustix/fs/stat_unchecked.rs
index 70b40d18..9cee69a8 100644
--- a/cap-primitives/src/rustix/fs/stat_unchecked.rs
+++ b/cap-primitives/src/rustix/fs/stat_unchecked.rs
@@ -54,7 +54,7 @@ pub(crate) fn stat_unchecked(
                     // the current working directory returns a similar error,
                     // then stop using `statx`.
                     if let Err(rustix::io::Errno::PERM) = statx(
-                        rustix::fs::cwd(),
+                        rustix::fs::CWD,
                         "",
                         AtFlags::EMPTY_PATH,
                         StatxFlags::empty(),
diff --git a/cap-primitives/src/rustix/linux/fs/open_impl.rs b/cap-primitives/src/rustix/linux/fs/open_impl.rs
index 793b77b0..cfe76b86 100644
--- a/cap-primitives/src/rustix/linux/fs/open_impl.rs
+++ b/cap-primitives/src/rustix/linux/fs/open_impl.rs
@@ -140,7 +140,7 @@ pub(crate) fn open_beneath(
 fn openat2_supported() -> bool {
     // `openat2` is supported in Linux 5.6 and later. Parse the current
     // Linux version from the `release` field from `uname` to detect this.
-    let uname = rustix::process::uname();
+    let uname = rustix::system::uname();
     let release = uname.release().to_bytes();
     if let Some((major, minor)) = linux_major_minor(release) {
         if major >= 6 || (major == 5 && minor >= 6) {
diff --git a/cap-primitives/src/rustix/linux/fs/procfs.rs b/cap-primitives/src/rustix/linux/fs/procfs.rs
index b596fc4d..42544c12 100644
--- a/cap-primitives/src/rustix/linux/fs/procfs.rs
+++ b/cap-primitives/src/rustix/linux/fs/procfs.rs
@@ -10,9 +10,9 @@ use crate::fs::{
     errors, open, read_link_unchecked, set_times_follow_unchecked, OpenOptions, SystemTimeSpec,
 };
 use io_lifetimes::{AsFd, AsFilelike};
-use rustix::fs::{chmodat, Mode, OFlags, RawMode};
-use rustix::io::proc_self_fd;
+use rustix::fs::{chmodat, AtFlags, Mode, OFlags, RawMode};
 use rustix::path::DecInt;
+use rustix::procfs::proc_self_fd;
 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
 use std::path::{Path, PathBuf};
 use std::{fs, io};
@@ -46,7 +46,12 @@ pub(crate) fn set_permissions_through_proc_self_fd(
 
     let dirfd = proc_self_fd()?;
     let mode = Mode::from_bits(perm.mode() as RawMode).ok_or_else(errors::invalid_flags)?;
-    Ok(chmodat(&dirfd, DecInt::from_fd(&opath), mode)?)
+    Ok(chmodat(
+        &dirfd,
+        DecInt::from_fd(&opath),
+        mode,
+        AtFlags::empty(),
+    )?)
 }
 
 pub(crate) fn set_times_through_proc_self_fd(
diff --git a/cap-primitives/src/windows/fs/metadata_ext.rs b/cap-primitives/src/windows/fs/metadata_ext.rs
index 0b1becf8..44a1940f 100644
--- a/cap-primitives/src/windows/fs/metadata_ext.rs
+++ b/cap-primitives/src/windows/fs/metadata_ext.rs
@@ -18,6 +18,8 @@ pub(crate) struct MetadataExt {
     volume_serial_number: Option<u32>,
     number_of_links: Option<u32>,
     file_index: Option<u64>,
+    #[cfg(windows_change_time)]
+    change_time: Option<u64>,
 }
 
 impl MetadataExt {
@@ -26,7 +28,8 @@ impl MetadataExt {
     #[inline]
     #[allow(unused_variables)]
     pub(crate) fn from(file: &fs::File, std: &fs::Metadata) -> io::Result<Self> {
-        let (mut volume_serial_number, mut number_of_links, mut file_index) = (None, None, None);
+        let (mut volume_serial_number, mut number_of_links, mut file_index, mut change_time) =
+            (None, None, None, None);
 
         #[cfg(windows_by_handle)]
         {
@@ -40,10 +43,17 @@ impl MetadataExt {
             if let Some(some) = std.file_index() {
                 file_index = Some(some);
             }
+            if let Some(some) = std.change_time() {
+                change_time = Some(some);
+            }
         }
 
         #[cfg(not(windows_by_handle))]
-        if volume_serial_number.is_none() || number_of_links.is_none() || file_index.is_none() {
+        if volume_serial_number.is_none()
+            || number_of_links.is_none()
+            || file_index.is_none()
+            || change_time.is_none()
+        {
             let fileinfo = winx::winapi_util::file::information(file)?;
             if volume_serial_number.is_none() {
                 let t64: u64 = fileinfo.volume_serial_number();
@@ -58,6 +68,7 @@ impl MetadataExt {
             if file_index.is_none() {
                 file_index = Some(fileinfo.file_index());
             }
+            change_time = None;
         }
 
         Ok(Self::from_parts(
@@ -65,6 +76,7 @@ impl MetadataExt {
             volume_serial_number,
             number_of_links,
             file_index,
+            change_time,
         ))
     }
 
@@ -78,7 +90,8 @@ impl MetadataExt {
     #[inline]
     #[allow(unused_mut)]
     pub(crate) fn from_just_metadata(std: &fs::Metadata) -> Self {
-        let (mut volume_serial_number, mut number_of_links, mut file_index) = (None, None, None);
+        let (mut volume_serial_number, mut number_of_links, mut file_index, mut change_time) =
+            (None, None, None, None);
 
         #[cfg(windows_by_handle)]
         {
@@ -92,9 +105,18 @@ impl MetadataExt {
             if let Some(some) = std.file_index() {
                 file_index = Some(some);
             }
+            if let Some(some) = std.change_time() {
+                change_time = Some(some);
+            }
         }
 
-        Self::from_parts(std, volume_serial_number, number_of_links, file_index)
+        Self::from_parts(
+            std,
+            volume_serial_number,
+            number_of_links,
+            file_index,
+            change_time,
+        )
     }
 
     #[inline]
@@ -103,8 +125,13 @@ impl MetadataExt {
         volume_serial_number: Option<u32>,
         number_of_links: Option<u32>,
         file_index: Option<u64>,
+        change_time: Option<u64>,
     ) -> Self {
         use std::os::windows::fs::MetadataExt;
+
+        #[cfg(not(windows_change_time))]
+        let _ = change_time;
+
         Self {
             file_attributes: std.file_attributes(),
             #[cfg(windows_by_handle)]
@@ -118,6 +145,8 @@ impl MetadataExt {
             volume_serial_number,
             number_of_links,
             file_index,
+            #[cfg(windows_change_time)]
+            change_time,
         }
     }
 
@@ -190,6 +219,11 @@ impl std::os::windows::fs::MetadataExt for MetadataExt {
     fn file_index(&self) -> Option<u64> {
         self.file_index
     }
+
+    #[inline]
+    fn change_time(&self) -> Option<u64> {
+        self.change_time
+    }
 }
 
 #[doc(hidden)]
diff --git a/cap-std/Cargo.toml b/cap-std/Cargo.toml
index 1260ea1f..5601ca5c 100644
--- a/cap-std/Cargo.toml
+++ b/cap-std/Cargo.toml
@@ -14,7 +14,7 @@ edition = "2018"
 
 [package.metadata.docs.rs]
 all-features = true
-rustdoc-args = ["--cfg=doc_cfg"]
+rustdoc-args = ["--cfg=docsrs"]
 
 [dependencies]
 arf-strings = { version = "0.7.0", optional = true }
@@ -24,7 +24,7 @@ io-lifetimes = { version = "1.0.0", default-features = false }
 camino = { version = "1.0.5", optional = true }
 
 [target.'cfg(not(windows))'.dependencies]
-rustix = { version = "0.37.0", features = ["fs"] }
+rustix = { version = "0.38.0", features = ["fs"] }
 
 [features]
 default = []
diff --git a/cap-std/build.rs b/cap-std/build.rs
index db85de0e..384b38a8 100644
--- a/cap-std/build.rs
+++ b/cap-std/build.rs
@@ -6,6 +6,10 @@ fn main() {
     use_feature_or_nothing("seek_convenience"); // https://github.com/rust-lang/rust/issues/59359
     use_feature_or_nothing("with_options"); // https://github.com/rust-lang/rust/issues/65439
     use_feature_or_nothing("write_all_vectored"); // https://github.com/rust-lang/rust/issues/70436
+    use_feature_or_nothing("windows_file_type_ext");
+
+    // Cfgs that users may set.
+    println!("cargo:rustc-check-cfg=cfg(io_lifetimes_use_std)");
 
     // Don't rerun this on changes other than build.rs, as we only depend on
     // the rustc version.
@@ -16,6 +20,7 @@ fn use_feature_or_nothing(feature: &str) {
     if has_feature(feature) {
         use_feature(feature);
     }
+    println!("cargo:rustc-check-cfg=cfg({})", feature);
 }
 
 fn use_feature(feature: &str) {
@@ -24,20 +29,60 @@ fn use_feature(feature: &str) {
 
 /// Test whether the rustc at `var("RUSTC")` supports the given feature.
 fn has_feature(feature: &str) -> bool {
-    let out_dir = var("OUT_DIR").unwrap();
+    can_compile(&format!(
+        "#![allow(stable_features)]\n#![feature({})]",
+        feature
+    ))
+}
+
+/// Test whether the rustc at `var("RUSTC")` can compile the given code.
+fn can_compile<T: AsRef<str>>(test: T) -> bool {
+    use std::process::Stdio;
+
     let rustc = var("RUSTC").unwrap();
+    let target = var("TARGET").unwrap();
+
+    // Use `RUSTC_WRAPPER` if it's set, unless it's set to an empty string,
+    // as documented [here].
+    // [here]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-reads
+    let wrapper = var("RUSTC_WRAPPER")
+        .ok()
+        .and_then(|w| if w.is_empty() { None } else { Some(w) });
 
-    let mut child = std::process::Command::new(rustc)
-        .arg("--crate-type=rlib") // Don't require `main`.
+    let mut cmd = if let Some(wrapper) = wrapper {
+        let mut cmd = std::process::Command::new(wrapper);
+        // The wrapper's first argument is supposed to be the path to rustc.
+        cmd.arg(rustc);
+        cmd
+    } else {
+        std::process::Command::new(rustc)
+    };
+
+    cmd.arg("--crate-type=rlib") // Don't require `main`.
         .arg("--emit=metadata") // Do as little as possible but still parse.
-        .arg("--out-dir")
-        .arg(out_dir) // Put the output somewhere inconsequential.
+        .arg("--target")
+        .arg(target)
+        .arg("-o")
+        .arg("-")
+        .stdout(Stdio::null()); // We don't care about the output (only whether it builds or not)
+
+    // If Cargo wants to set RUSTFLAGS, use that.
+    if let Ok(rustflags) = var("CARGO_ENCODED_RUSTFLAGS") {
+        if !rustflags.is_empty() {
+            for arg in rustflags.split('\x1f') {
+                cmd.arg(arg);
+            }
+        }
+    }
+
+    let mut child = cmd
         .arg("-") // Read from stdin.
-        .stdin(std::process::Stdio::piped()) // Stdin is a pipe.
+        .stdin(Stdio::piped()) // Stdin is a pipe.
+        .stderr(Stdio::null()) // Errors from feature detection aren't interesting and can be confusing.
         .spawn()
         .unwrap();
 
-    writeln!(child.stdin.take().unwrap(), "#![feature({})]", feature).unwrap();
+    writeln!(child.stdin.take().unwrap(), "{}", test.as_ref()).unwrap();
 
     child.wait().unwrap().success()
 }
diff --git a/cap-std/src/lib.rs b/cap-std/src/lib.rs
index 5ddf9f49..e425778f 100644
--- a/cap-std/src/lib.rs
+++ b/cap-std/src/lib.rs
@@ -23,7 +23,7 @@
 //! [`Pool`]: net::Pool
 
 #![deny(missing_docs)]
-#![cfg_attr(doc_cfg, feature(doc_cfg, doc_auto_cfg))]
+#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
 #![cfg_attr(target_os = "wasi", feature(wasi_ext))]
 #![cfg_attr(can_vector, feature(can_vector))]
 #![cfg_attr(seek_convenience, feature(seek_convenience))]
diff --git a/cap-tempfile/Cargo.toml b/cap-tempfile/Cargo.toml
index 106a2cb0..8c565689 100644
--- a/cap-tempfile/Cargo.toml
+++ b/cap-tempfile/Cargo.toml
@@ -21,7 +21,7 @@ camino = { version = "1.0.5", optional = true }
 rand = "0.8.1"
 
 [target.'cfg(not(windows))'.dependencies]
-rustix = { version = "0.37.0", features = ["procfs"] }
+rustix = { version = "0.38.0", features = ["procfs"] }
 
 [target.'cfg(windows)'.dev-dependencies.windows-sys]
 version = "0.45.0"
diff --git a/cap-tempfile/src/tempfile.rs b/cap-tempfile/src/tempfile.rs
index 4a00f4f3..ade0ee98 100644
--- a/cap-tempfile/src/tempfile.rs
+++ b/cap-tempfile/src/tempfile.rs
@@ -84,7 +84,7 @@ fn new_tempfile_linux(d: &Dir, anonymous: bool) -> io::Result<Option<File>> {
 fn generate_name_in(subdir: &Dir, f: &File) -> io::Result<String> {
     use rustix::fd::AsFd;
     use rustix::fs::AtFlags;
-    let procself_fd = rustix::io::proc_self_fd()?;
+    let procself_fd = rustix::procfs::proc_self_fd()?;
     let fdnum = rustix::path::DecInt::from_fd(&f.as_fd());
     let fdnum = fdnum.as_c_str();
     super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| {
@@ -265,7 +265,7 @@ mod test {
             let metadata = tf.as_file().metadata().unwrap();
             let mode = metadata.mode();
             let mode = Mode::from_bits_truncate(mode);
-            assert_eq!(0o666 & !umask, mode.bits());
+            assert_eq!(0o666 & !umask, mode.bits() & 0o777);
         }
         // And that we can write
         tf.write_all(b"hello world")?;
diff --git a/cap-time-ext/Cargo.toml b/cap-time-ext/Cargo.toml
index 864893a9..2e75c85e 100644
--- a/cap-time-ext/Cargo.toml
+++ b/cap-time-ext/Cargo.toml
@@ -17,7 +17,7 @@ cap-primitives = { path = "../cap-primitives", version = "^1.0.9" }
 cap-std = { path = "../cap-std", optional = true, version = "^1.0.9" }
 
 [target.'cfg(not(windows))'.dependencies]
-rustix = { version = "0.37.0", features = ["time"] }
+rustix = { version = "0.38.0", features = ["time"] }
 
 [target.'cfg(windows)'.dependencies]
 once_cell = "1.5.2"
diff --git a/tests/cap-basics.rs b/tests/cap-basics.rs
index eae9925d..b1f37f0a 100644
--- a/tests/cap-basics.rs
+++ b/tests/cap-basics.rs
@@ -217,10 +217,11 @@ fn symlink_loop_from_rename() {
     check!(tmpdir.open("link"));
 }
 
-#[cfg(linux)]
+#[cfg(target_os = "linux")]
 #[test]
 fn proc_self_fd() {
-    let fd = check!(File::open("/proc/self/fd"));
+    let fd = check!(std::fs::File::open("/proc/self/fd"));
     let dir = cap_std::fs::Dir::from_std_file(fd);
-    error!(dir.open("0"), "No such file");
+    // This should fail with "too many levels of symbolic links".
+    dir.open("0").unwrap_err();
 }
diff --git a/tests/dir-ext.rs b/tests/dir-ext.rs
index d5bf4996..0b0f5d91 100644
--- a/tests/dir-ext.rs
+++ b/tests/dir-ext.rs
@@ -39,8 +39,9 @@ fn remove_symlink_to_dir() {
     }
 
     let tempdir = TempDir::new(ambient_authority()).expect("create tempdir");
-    let target = tempdir.create_dir("target").expect("create target dir");
-    drop(target);
+    {
+        let _target = tempdir.create_dir("target").expect("create target dir");
+    }
     tempdir.symlink("target", "link").expect("create symlink");
     assert!(tempdir.exists("link"), "link exists");
     tempdir
@@ -53,8 +54,9 @@ fn remove_symlink_to_dir() {
 #[test]
 fn do_not_remove_dir() {
     let tempdir = TempDir::new(ambient_authority()).expect("create tempdir");
-    let subdir = tempdir.create_dir("subdir").expect("create dir");
-    drop(subdir);
+    {
+        let _subdir = tempdir.create_dir("subdir").expect("create dir");
+    }
     assert!(tempdir.exists("subdir"), "subdir created");
     tempdir
         .remove_file_or_symlink("subdir")
diff --git a/tests/fs.rs b/tests/fs.rs
index b51d5183..df8e332e 100644
--- a/tests/fs.rs
+++ b/tests/fs.rs
@@ -673,7 +673,6 @@ fn recursive_rmdir_toctou() {
     // `attack_dest/attack_file` is deleted.
     let tmpdir = tmpdir();
     let victim_del_path = "victim_del";
-    let victim_del_path_clone = victim_del_path.clone();
 
     // setup dest
     let attack_dest_dir = "attack_dest";
@@ -691,7 +690,7 @@ fn recursive_rmdir_toctou() {
     let tmpdir_clone = tmpdir.try_clone().unwrap();
     let _t = thread::spawn(move || {
         while drop_canary_weak.upgrade().is_some() {
-            let _ = tmpdir_clone.remove_dir_all(victim_del_path_clone);
+            let _ = tmpdir_clone.remove_dir_all(victim_del_path);
         }
     });
 
diff --git a/tests/fs_utf8.rs b/tests/fs_utf8.rs
index 06cfda28..882d726f 100644
--- a/tests/fs_utf8.rs
+++ b/tests/fs_utf8.rs
@@ -676,7 +676,6 @@ fn recursive_rmdir_toctou() {
     // `attack_dest/attack_file` is deleted.
     let tmpdir = tmpdir();
     let victim_del_path = "victim_del";
-    let victim_del_path_clone = victim_del_path.clone();
 
     // setup dest
     let attack_dest_dir = "attack_dest";
@@ -694,7 +693,7 @@ fn recursive_rmdir_toctou() {
     let tmpdir_clone = tmpdir.try_clone().unwrap();
     let _t = thread::spawn(move || {
         while drop_canary_weak.upgrade().is_some() {
-            let _ = tmpdir_clone.remove_dir_all(victim_del_path_clone);
+            let _ = tmpdir_clone.remove_dir_all(victim_del_path);
         }
     });
 
diff --git a/tests/sys/mod.rs b/tests/sys/mod.rs
index e1163bab..cc2c50f9 100644
--- a/tests/sys/mod.rs
+++ b/tests/sys/mod.rs
@@ -1,3 +1,5 @@
+#![allow(unused_imports)]
+
 #[cfg(unix)]
 mod unix;
 
diff --git a/tests/sys_common/mod.rs b/tests/sys_common/mod.rs
index fa803baf..d13e417d 100644
--- a/tests/sys_common/mod.rs
+++ b/tests/sys_common/mod.rs
@@ -1,3 +1,5 @@
+#![allow(unused_imports)]
+
 mod symlink_junction;
 
 pub mod io;