Skip to content

Commit c5dbbcd

Browse files
committed
dirext: improve permissions handling in atomic_*
`atomic_replace_with` now takes the target permissions as an Option. Existing permissions copy now happen after flushing the buffers, so security related bits are not removed by the kernel. When using filesystems that do not support `O_TEMPFILE`, we now change the tempfile permissions to 0o000 as soon as possible. A malicious process still has a really short time to `open()` the tempfile, to prevent that for now you must the a restrictive umask. Signed-off-by: Etienne Champetier <[email protected]>
1 parent 677efca commit c5dbbcd

File tree

2 files changed

+72
-42
lines changed

2 files changed

+72
-42
lines changed

src/dirext.rs

Lines changed: 58 additions & 38 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,27 @@ 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())?;
332340
/// # let somedir = cap_std::fs_utf8::Dir::from_cap_std((&*somedir).try_clone()?);
333341
/// use cap_std_ext::prelude::*;
334342
/// let contents = b"hello world\n";
335-
/// somedir.atomic_replace_with("somefilename", |f| -> io::Result<_> {
343+
/// let perms = cap_std::fs::Permissions::from_mode(0o600);
344+
/// somedir.atomic_replace_with("somefilename", Some(perms), |f| -> io::Result<_> {
336345
/// 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)?;
341346
/// Ok(())
342347
/// })
343348
/// # }
@@ -347,6 +352,7 @@ pub trait CapStdExtDirExtUtf8 {
347352
fn atomic_replace_with<F, T, E>(
348353
&self,
349354
destname: impl AsRef<Utf8Path>,
355+
perms: Option<cap_std::fs::Permissions>,
350356
f: F,
351357
) -> std::result::Result<T, E>
352358
where
@@ -710,6 +716,7 @@ impl CapStdExtDirExt for Dir {
710716
fn atomic_replace_with<F, T, E>(
711717
&self,
712718
destname: impl AsRef<Path>,
719+
perms: Option<cap_std::fs::Permissions>,
713720
f: F,
714721
) -> std::result::Result<T, E>
715722
where
@@ -718,23 +725,47 @@ impl CapStdExtDirExt for Dir {
718725
{
719726
let destname = destname.as_ref();
720727
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());
728+
729+
// cap_tempfile doesn't support passing permissions on creation.
730+
// https://github.com/bytecodealliance/cap-std/pull/390
727731
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)?;
732+
#[cfg(unix)]
733+
let t_metadata = t.as_file().metadata()?;
734+
#[cfg(unix)]
735+
if t_metadata.nlink() > 0 {
736+
let zeroperms = cap_std::fs::PermissionsExt::from_mode(0o000);
737+
t.as_file().set_permissions(zeroperms)?;
731738
}
739+
732740
// We always operate in terms of buffered writes
733741
let mut bufw = std::io::BufWriter::new(t);
734742
// Call the provided closure to generate the file content
735743
let r = f(&mut bufw)?;
736744
// Flush the buffer, get the TempFile
737745
t = bufw.into_inner().map_err(From::from)?;
746+
// Set the file permissions
747+
// This must be done after flushing the application buffer else Linux clears the security related bits:
748+
// https://github.com/torvalds/linux/blob/94305e83eccb3120c921cd3a015cd74731140bac/fs/inode.c#L2360
749+
if let Some(perms) = perms {
750+
t.as_file().set_permissions(perms)?;
751+
} else {
752+
let existing_metadata = d.symlink_metadata_optional(destname)?;
753+
// If the target is already a file, then acquire its mode, which we will preserve by default.
754+
// We don't follow symlinks here for replacement, and so we definitely don't want to pick up its mode.
755+
let existing_perms = existing_metadata
756+
.filter(|m| m.is_file())
757+
.map(|m| m.permissions());
758+
if let Some(existing_perms) = existing_perms {
759+
// Apply the existing permissions, if we have them
760+
t.as_file().set_permissions(existing_perms)?;
761+
} else {
762+
// Else restore default permissions
763+
#[cfg(unix)]
764+
if t_metadata.nlink() > 0 {
765+
t.as_file().set_permissions(t_metadata.permissions())?;
766+
}
767+
}
768+
}
738769
// fsync the TempFile
739770
t.as_file().sync_all()?;
740771
// rename the TempFile
@@ -745,7 +776,7 @@ impl CapStdExtDirExt for Dir {
745776
}
746777

747778
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()))
779+
self.atomic_replace_with(destname, None, |f| f.write_all(contents.as_ref()))
749780
}
750781

751782
fn atomic_write_with_perms(
@@ -754,19 +785,8 @@ impl CapStdExtDirExt for Dir {
754785
contents: impl AsRef<[u8]>,
755786
perms: cap_std::fs::Permissions,
756787
) -> 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-
}
788+
self.atomic_replace_with(destname, Some(perms), |f| -> io::Result<_> {
767789
f.write_all(contents.as_ref())?;
768-
f.flush()?;
769-
f.get_mut().as_file_mut().set_permissions(perms)?;
770790
Ok(())
771791
})
772792
}

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)