Skip to content

Commit 249a5cc

Browse files
committed
dirext: rework permissions handling in atomic_*
Signed-off-by: Etienne Champetier <[email protected]>
1 parent 677efca commit 249a5cc

File tree

2 files changed

+69
-44
lines changed

2 files changed

+69
-44
lines changed

src/dirext.rs

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//!
99
//! [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
1010
11-
use cap_primitives::fs::FileType;
11+
use cap_primitives::fs::{FileType, PermissionsExt};
1212
use cap_std::fs::{Dir, File, Metadata};
1313
use cap_tempfile::cap_std;
1414
use cap_tempfile::cap_std::fs::DirEntry;
@@ -172,25 +172,26 @@ pub trait CapStdExtDirExt {
172172
/// require a higher level wrapper which queries the existing file and gathers such metadata
173173
/// before replacement.
174174
///
175-
/// # Example, including setting permissions
175+
/// # Security
176176
///
177-
/// The closure may also perform other file operations beyond writing, such as changing
178-
/// file permissions:
177+
/// On filesystems that do not support `O_TMPFILE` (e.g. `vfat`), the TempFile is first created
178+
/// with default permissions (mode 0o666 & ~umask) that can be readeable by everyone.
179+
/// If this is a concern, you must set a more restrictive umask for your program.
180+
///
181+
/// # Example
179182
///
180183
/// ```rust
181184
/// # use std::io;
182185
/// # use std::io::Write;
183186
/// # use cap_tempfile::cap_std;
187+
/// # use cap_std::fs::PermissionsExt;
184188
/// # fn main() -> io::Result<()> {
185189
/// # let somedir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
186190
/// use cap_std_ext::prelude::*;
187191
/// let contents = b"hello world\n";
188-
/// somedir.atomic_replace_with("somefilename", |f| -> io::Result<_> {
192+
/// let perms = cap_std::fs::Permissions::from_mode(0o600);
193+
/// somedir.atomic_replace_with("somefilename", Some(perms), |f| -> io::Result<_> {
189194
/// f.write_all(contents)?;
190-
/// f.flush()?;
191-
/// use cap_std::fs::PermissionsExt;
192-
/// let perms = cap_std::fs::Permissions::from_mode(0o600);
193-
/// f.get_mut().as_file_mut().set_permissions(perms)?;
194195
/// Ok(())
195196
/// })
196197
/// # }
@@ -200,6 +201,7 @@ pub trait CapStdExtDirExt {
200201
fn atomic_replace_with<F, T, E>(
201202
&self,
202203
destname: impl AsRef<Path>,
204+
perms: Option<cap_std::fs::Permissions>,
203205
f: F,
204206
) -> std::result::Result<T, E>
205207
where
@@ -318,26 +320,26 @@ pub trait CapStdExtDirExtUtf8 {
318320
/// require a higher level wrapper which queries the existing file and gathers such metadata
319321
/// before replacement.
320322
///
321-
/// # Example, including setting permissions
323+
/// # Security
324+
///
325+
/// On filesystems that do not support `O_TMPFILE` (e.g. `vfat`), the TempFile is first created
326+
/// with default permissions (mode 0o666 & ~umask) that can be readeable by everyone.
327+
/// If this is a concern, you must set a more restrictive umask for your program.
322328
///
323-
/// The closure may also perform other file operations beyond writing, such as changing
324-
/// file permissions:
329+
/// # Example
325330
///
326331
/// ```rust
327332
/// # use std::io;
328333
/// # use std::io::Write;
329334
/// # use cap_tempfile::cap_std;
335+
/// # use cap_std::fs::PermissionsExt;
330336
/// # fn main() -> io::Result<()> {
331337
/// # let somedir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
332-
/// # let somedir = cap_std::fs_utf8::Dir::from_cap_std((&*somedir).try_clone()?);
333338
/// use cap_std_ext::prelude::*;
334339
/// let contents = b"hello world\n";
335-
/// somedir.atomic_replace_with("somefilename", |f| -> io::Result<_> {
340+
/// let perms = cap_std::fs::Permissions::from_mode(0o600);
341+
/// somedir.atomic_replace_with("somefilename", Some(perms), |f| -> io::Result<_> {
336342
/// f.write_all(contents)?;
337-
/// f.flush()?;
338-
/// use cap_std::fs::PermissionsExt;
339-
/// let perms = cap_std::fs::Permissions::from_mode(0o600);
340-
/// f.get_mut().as_file_mut().set_permissions(perms)?;
341343
/// Ok(())
342344
/// })
343345
/// # }
@@ -347,6 +349,7 @@ pub trait CapStdExtDirExtUtf8 {
347349
fn atomic_replace_with<F, T, E>(
348350
&self,
349351
destname: impl AsRef<Utf8Path>,
352+
perms: Option<cap_std::fs::Permissions>,
350353
f: F,
351354
) -> std::result::Result<T, E>
352355
where
@@ -710,6 +713,7 @@ impl CapStdExtDirExt for Dir {
710713
fn atomic_replace_with<F, T, E>(
711714
&self,
712715
destname: impl AsRef<Path>,
716+
perms: Option<cap_std::fs::Permissions>,
713717
f: F,
714718
) -> std::result::Result<T, E>
715719
where
@@ -718,23 +722,45 @@ impl CapStdExtDirExt for Dir {
718722
{
719723
let destname = destname.as_ref();
720724
let (d, name) = subdir_of(self, destname)?;
721-
let existing_metadata = d.symlink_metadata_optional(destname)?;
722-
// If the target is already a file, then acquire its mode, which we will preserve by default.
723-
// We don't follow symlinks here for replacement, and so we definitely don't want to pick up its mode.
724-
let existing_perms = existing_metadata
725-
.filter(|m| m.is_file())
726-
.map(|m| m.permissions());
725+
727726
let mut t = cap_tempfile::TempFile::new(&d)?;
728-
// Apply the permissions, if we have them
729-
if let Some(existing_perms) = existing_perms {
730-
t.as_file_mut().set_permissions(existing_perms)?;
727+
#[cfg(unix)]
728+
let default_perms = t.as_file().metadata()?.permissions();
729+
// cap_tempfile doesn't support passing permissions on creation yet.
730+
// https://github.com/bytecodealliance/cap-std/pull/390
731+
#[cfg(unix)]
732+
{
733+
let zeroperms = cap_std::fs::PermissionsExt::from_mode(0o000);
734+
t.as_file().set_permissions(zeroperms)?;
731735
}
736+
732737
// We always operate in terms of buffered writes
733738
let mut bufw = std::io::BufWriter::new(t);
734739
// Call the provided closure to generate the file content
735740
let r = f(&mut bufw)?;
736741
// Flush the buffer, get the TempFile
737742
t = bufw.into_inner().map_err(From::from)?;
743+
// Set the file permissions
744+
// This must be done after flushing the application buffer else Linux clears the security related bits:
745+
// https://github.com/torvalds/linux/blob/94305e83eccb3120c921cd3a015cd74731140bac/fs/inode.c#L2360
746+
if let Some(perms) = perms {
747+
t.as_file().set_permissions(perms)?;
748+
} else {
749+
let existing_metadata = d.symlink_metadata_optional(destname)?;
750+
// If the target is already a file, then acquire its mode, which we will preserve by default.
751+
// We don't follow symlinks here for replacement, and so we definitely don't want to pick up its mode.
752+
let existing_perms = existing_metadata
753+
.filter(|m| m.is_file())
754+
.map(|m| m.permissions());
755+
if let Some(existing_perms) = existing_perms {
756+
// Apply the existing permissions, if we have them
757+
t.as_file().set_permissions(existing_perms)?;
758+
} else {
759+
// Else restore default permissions
760+
#[cfg(unix)]
761+
t.as_file().set_permissions(default_perms)?;
762+
}
763+
}
738764
// fsync the TempFile
739765
t.as_file().sync_all()?;
740766
// rename the TempFile
@@ -745,7 +771,7 @@ impl CapStdExtDirExt for Dir {
745771
}
746772

747773
fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
748-
self.atomic_replace_with(destname, |f| f.write_all(contents.as_ref()))
774+
self.atomic_replace_with(destname, None, |f| f.write_all(contents.as_ref()))
749775
}
750776

751777
fn atomic_write_with_perms(
@@ -754,19 +780,8 @@ impl CapStdExtDirExt for Dir {
754780
contents: impl AsRef<[u8]>,
755781
perms: cap_std::fs::Permissions,
756782
) -> Result<()> {
757-
self.atomic_replace_with(destname, |f| -> io::Result<_> {
758-
// If the user is overriding the permissions, let's make the default be
759-
// writable by us but not readable by anyone else, in case it has
760-
// secret data.
761-
#[cfg(unix)]
762-
{
763-
use cap_std::fs::PermissionsExt;
764-
let perms = cap_std::fs::Permissions::from_mode(0o600);
765-
f.get_mut().as_file_mut().set_permissions(perms)?;
766-
}
783+
self.atomic_replace_with(destname, Some(perms), |f| -> io::Result<_> {
767784
f.write_all(contents.as_ref())?;
768-
f.flush()?;
769-
f.get_mut().as_file_mut().set_permissions(perms)?;
770785
Ok(())
771786
})
772787
}

tests/it/main.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,21 +164,21 @@ fn default_mode(d: &Dir) -> Result<Permissions> {
164164
fn link_tempfile_with() -> Result<()> {
165165
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;
166166
let p = Path::new("foo");
167-
td.atomic_replace_with(p, |f| writeln!(f, "hello world"))
167+
td.atomic_replace_with(p, None, |f| writeln!(f, "hello world"))
168168
.unwrap();
169169
assert_eq!(td.read_to_string(p).unwrap().as_str(), "hello world\n");
170170
let default_perms = default_mode(&td)?;
171171
assert_eq!(td.metadata(p)?.permissions(), default_perms);
172172

173-
td.atomic_replace_with(p, |f| writeln!(f, "atomic replacement"))
173+
td.atomic_replace_with(p, None, |f| writeln!(f, "atomic replacement"))
174174
.unwrap();
175175
assert_eq!(
176176
td.read_to_string(p).unwrap().as_str(),
177177
"atomic replacement\n"
178178
);
179179

180180
let e = td
181-
.atomic_replace_with(p, |f| {
181+
.atomic_replace_with(p, None, |f| {
182182
writeln!(f, "should not be written")?;
183183
Err::<(), _>(std::io::Error::new(std::io::ErrorKind::Other, "oops"))
184184
})
@@ -238,14 +238,24 @@ fn link_tempfile_with() -> Result<()> {
238238
td.atomic_write_with_perms(p, "self-only file v3", Permissions::from_mode(0o640))
239239
.unwrap();
240240
assert_eq!(td.metadata(p).unwrap().permissions().mode() & 0o777, 0o640);
241+
// Write a file with mode 000
242+
td.atomic_write_with_perms(p, "no permissions", Permissions::from_mode(0o000))
243+
.unwrap();
244+
let metadata = td.metadata(p).unwrap();
245+
assert_eq!(metadata.permissions().mode() & 0o777, 0o000);
246+
assert_eq!(metadata.len(), 14);
247+
// Replace a file with mode 000
248+
td.atomic_write_with_perms(p, "600", Permissions::from_mode(0o600))
249+
.unwrap();
250+
assert_eq!(td.metadata(p).unwrap().permissions().mode() & 0o777, 0o600);
241251
Ok(())
242252
}
243253

244254
#[test]
245255
fn test_timestamps() -> Result<()> {
246256
let td = cap_tempfile::tempdir(cap_std::ambient_authority())?;
247257
let p = Path::new("foo");
248-
td.atomic_replace_with(p, |f| writeln!(f, "hello world"))
258+
td.atomic_replace_with(p, None, |f| writeln!(f, "hello world"))
249259
.unwrap();
250260
let ts0 = td.metadata(p)?.modified()?;
251261
// This test assumes at least second granularity on filesystem timestamps, and

0 commit comments

Comments
 (0)