Skip to content

Commit bed2029

Browse files
committed
dirext: Add xattr wrappers
There's a lot of subtleties to these APIs. First, unlike the `xattrs` crate, we operate on cap-std `Dir` objects. As part of that, we also explicitly error out on absolute paths, as well as paths containing any uplinks (`../`) at all. - Always use `/proc/self/fd` with `lgetxattr` because this is the only way to get/set xattrs on symlinks. - Return a `Result<Option<>>` with getxattr for consistency with our other APIs to handle the common case of looking for a nonexistent xattr. - The `getxattr` and `listxattr` APIs are also higher level than what Rustix offers to be maximally convenient; we always return an owned buffer, and handle resizing it. Signed-off-by: Colin Walters <[email protected]>
1 parent 7419249 commit bed2029

File tree

4 files changed

+270
-0
lines changed

4 files changed

+270
-0
lines changed

src/dirext.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,24 @@ pub trait CapStdExtDirExt {
226226
/// to determine, and `None` will be returned in those cases.
227227
fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>>;
228228

229+
#[cfg(not(windows))]
230+
/// Get the value of an extended attribute. If the attribute is not present,
231+
/// this function will return `Ok(None)`.
232+
fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>>;
233+
234+
#[cfg(not(windows))]
235+
/// List all extended attribute keys for this path.
236+
fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList>;
237+
238+
#[cfg(not(windows))]
239+
/// Set the value of an extended attribute.
240+
fn setxattr(
241+
&self,
242+
path: impl AsRef<Path>,
243+
key: impl AsRef<OsStr>,
244+
value: impl AsRef<[u8]>,
245+
) -> Result<()>;
246+
229247
/// Recursively walk a directory. If the function returns [`std::ops::ControlFlow::Break`]
230248
/// while inspecting a directory, traversal of that directory is skipped. If
231249
/// [`std::ops::ControlFlow::Break`] is returned when inspecting a non-directory,
@@ -562,6 +580,20 @@ where
562580
Ok(())
563581
}
564582

