From 3e40047ad9b2e443773af0b5c1a35b22f84c5fce Mon Sep 17 00:00:00 2001 From: alysajad Date: Wed, 11 Mar 2026 16:38:56 +0530 Subject: [PATCH 1/8] fix(security): resolve TOCTOU race condition in atomic writes * Enforced --- src/credential_store.rs | 12 ------------ src/fs_util.rs | 27 +++++++++++++++++++++++++-- src/oauth_config.rs | 7 ------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/credential_store.rs b/src/credential_store.rs index a0210fc2..d697c446 100644 --- a/src/credential_store.rs +++ b/src/credential_store.rs @@ -391,18 +391,6 @@ pub fn save_encrypted(json: &str) -> anyhow::Result { crate::fs_util::atomic_write(&path, &encrypted) .map_err(|e| anyhow::anyhow!("Failed to write credentials: {e}"))?; - // Set permissions to 600 on Unix (contains secrets) - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Err(e) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) { - eprintln!( - "Warning: failed to set file permissions on {}: {e}", - path.display() - ); - } - } - Ok(path) } diff --git a/src/fs_util.rs b/src/fs_util.rs index b387565e..f86aa315 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -40,7 +40,19 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> { .map(|p| p.join(&tmp_name)) .unwrap_or_else(|| std::path::PathBuf::from(&tmp_name)); - std::fs::write(&tmp_path, data)?; + { + use std::io::Write; + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let mut file = opts.open(&tmp_path)?; + file.write_all(data)?; + file.sync_all()?; + } std::fs::rename(&tmp_path, path)?; Ok(()) } @@ -56,7 +68,18 @@ pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { .map(|p| p.join(&tmp_name)) .unwrap_or_else(|| std::path::PathBuf::from(&tmp_name)); - tokio::fs::write(&tmp_path, data).await?; + { + use tokio::io::AsyncWriteExt; + let mut opts = tokio::fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + opts.mode(0o600); + } + let mut file = opts.open(&tmp_path).await?; + file.write_all(data).await?; + file.sync_all().await?; + } tokio::fs::rename(&tmp_path, path).await?; Ok(()) } diff --git a/src/oauth_config.rs b/src/oauth_config.rs index 02154b58..89f34535 100644 --- a/src/oauth_config.rs +++ b/src/oauth_config.rs @@ -84,13 +84,6 @@ pub fn save_client_config( crate::fs_util::atomic_write(&path, json.as_bytes()) .map_err(|e| anyhow::anyhow!("Failed to write client config: {e}"))?; - // Set file permissions to 600 on Unix (contains secrets) - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?; - } - Ok(path) } From 1fc72ee066ee2ed7f222cdcc9d1fc6de3d5a9718 Mon Sep 17 00:00:00 2001 From: alysajad Date: Wed, 11 Mar 2026 16:42:01 +0530 Subject: [PATCH 2/8] docs: add changeset for TOCTOU security fix --- .changeset/resolute-toctou.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/resolute-toctou.md diff --git a/.changeset/resolute-toctou.md b/.changeset/resolute-toctou.md new file mode 100644 index 00000000..9104032d --- /dev/null +++ b/.changeset/resolute-toctou.md @@ -0,0 +1,5 @@ +--- +"gws": patch +--- + +Resolve TOCTOU race condition in `fs_util::atomic_write` and `atomic_write_async` to securely enforce 0600 file permissions upon file creation, preventing intermediate local read access to secrets. From c0329309ecdc98de4dae3e9b0ba1bbafc8d19e35 Mon Sep 17 00:00:00 2001 From: alysajad Date: Mon, 16 Mar 2026 23:03:37 +0530 Subject: [PATCH 3/8] fix: resolve TOCTOU vulnerabilities and restore defense-in-depth This commit addresses the issues raised regarding the Time-of-Check to Time-of-Use (TOCTOU) race conditions during file creation, while restoring the necessary defense-in-depth permissions logic... --- .changeset/resolute-toctou.md | 2 +- Cargo.toml | 2 +- check.txt | 33 +++++++++++++++++++ src/credential_store.rs | 12 +++++++ src/fs_util.rs | 59 ++++++++++++++++++---------------- src/oauth_config.rs | 7 ++++ test.rs | Bin 0 -> 194 bytes 7 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 check.txt create mode 100644 test.rs diff --git a/.changeset/resolute-toctou.md b/.changeset/resolute-toctou.md index 9104032d..9476d176 100644 --- a/.changeset/resolute-toctou.md +++ b/.changeset/resolute-toctou.md @@ -1,5 +1,5 @@ --- -"gws": patch +"@googleworkspace/cli": patch --- Resolve TOCTOU race condition in `fs_util::atomic_write` and `atomic_write_async` to securely enforce 0600 file permissions upon file creation, preventing intermediate local read access to secrets. diff --git a/Cargo.toml b/Cargo.toml index 44bf0235..99966a54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls-native-r rand = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" +tempfile = "3" sha2 = "0.10" thiserror = "2" tokio = { version = "1", features = ["full"] } @@ -66,4 +67,3 @@ lto = "thin" [dev-dependencies] serial_test = "3.4.0" -tempfile = "3" diff --git a/check.txt b/check.txt new file mode 100644 index 00000000..95974dd8 --- /dev/null +++ b/check.txt @@ -0,0 +1,33 @@ + Updating crates.io index + Locking 9 packages to latest compatible versions + Adding aws-lc-rs v1.16.1 + Adding aws-lc-sys v0.38.0 + Adding cmake v0.1.57 + Adding dunce v1.0.5 + Adding fs_extra v1.3.0 + Adding hyper-rustls v0.27.7 + Adding jobserver v0.1.34 + Adding rustls-native-certs v0.8.3 + Adding tokio-rustls v0.26.4 + Downloading crates ... + Downloaded aws-lc-rs v1.16.1 + Downloaded fs_extra v1.3.0 + Downloaded jobserver v0.1.34 + Downloaded cmake v0.1.57 + Downloaded dunce v1.0.5 + Downloaded aws-lc-sys v0.38.0 + Compiling getrandom v0.3.4 + Compiling cfg-if v1.0.4 + Compiling fs_extra v1.3.0 + Compiling dunce v1.0.5 + Compiling aws-lc-rs v1.16.1 + Compiling rustls v0.23.37 + Checking rustls-native-certs v0.8.3 + Compiling jobserver v0.1.34 +error: dlltool could not create import library with C:\Program Files (x86)\mingw-w64\MinGW\bin\dlltool.exe -d C:\Users\alisa\OneDrive\Desktop\cli\cli\target\debug\deps\rustcmJtPu6\bcryptprimitives.dll_imports.def -D bcryptprimitives.dll -l C:\Users\alisa\OneDrive\Desktop\cli\cli\target\debug\deps\rustcmJtPu6\bcryptprimitives.dll_imports.lib -m i386:x86-64 -f --64 --no-leading-underscore --temp-prefix C:\Users\alisa\OneDrive\Desktop\cli\cli\target\debug\deps\rustcmJtPu6\bcryptprimitives.dll: + + C:\Program Files (x86)\mingw-w64\MinGW\bin\dlltool.exe: Can't create .lib file: C:\Users\alisa\OneDrive\Desktop\cli\cli\target\debug\deps\rustcmJtPu6\bcryptprimitives.dll_imports.lib: Invalid bfd target␍ + + +error: could not compile `getrandom` (lib) due to 1 previous error +warning: build failed, waiting for other jobs to finish... diff --git a/src/credential_store.rs b/src/credential_store.rs index d697c446..a0210fc2 100644 --- a/src/credential_store.rs +++ b/src/credential_store.rs @@ -391,6 +391,18 @@ pub fn save_encrypted(json: &str) -> anyhow::Result { crate::fs_util::atomic_write(&path, &encrypted) .map_err(|e| anyhow::anyhow!("Failed to write credentials: {e}"))?; + // Set permissions to 600 on Unix (contains secrets) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Err(e) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) { + eprintln!( + "Warning: failed to set file permissions on {}: {e}", + path.display() + ); + } + } + Ok(path) } diff --git a/src/fs_util.rs b/src/fs_util.rs index f86aa315..89beba1d 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -30,57 +30,62 @@ use std::path::Path; /// Returns an `io::Error` if the temporary file cannot be written or if the /// rename fails. pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> { - // Derive a sibling tmp path, e.g. `/home/user/.config/gws/credentials.enc.tmp` - let file_name = path - .file_name() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?; - let tmp_name = format!("{}.tmp", file_name.to_string_lossy()); - let tmp_path = path - .parent() - .map(|p| p.join(&tmp_name)) - .unwrap_or_else(|| std::path::PathBuf::from(&tmp_name)); + let parent = path.parent().unwrap_or_else(|| std::path::Path::new("")); + let mut tmp_file = tempfile::Builder::new() + .prefix(".tmp") + .make_in(parent)?; { use std::io::Write; - let mut opts = std::fs::OpenOptions::new(); - opts.write(true).create(true).truncate(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - opts.mode(0o600); - } - let mut file = opts.open(&tmp_path)?; - file.write_all(data)?; - file.sync_all()?; + tmp_file.write_all(data)?; + tmp_file.as_file_mut().sync_all()?; } - std::fs::rename(&tmp_path, path)?; + + tmp_file.persist(path).map_err(|e| e.error)?; Ok(()) } +struct CleanupTmp(std::path::PathBuf); + +impl Drop for CleanupTmp { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} + /// Async variant of [`atomic_write`] for use with tokio. pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { + let parent = path.parent().unwrap_or_else(|| std::path::Path::new("")); + + // Generate a random suffix for the temporary file + let suffix: String = std::iter::repeat_with(rand::random::) + .filter(|c| c.is_ascii_alphanumeric()) + .take(8) + .collect(); + let file_name = path .file_name() .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?; - let tmp_name = format!("{}.tmp", file_name.to_string_lossy()); - let tmp_path = path - .parent() - .map(|p| p.join(&tmp_name)) - .unwrap_or_else(|| std::path::PathBuf::from(&tmp_name)); + let tmp_name = format!("{}.{}.tmp", file_name.to_string_lossy(), suffix); + let tmp_path = parent.join(&tmp_name); + + let cleanup = CleanupTmp(tmp_path.clone()); { use tokio::io::AsyncWriteExt; let mut opts = tokio::fs::OpenOptions::new(); - opts.write(true).create(true).truncate(true); + opts.write(true).create_new(true); // O_EXCL to prevent TOCTOU #[cfg(unix)] { - opts.mode(0o600); + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); // Only applies to new files (which this is, due to create_new) } let mut file = opts.open(&tmp_path).await?; file.write_all(data).await?; file.sync_all().await?; } tokio::fs::rename(&tmp_path, path).await?; + std::mem::forget(cleanup); Ok(()) } diff --git a/src/oauth_config.rs b/src/oauth_config.rs index 89f34535..02154b58 100644 --- a/src/oauth_config.rs +++ b/src/oauth_config.rs @@ -84,6 +84,13 @@ pub fn save_client_config( crate::fs_util::atomic_write(&path, json.as_bytes()) .map_err(|e| anyhow::anyhow!("Failed to write client config: {e}"))?; + // Set file permissions to 600 on Unix (contains secrets) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?; + } + Ok(path) } diff --git a/test.rs b/test.rs new file mode 100644 index 0000000000000000000000000000000000000000..3fbd7f4942a5608305e002bb5d90dd552eba74d6 GIT binary patch literal 194 zcmZvVu?hk)5JX=s_zx-1My|R-@f-XGK@a3K5zJvB{<`|IU?*XBHZ!xcpZCn26NyS; z;%wjeM%jjYZmK!3hMHBj`bXKsm4j@iW99E@UG&#b=D=b?btkd2{^zwz*}X^PwDjNB U+6#rFT1W3R*n4j^&x<7szCFw# Date: Mon, 16 Mar 2026 23:07:47 +0530 Subject: [PATCH 4/8] chore: remove accidental test files --- check.txt | 33 --------------------------------- test.rs | Bin 194 -> 0 bytes 2 files changed, 33 deletions(-) delete mode 100644 check.txt delete mode 100644 test.rs diff --git a/check.txt b/check.txt deleted file mode 100644 index 95974dd8..00000000 --- a/check.txt +++ /dev/null @@ -1,33 +0,0 @@ - Updating crates.io index - Locking 9 packages to latest compatible versions - Adding aws-lc-rs v1.16.1 - Adding aws-lc-sys v0.38.0 - Adding cmake v0.1.57 - Adding dunce v1.0.5 - Adding fs_extra v1.3.0 - Adding hyper-rustls v0.27.7 - Adding jobserver v0.1.34 - Adding rustls-native-certs v0.8.3 - Adding tokio-rustls v0.26.4 - Downloading crates ... - Downloaded aws-lc-rs v1.16.1 - Downloaded fs_extra v1.3.0 - Downloaded jobserver v0.1.34 - Downloaded cmake v0.1.57 - Downloaded dunce v1.0.5 - Downloaded aws-lc-sys v0.38.0 - Compiling getrandom v0.3.4 - Compiling cfg-if v1.0.4 - Compiling fs_extra v1.3.0 - Compiling dunce v1.0.5 - Compiling aws-lc-rs v1.16.1 - Compiling rustls v0.23.37 - Checking rustls-native-certs v0.8.3 - Compiling jobserver v0.1.34 -error: dlltool could not create import library with C:\Program Files (x86)\mingw-w64\MinGW\bin\dlltool.exe -d C:\Users\alisa\OneDrive\Desktop\cli\cli\target\debug\deps\rustcmJtPu6\bcryptprimitives.dll_imports.def -D bcryptprimitives.dll -l C:\Users\alisa\OneDrive\Desktop\cli\cli\target\debug\deps\rustcmJtPu6\bcryptprimitives.dll_imports.lib -m i386:x86-64 -f --64 --no-leading-underscore --temp-prefix C:\Users\alisa\OneDrive\Desktop\cli\cli\target\debug\deps\rustcmJtPu6\bcryptprimitives.dll: - - C:\Program Files (x86)\mingw-w64\MinGW\bin\dlltool.exe: Can't create .lib file: C:\Users\alisa\OneDrive\Desktop\cli\cli\target\debug\deps\rustcmJtPu6\bcryptprimitives.dll_imports.lib: Invalid bfd target␍ - - -error: could not compile `getrandom` (lib) due to 1 previous error -warning: build failed, waiting for other jobs to finish... diff --git a/test.rs b/test.rs deleted file mode 100644 index 3fbd7f4942a5608305e002bb5d90dd552eba74d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194 zcmZvVu?hk)5JX=s_zx-1My|R-@f-XGK@a3K5zJvB{<`|IU?*XBHZ!xcpZCn26NyS; z;%wjeM%jjYZmK!3hMHBj`bXKsm4j@iW99E@UG&#b=D=b?btkd2{^zwz*}X^PwDjNB U+6#rFT1W3R*n4j^&x<7szCFw# Date: Mon, 16 Mar 2026 23:10:02 +0530 Subject: [PATCH 5/8] fix: refactor atomic_write_async cleanup to be async-native Using a blocking Drop implementation in an async context (for temporary file cleanup) stalls the tokio executor thread. This commit refactors the function to explicitly handle error paths using tokio::fs::remove_file which properly awaits file unlinking. --- src/fs_util.rs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/fs_util.rs b/src/fs_util.rs index 89beba1d..d717dbb5 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -45,14 +45,6 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> { Ok(()) } -struct CleanupTmp(std::path::PathBuf); - -impl Drop for CleanupTmp { - fn drop(&mut self) { - let _ = std::fs::remove_file(&self.0); - } -} - /// Async variant of [`atomic_write`] for use with tokio. pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { let parent = path.parent().unwrap_or_else(|| std::path::Path::new("")); @@ -69,9 +61,7 @@ pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { let tmp_name = format!("{}.{}.tmp", file_name.to_string_lossy(), suffix); let tmp_path = parent.join(&tmp_name); - let cleanup = CleanupTmp(tmp_path.clone()); - - { + let write_result = async { use tokio::io::AsyncWriteExt; let mut opts = tokio::fs::OpenOptions::new(); opts.write(true).create_new(true); // O_EXCL to prevent TOCTOU @@ -83,10 +73,21 @@ pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { let mut file = opts.open(&tmp_path).await?; file.write_all(data).await?; file.sync_all().await?; + Ok::<(), io::Error>(()) + }.await; + + if write_result.is_err() { + let _ = tokio::fs::remove_file(&tmp_path).await; + return write_result; + } + + match tokio::fs::rename(&tmp_path, path).await { + Ok(()) => Ok(()), + Err(e) => { + let _ = tokio::fs::remove_file(&tmp_path).await; + Err(e) + } } - tokio::fs::rename(&tmp_path, path).await?; - std::mem::forget(cleanup); - Ok(()) } #[cfg(test)] From ab78f8310426652243c199326a15a6272914e056 Mon Sep 17 00:00:00 2001 From: alysajad Date: Mon, 16 Mar 2026 23:14:01 +0530 Subject: [PATCH 6/8] perf: improve random temp string generation efficiency Using rand::random::() over thousands of scalar iterations to find 8 ASCII alphanumeric characters is heavily inefficient. This commit refactors the atomic_write_async string generator to use rand::distributions::Alphanumeric directly for optimal sampling. --- src/fs_util.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fs_util.rs b/src/fs_util.rs index d717dbb5..8f345f55 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -50,9 +50,11 @@ pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { let parent = path.parent().unwrap_or_else(|| std::path::Path::new("")); // Generate a random suffix for the temporary file - let suffix: String = std::iter::repeat_with(rand::random::) - .filter(|c| c.is_ascii_alphanumeric()) + use rand::Rng; + let suffix: String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) .take(8) + .map(char::from) .collect(); let file_name = path From c073fff42058d040706b2ca777541dda82fa38b8 Mon Sep 17 00:00:00 2001 From: alysajad Date: Mon, 16 Mar 2026 23:19:27 +0530 Subject: [PATCH 7/8] refactor: simplify atomic_write_async control flow This refactors atomic_write_async to match the cleaner structure suggested by the Code Assist bot, wrapping the operations in an async block and evaluating the result. --- src/fs_util.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/fs_util.rs b/src/fs_util.rs index 8f345f55..bad7c2a1 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -63,7 +63,7 @@ pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { let tmp_name = format!("{}.{}.tmp", file_name.to_string_lossy(), suffix); let tmp_path = parent.join(&tmp_name); - let write_result = async { + let write_and_rename = async { use tokio::io::AsyncWriteExt; let mut opts = tokio::fs::OpenOptions::new(); opts.write(true).create_new(true); // O_EXCL to prevent TOCTOU @@ -75,17 +75,13 @@ pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { let mut file = opts.open(&tmp_path).await?; file.write_all(data).await?; file.sync_all().await?; - Ok::<(), io::Error>(()) - }.await; + tokio::fs::rename(&tmp_path, path).await + }; - if write_result.is_err() { - let _ = tokio::fs::remove_file(&tmp_path).await; - return write_result; - } - - match tokio::fs::rename(&tmp_path, path).await { - Ok(()) => Ok(()), + match write_and_rename.await { + Ok(_) => Ok(()), Err(e) => { + // On any error, try to clean up the temp file. let _ = tokio::fs::remove_file(&tmp_path).await; Err(e) } From e0ab14b1ab77600a0da8a8a37cbf62f038a5eaa4 Mon Sep 17 00:00:00 2001 From: alysajad Date: Mon, 16 Mar 2026 23:24:53 +0530 Subject: [PATCH 8/8] refactor: simplify atomic_write_async using spawn_blocking This addresses the bot's suggestion to eliminate duplicate security logic. Rather than manually implementing secure tempfile generation and random naming twice, atomic_write_async now neatly wraps the battle-tested synchronous atomic_write inside tokio::task::spawn_blocking. This reduces cognitive overhead and centralizes security. --- src/fs_util.rs | 44 +++++--------------------------------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/src/fs_util.rs b/src/fs_util.rs index bad7c2a1..560cfa97 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -47,45 +47,11 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> { /// Async variant of [`atomic_write`] for use with tokio. pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { - let parent = path.parent().unwrap_or_else(|| std::path::Path::new("")); - - // Generate a random suffix for the temporary file - use rand::Rng; - let suffix: String = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(8) - .map(char::from) - .collect(); - - let file_name = path - .file_name() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?; - let tmp_name = format!("{}.{}.tmp", file_name.to_string_lossy(), suffix); - let tmp_path = parent.join(&tmp_name); - - let write_and_rename = async { - use tokio::io::AsyncWriteExt; - let mut opts = tokio::fs::OpenOptions::new(); - opts.write(true).create_new(true); // O_EXCL to prevent TOCTOU - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - opts.mode(0o600); // Only applies to new files (which this is, due to create_new) - } - let mut file = opts.open(&tmp_path).await?; - file.write_all(data).await?; - file.sync_all().await?; - tokio::fs::rename(&tmp_path, path).await - }; - - match write_and_rename.await { - Ok(_) => Ok(()), - Err(e) => { - // On any error, try to clean up the temp file. - let _ = tokio::fs::remove_file(&tmp_path).await; - Err(e) - } - } + let path = path.to_path_buf(); + let data = data.to_vec(); + tokio::task::spawn_blocking(move || atomic_write(&path, &data)) + .await + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))? } #[cfg(test)]