Skip to content

Commit 07f0aaf

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

File tree

2 files changed

+72
-43
lines changed

2 files changed

+72
-43
lines changed

src/dirext.rs

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
//! [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
1010
1111
use cap_primitives::fs::FileType;
12+
#[cfg(unix)]
13+
use cap_primitives::fs::MetadataExt;
1214
use cap_std::fs::{Dir, File, Metadata};
1315
use cap_tempfile::cap_std;
1416
use cap_tempfile::cap_std::fs::DirEntry;
@@ -172,25 +174,26 @@ pub trait CapStdExtDirExt {
172174
/// require a higher level wrapper which queries the existing file and gathers such metadata
173175
/// before replacement.
174176
///
175-
/// # Example, including setting permissions
177+
/// # Security
176178
///
177-
/// The closure may also perform other file operations beyond writing, such as changing
178-
/// file permissions:
179+
/// On filesystems that do not support `O_TMPFILE` (e.g. `vfat`), the TempFile is first created
180+
/// with default permissions (mode 0o666 & ~umask) that can be readeable by everyone.
181+
/// If this is a concern, you must set a more restrictive umask for your program.
182+
///
183+
/// # Example
179184
///
180185
/// ```rust
181186
/// # use std::io;
182187
/// # use std::io::Write;
183188
/// # use cap_tempfile::cap_std;
189+
/// # use cap_std::fs::PermissionsExt;
184190
/// # fn main() -> io::Result<()> {
185191
/// # let somedir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
186192
/// use cap_std_ext::prelude::*;
187193
/// let contents = b"hello world\n";
188-
/// somedir.atomic_replace_with("somefilename", |f| -> io::Result<_> {
194+
/// let perms = cap_std::fs::Permissions::from_mode(0o600);
195+
/// somedir.atomic_replace_with("somefilename", Some(perms), |f| -> io::Result<_> {
189196
/// 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)?;
194197
/// Ok(())
195198
/// })
196199
/// # }
@@ -200,6 +203,7 @@ pub trait CapStdExtDirExt {
200203
fn atomic_replace_with<F, T, E>(
201204
&self,
202205
destname: impl AsRef<Path>,
206+
perms: Option<cap_std::fs::Permissions>,
203207
f: F,
204208
) -> std::result::Result<T, E>
205209
where
@@ -318,26 +322,26 @@ pub trait CapStdExtDirExtUtf8 {
318322
/// require a higher level wrapper which queries the existing file and gathers such metadata
319323
/// before replacement.
320324
///
321-
/// # Example, including setting permissions
325+
/// # Security
326+
///
327+
/// On filesystems that do not support `O_TMPFILE` (e.g. `vfat`), the TempFile is first created
328+
/// with default permissions (mode 0o666 & ~umask) that can be readeable by everyone.
329+
/// If this is a concern, you must set a more restrictive umask for your program.
322330
///
323-
/// The closure may also perform other file operations beyond writing, such as changing
324-
/// file permissions:
331+
/// # Example
325332
///
326333
/// ```rust
327334
/// # use std::io;
328335
/// # use std::io::Write;
329336
/// # use cap_tempfile::cap_std;
337+
/// # use cap_std::fs::PermissionsExt;
330338
/// # fn main() -> io::Result<()> {
331339
/// # let somedir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
332-
/// # let somedir = cap_std::fs_utf8::Dir::from_cap_std((&*somedir).try_clone()?);
333340
/// use cap_std_ext::prelude::*;
334341
/// let contents = b"hello world\n";
335-
/// somedir.atomic_replace_with("somefilename", |f| -> io::Result<_> {
342+
/// let perms = cap_std::fs::Permissions::from_mode(0o600);
343+
/// somedir.atomic_replace_with("somefilename", Some(perms), |f| -> io::Result<_> {
336344
/// 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)?;
341345
/// Ok(())
342346
/// })
343347
/// # }
@@ -347,6 +351,7 @@ pub trait CapStdExtDirExtUtf8 {
347351
fn atomic_replace_with<F, T, E>(
348352
&self,
349353
destname: impl AsRef<Utf8Path>,
354+
perms: Option<cap_std::fs::Permissions>,
350355
f: F,
351356
) -> std::result::Result<T, E>
352357
where
@@ -710,6 +715,7 @@ impl CapStdExtDirExt for Dir {
710715
fn atomic_replace_with<F, T, E>(
711716
&self,
712717
destname: impl AsRef<Path>,
718+
perms: Option<cap_std::fs::Permissions>,
713719
f: F,
714720
) -> std::result::Result<T, E>
715721
where
@@ -718,23 +724,47 @@ impl CapStdExtDirExt for Dir {
718724
{
719725
let destname = destname.as_ref();
720726
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());
727+
728+
// cap_tempfile doesn't support passing permissions on creation.
729+
// https://github.com/bytecodealliance/cap-std/pull/390
727730
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)?;
731+
#[cfg(unix)]
732+
let t_metadata = t.as_file().metadata()?;
733+
#[cfg(unix)]
734+
if t_metadata.nlink() > 0 {
735+
let zeroperms = cap_std::fs::PermissionsExt::from_mode(0o000);
736+
t.as_file().set_permissions(zeroperms)?;
731737
}
738+
732739
// We always operate in terms of buffered writes
733740
let mut bufw = std::io::BufWriter::new(t);
734741
// Call the provided closure to generate the file content
735742
let r = f(&mut bufw)?;
736743
// Flush the buffer, get the TempFile
737744
t = bufw.into_inner().map_err(From::from)?;
745+
// Set the file permissions
746+
// This must be done after flushing the application buffer else Linux clears the security related bits:
747+
// https://github.com/torvalds/linux/blob/94305e83eccb3120c921cd3a015cd74731140bac/fs/inode.c#L2360
748+
if let Some(perms) = perms {
749+
t.as_file().set_permissions(perms)?;
750+
} else {
751+
let existing_metadata = d.symlink_metadata_optional(destname)?;
752+
// If the target is already a file, then acquire its mode, which we will preserve by default.
753+
// We don't follow symlinks here for replacement, and so we definitely don't want to pick up its mode.
754+
let existing_perms = existing_metadata
755+
.filter(|m| m.is_file())
756+
.map(|m| m.permissions());
757+
if let Some(existing_perms) = existing_perms {
758+
// Apply the existing permissions, if we have them
759+
t.as_file().set_permissions(existing_perms)?;
760+
} else {
761+
// Else restore default permissions
762+
#[cfg(unix)]
763+
if t_metadata.nlink() > 0 {
764+
t.as_file().set_permissions(t_metadata.permissions())?;
765+
}
766+
}
767+
}
738768
// fsync the TempFile
739769
t.as_file().sync_all()?;
740770
// rename the TempFile
@@ -745,7 +775,7 @@ impl CapStdExtDirExt for Dir {
745775
}
746776

747777
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()))
778+
self.atomic_replace_with(destname, None, |f| f.write_all(contents.as_ref()))
749779
}
750780

751781
fn atomic_write_with_perms(
@@ -754,19 +784,8 @@ impl CapStdExtDirExt for Dir {
754784
contents: impl AsRef<[u8]>,
755785
perms: cap_std::fs::Permissions,
756786
) -> 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-
}
787+
self.atomic_replace_with(destname, Some(perms), |f| -> io::Result<_> {
767788
f.write_all(contents.as_ref())?;
768-
f.flush()?;
769-
f.get_mut().as_file_mut().set_permissions(perms)?;
770789
Ok(())
771790
})
772791
}

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)