583+
// Ensure that the target path isn't absolute, and doesn't
584+
// have any parent references.
585+
pub(crate) fn validate_relpath_no_uplinks(path: &Path) -> Result<&Path> {
586+
let is_absolute = path.is_absolute();
587+
let contains_uplinks = path
588+
.components()
589+
.any(|e| e == std::path::Component::ParentDir);
590+
if is_absolute || contains_uplinks {
591+
Err(crate::escape_attempt())
592+
} else {
593+
Ok(path)
594+
}
595+
}
596+
565597
impl CapStdExtDirExt for Dir {
566598
fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>> {
567599
map_optional(self.open(path.as_ref()))
@@ -738,6 +770,26 @@ impl CapStdExtDirExt for Dir {
738770
is_mountpoint_impl_statx(self, path.as_ref()).map_err(Into::into)
739771
}
740772

773+
#[cfg(not(windows))]
774+
fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>> {
775+
crate::xattrs::impl_getxattr(self, path.as_ref(), key.as_ref())
776+
}
777+
778+
#[cfg(not(windows))]
779+
fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList> {
780+
crate::xattrs::impl_listxattrs(self, path.as_ref())
781+
}
782+
783+
#[cfg(not(windows))]
784+
fn setxattr(
785+
&self,
786+
path: impl AsRef<Path>,
787+
key: impl AsRef<OsStr>,
788+
value: impl AsRef<[u8]>,
789+
) -> Result<()> {
790+
crate::xattrs::impl_setxattr(self, path.as_ref(), key.as_ref(), value.as_ref())
791+
}
792+
741793
fn walk<C, E>(&self, config: &WalkConfiguration, mut callback: C) -> std::result::Result<(), E>
742794
where
743795
C: FnMut(&WalkComponent) -> WalkResult<E>,
@@ -851,3 +903,23 @@ impl CapStdExtDirExtUtf8 for cap_std::fs_utf8::Dir {
851903
Ok(r)
852904
}
853905
}
906+
907+
#[cfg(test)]
908+
mod tests {
909+
use std::path::Path;
910+
911+
use super::*;
912+
913+
#[test]
914+
fn test_validate_relpath_no_uplinks() {
915+
let ok_cases = ["foo", "foo/bar", "foo/bar/"];
916+
let err_cases = ["/foo", "/", "../foo", "foo/../bar"];
917+
918+
for case in ok_cases {
919+
assert!(validate_relpath_no_uplinks(Path::new(case)).is_ok());
920+
}
921+
for case in err_cases {
922+
assert!(validate_relpath_no_uplinks(Path::new(case)).is_err());
923+
}
924+
}
925+
}

src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#![deny(unsafe_code)]
77
#![cfg_attr(feature = "dox", feature(doc_cfg))]
88

9+
use std::io;
10+
911
// Re-export our dependencies
1012
pub use cap_primitives;
1113
#[cfg(feature = "fs_utf8")]
@@ -19,7 +21,20 @@ pub mod dirext;
1921

2022
#[cfg(any(target_os = "android", target_os = "linux"))]
2123
mod rootdir;
24+
#[cfg(any(target_os = "android", target_os = "linux"))]
2225
pub use rootdir::*;
26+
#[cfg(not(windows))]
27+
mod xattrs;
28+
#[cfg(not(windows))]
29+
pub use xattrs::XattrList;
30+
31+
#[cold]
32+
pub(crate) fn escape_attempt() -> io::Error {
33+
io::Error::new(
34+
io::ErrorKind::PermissionDenied,
35+
"a path led outside of the filesystem",
36+
)
37+
}
2338

2439
/// Prelude, intended for glob import.
2540
pub mod prelude {

src/xattrs.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use std::io::Result;
2+
use std::{
3+
ffi::OsStr,
4+
os::unix::ffi::OsStrExt,
5+
path::{Path, PathBuf},
6+
};
7+
8+
use cap_tempfile::cap_std::fs::Dir;
9+
use rustix::buffer::spare_capacity;
10+
11+
use crate::dirext::validate_relpath_no_uplinks;
12+
13+
/// Convert the directory and path pair into a /proc/self/fd path
14+
/// which is useful for xattr functions in particular to be able
15+
/// to operate on symlinks.
16+
///
17+
/// Absolute paths as well as paths with uplinks (`..`) are an error.
18+
fn proc_self_path(d: &Dir, path: &Path) -> Result<PathBuf> {
19+
use rustix::path::DecInt;
20+
use std::os::fd::{AsFd, AsRawFd};
21+
22+
// Require relative paths here.
23+
let path = validate_relpath_no_uplinks(path)?;
24+
25+
let mut pathbuf = PathBuf::from("/proc/self/fd");
26+
pathbuf.push(DecInt::new(d.as_fd().as_raw_fd()));
27+
pathbuf.push(path);
28+
Ok(pathbuf)
29+
}
30+
31+
pub(crate) fn impl_getxattr(d: &Dir, path: &Path, key: &OsStr) -> Result<Option<Vec<u8>>> {
32+
let path = &proc_self_path(d, path)?;
33+
34+
// In my experience few extended attributes exceed this
35+
let mut buf = Vec::with_capacity(256);
36+
37+
loop {
38+
match rustix::fs::lgetxattr(path, key, spare_capacity(&mut buf)) {
39+
Ok(_) => {
40+
return Ok(Some(buf));
41+
}
42+
Err(rustix::io::Errno::NODATA) => {
43+
return Ok(None);
44+
}
45+
Err(rustix::io::Errno::RANGE) => {
46+
buf.reserve(buf.capacity().saturating_mul(2));
47+
}
48+
Err(e) => {
49+
return Err(e.into());
50+
}
51+
}
52+
}
53+
}
54+
55+
/// A list of extended attribute value names
56+
#[derive(Debug)]
57+
#[cfg(any(target_os = "android", target_os = "linux"))]
58+
pub struct XattrList {
59+
/// Contents of the return value from the llistxattr system call;
60+
/// effectively Vec<OsStr> with an empty value as terminator.
61+
/// Not public - we expect callers to invoke the `iter()` method.
62+
/// When Rust has lending iterators then we could implement IntoIterator
63+
/// in a way that borrows from this value.
64+
buf: Vec<u8>,
65+
}
66+
67+
#[cfg(any(target_os = "android", target_os = "linux"))]
68+
impl XattrList {
69+
/// Return an iterator over the elements of this extended attribute list.
70+
pub fn iter(&self) -> impl Iterator<Item = &'_ std::ffi::OsStr> {
71+
self.buf.split(|&v| v == 0).filter_map(|v| {
72+
// Note this case should only happen once at the end
73+
if v.is_empty() {
74+
None
75+
} else {
76+
Some(OsStr::from_bytes(v))
77+
}
78+
})
79+
}
80+
}
81+
82+
#[cfg(any(target_os = "android", target_os = "linux"))]
83+
pub(crate) fn impl_listxattrs(d: &Dir, path: &Path) -> Result<XattrList> {
84+
let path = &proc_self_path(d, path)?;
85+
86+
let mut buf = Vec::with_capacity(512);
87+
88+
loop {
89+
match rustix::fs::llistxattr(path, spare_capacity(&mut buf)) {
90+
Ok(_) => {
91+
return Ok(XattrList { buf });
92+
}
93+
Err(rustix::io::Errno::RANGE) => {
94+
buf.reserve(buf.capacity().saturating_mul(2));
95+
}
96+
Err(e) => {
97+
return Err(e.into());
98+
}
99+
}
100+
}
101+
}
102+
103+
#[cfg(any(target_os = "android", target_os = "linux"))]
104+
pub(crate) fn impl_setxattr(d: &Dir, path: &Path, key: &OsStr, value: &[u8]) -> Result<()> {
105+
let path = &proc_self_path(d, path)?;
106+
rustix::fs::lsetxattr(path, key, value, rustix::fs::XattrFlags::empty())?;
107+
Ok(())
108+
}

tests/it/main.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,3 +568,78 @@ fn test_walk_noxdev() -> Result<()> {
568568

569569
Ok(())
570570
}
571+
572+
#[test]
573+
#[cfg(not(windows))]
574+
fn test_xattrs() -> Result<()> {
575+
use std::os::unix::ffi::OsStrExt;
576+
577+
let td = &cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
578+
579+
assert!(td.getxattr(".", "user.test").unwrap().is_none());
580+
td.setxattr(".", "user.test", "somevalue").unwrap();
581+
assert_eq!(
582+
td.getxattr(".", "user.test").unwrap().unwrap(),
583+
b"somevalue"
584+
);
585+
// Helper fn to list only user. xattrs - filter out especially
586+
// system xattrs like security.selinux which may be synthesized by the OS
587+
fn list_user_xattrs(d: &Dir, p: impl AsRef<Path>) -> Vec<std::ffi::OsString> {
588+
let attrs = d.listxattrs(p).unwrap();
589+
attrs
590+
.iter()
591+
.filter(|k| {
592+
let Some(k) = k.to_str() else {
593+
return false;
594+
};
595+
k.split_once('.').unwrap().0 == "user"
596+
})
597+
.map(ToOwned::to_owned)
598+
.collect::<Vec<_>>()
599+
}
600+
match list_user_xattrs(td, ".").as_slice() {
601+
[] => unreachable!(),
602+
[v] => assert_eq!(v.as_bytes(), b"user.test"),
603+
_ => unreachable!(),
604+
};
605+
606+
// Test multiple xattrs
607+
td.setxattr(".", "user.othertest", b"anothervalue").unwrap();
608+
assert_eq!(list_user_xattrs(td, ".").len(), 2);
609+
610+
// Test operating on a subdirectory file
611+
td.create_dir_all("foo/bar/baz")?;
612+
let p = "foo/bar/baz/subfile";
613+
td.write(p, "afile")?;
614+
td.setxattr(p, "user.filetest", b"filexattr").unwrap();
615+
assert_eq!(
616+
td.getxattr(p, "user.filetest").unwrap().unwrap(),
617+
b"filexattr"
618+
);
619+
match list_user_xattrs(td, p).as_slice() {
620+
[v] => assert_eq!(v.as_bytes(), b"user.filetest"),
621+
_ => unreachable!(),
622+
};
623+
624+
// Test listing xattrs on a broken symlink. We can't set user.
625+
// xattrs on a symlink, so we don't test that in unit tests.
626+
let p = "foo/bar/baz/somelink";
627+
td.symlink("enoent", p)?;
628+
assert!(list_user_xattrs(td, p).is_empty());
629+
630+
Ok(())
631+
}
632+
633+
#[test]
634+
#[cfg(not(windows))]
635+
fn test_big_xattr() -> Result<()> {
636+
let td = &cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
637+
638+
let bigbuf = b"x".repeat(4000);
639+
td.setxattr(".", "user.test", &bigbuf).unwrap();
640+
let v = td.getxattr(".", "user.test").unwrap().unwrap();
641+
assert_eq!(bigbuf.len(), v.len());
642+
assert_eq!(bigbuf, v);
643+
644+
Ok(())
645+
}

0 commit comments

Comments
 (0)