Skip to content

Commit

Permalink
fs: Two fixes for set_file_contents
Browse files Browse the repository at this point in the history
I was just skimming this code and noticed
we were incorrectly using `write`.

I'd like to reopen that conversation about using
cap-std and especially cap-std-ext, which
would have fixed several of these bugs
with our nice high level helper API like

https://docs.rs/cap-std-ext/latest/cap_std_ext/dirext/trait.CapStdExtDirExt.html#tymethod.atomic_replace_with

Anyways for now I just fixed the implementation here.

- Add a doc comment
- Add O_CLOEXEC
- Use `write_all` instead of a single `write` (!)
- Add a test

(Also a different bug, this function takes a `Stat` but
 ignores everything except st_mode, but we can fix that later)

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters authored and allisonkarlitskaya committed Dec 9, 2024
1 parent 873b82d commit 55ae2e9
Showing 1 changed file with 45 additions and 11 deletions.
56 changes: 45 additions & 11 deletions src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use std::{
cell::RefCell,
cmp::max,
collections::{BTreeMap, HashMap},
ffi::OsString,
ffi::{CStr, OsStr},
ffi::{CStr, OsStr, OsString},
fs::File,
io::Write,
mem::MaybeUninit,
os::unix::ffi::{OsStrExt, OsStringExt},
path::Path,
Expand All @@ -14,10 +15,10 @@ use anyhow::{ensure, Result};
use rustix::{
fd::{AsFd, OwnedFd},
fs::{
fdatasync, fstat, getxattr, linkat, listxattr, mkdirat, mknodat, openat, readlinkat,
symlinkat, AtFlags, Dir, FileType, Mode, OFlags, CWD,
fstat, getxattr, linkat, listxattr, mkdirat, mknodat, openat, readlinkat, symlinkat,
AtFlags, Dir, FileType, Mode, OFlags, CWD,
},
io::{read_uninit, write, Errno},
io::{read_uninit, Errno},
};
use zerocopy::IntoBytes;

Expand All @@ -30,16 +31,19 @@ use crate::{
INLINE_CONTENT_MAX,
};

/// Attempt to use O_TMPFILE + rename to atomically set file contents.
/// Will fall back to a non-atomic write if the target doesn't support O_TMPFILE.
fn set_file_contents(dirfd: &OwnedFd, name: &OsStr, stat: &Stat, data: &[u8]) -> Result<()> {
match openat(
dirfd,
".",
OFlags::WRONLY | OFlags::TMPFILE,
OFlags::WRONLY | OFlags::TMPFILE | OFlags::CLOEXEC,
stat.st_mode.into(),
) {
Ok(tmp) => {
write(&tmp, data)?; // TODO: make this better
fdatasync(&tmp)?;
let mut tmp = File::from(tmp);
tmp.write_all(data)?;
tmp.sync_data()?;
linkat(
CWD,
proc_self_fd(&tmp),
Expand All @@ -53,11 +57,12 @@ fn set_file_contents(dirfd: &OwnedFd, name: &OsStr, stat: &Stat, data: &[u8]) ->
let fd = openat(
dirfd,
name,
OFlags::CREATE | OFlags::WRONLY,
OFlags::CREATE | OFlags::WRONLY | OFlags::CLOEXEC,
stat.st_mode.into(),
)?;
write(&fd, data)?;
fdatasync(&fd)?;
let mut f = File::from(fd);
f.write_all(data)?;
f.sync_data()?;
}
Err(e) => Err(e)?,
}
Expand Down Expand Up @@ -321,3 +326,32 @@ pub fn create_dumpfile(path: &Path) -> Result<()> {
let fs = read_from_path(path, None)?;
super::dumpfile::write_dumpfile(&mut std::io::stdout(), &fs)
}

#[cfg(test)]
mod tests {
use super::*;
use rustix::fs::{openat, CWD};

#[test]
fn test_write_contents() -> Result<()> {
let td = tempfile::tempdir()?;
let testpath = &td.path().join("testfile");
let td = openat(
CWD,
td.path(),
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::from_raw_mode(0),
)?;
let st = Stat {
st_mode: 0o755,
st_uid: 0,
st_gid: 0,
st_mtim_sec: Default::default(),
xattrs: Default::default(),
};
set_file_contents(&td, OsStr::new("testfile"), &st, b"new contents").unwrap();
drop(td);
assert_eq!(std::fs::read(testpath)?, b"new contents");
Ok(())
}
}

0 comments on commit 55ae2e9

Please sign in to comment.