• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2024 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 //! Provides utilites for extended attributes.
6 
7 use std::ffi::c_char;
8 use std::ffi::CString;
9 use std::os::unix::ffi::OsStrExt;
10 use std::path::Path;
11 
12 use anyhow::bail;
13 use anyhow::Context;
14 use anyhow::Result;
15 use zerocopy::FromBytes;
16 use zerocopy::Immutable;
17 use zerocopy::IntoBytes;
18 use zerocopy::KnownLayout;
19 
20 use crate::inode::Inode;
21 
listxattr(path: &CString) -> Result<Vec<Vec<u8>>>22 fn listxattr(path: &CString) -> Result<Vec<Vec<u8>>> {
23     // SAFETY: Passing valid pointers and values.
24     let size = unsafe { libc::llistxattr(path.as_ptr(), std::ptr::null_mut(), 0) };
25     if size < 0 {
26         bail!(
27             "failed to get xattr size: {}",
28             std::io::Error::last_os_error()
29         );
30     }
31 
32     if size == 0 {
33         // No extended attributes were set.
34         return Ok(vec![]);
35     }
36 
37     let mut buf = vec![0 as c_char; size as usize];
38 
39     // SAFETY: Passing valid pointers and values.
40     let size = unsafe { libc::llistxattr(path.as_ptr(), buf.as_mut_ptr(), buf.len()) };
41     if size < 0 {
42         bail!(
43             "failed to list of xattr: {}",
44             std::io::Error::last_os_error()
45         );
46     }
47 
48     buf.pop(); // Remove null terminator
49 
50     // While `c_char` is `i8` on x86_64, it's `u8` on ARM. So, disable the clippy for the cast.
51     #[cfg_attr(
52         any(target_arch = "arm", target_arch = "aarch64"),
53         allow(clippy::unnecessary_cast)
54     )]
55     let keys = buf
56         .split(|c| *c == 0)
57         .map(|v| v.iter().map(|c| *c as u8).collect::<Vec<_>>())
58         .collect::<Vec<Vec<_>>>();
59 
60     Ok(keys)
61 }
62 
lgetxattr(path: &CString, name: &CString) -> Result<Vec<u8>>63 fn lgetxattr(path: &CString, name: &CString) -> Result<Vec<u8>> {
64     // SAFETY: passing valid pointers.
65     let size = unsafe { libc::lgetxattr(path.as_ptr(), name.as_ptr(), std::ptr::null_mut(), 0) };
66     if size < 0 {
67         bail!(
68             "failed to get xattr size for {:?}: {}",
69             name,
70             std::io::Error::last_os_error()
71         );
72     }
73     let mut buf = vec![0; size as usize];
74     // SAFETY: passing valid pointers and length.
75     let size = unsafe {
76         libc::lgetxattr(
77             path.as_ptr(),
78             name.as_ptr(),
79             buf.as_mut_ptr() as *mut libc::c_void,
80             buf.len(),
81         )
82     };
83     if size < 0 {
84         bail!(
85             "failed to get xattr for {:?}: {}",
86             name,
87             std::io::Error::last_os_error()
88         );
89     }
90 
91     Ok(buf)
92 }
93 
94 /// Retrieves the list of pairs of a name and a value of the extended attribute of the given `path`.
95 /// If `path` is a symbolic link, it won't be followed and the value of the symlink itself is
96 /// returned.
97 /// The return values are byte arrays WITHOUT trailing NULL byte.
dump_xattrs(path: &Path) -> Result<Vec<(Vec<u8>, Vec<u8>)>>98 pub fn dump_xattrs(path: &Path) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
99     let mut path_vec = path.as_os_str().as_bytes().to_vec();
100     path_vec.push(0);
101     let path_str = CString::from_vec_with_nul(path_vec)?;
102 
103     let keys = listxattr(&path_str).context("failed to listxattr")?;
104 
105     let mut kvs = vec![];
106     for key in keys {
107         let mut key_vec = key.to_vec();
108         key_vec.push(0);
109         let name = CString::from_vec_with_nul(key_vec)?;
110 
111         let buf = lgetxattr(&path_str, &name).context("failed to getxattr")?;
112         kvs.push((key.to_vec(), buf));
113     }
114 
115     Ok(kvs)
116 }
117 
118 /// Sets the extended attribute of the given `path` with the given `key` and `value`.
set_xattr(path: &Path, key: &str, value: &str) -> Result<()>119 pub fn set_xattr(path: &Path, key: &str, value: &str) -> Result<()> {
120     let mut path_bytes = path
121         .as_os_str()
122         .as_bytes()
123         .iter()
124         .map(|i| *i as c_char)
125         .collect::<Vec<_>>();
126     path_bytes.push(0); // null terminator
127 
128     // While name must be a nul-terminated string, value is not, as it can be a binary data.
129     let mut key_vec = key.bytes().collect::<Vec<_>>();
130     key_vec.push(0);
131     let name = CString::from_vec_with_nul(key_vec)?;
132     let v = value.bytes().collect::<Vec<_>>();
133 
134     // SAFETY: `path_bytes` and `nam` are null-terminated byte arrays.
135     // `v` is valid data.
136     let size = unsafe {
137         libc::lsetxattr(
138             path_bytes.as_ptr(),
139             name.as_ptr(),
140             v.as_ptr() as *const libc::c_void,
141             v.len(),
142             0,
143         )
144     };
145     if size != 0 {
146         bail!(
147             "failed to set xattr for {:?}: {}",
148             path,
149             std::io::Error::last_os_error()
150         );
151     }
152     Ok(())
153 }
154 
155 #[repr(C)]
156 #[derive(Default, Debug, Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
157 pub(crate) struct XattrEntry {
158     name_len: u8,
159     name_index: u8,
160     value_offs: u16,
161     value_inum: u32,
162     value_size: u32,
163     hash: u32,
164     // name[name_len] follows
165 }
166 
167 impl XattrEntry {
168     /// Creates a new `XattrEntry` instance with the name as a byte sequence that follows.
new_with_name<'a>( name: &'a [u8], value: &[u8], value_offs: u16, ) -> Result<(Self, &'a [u8])>169     pub(crate) fn new_with_name<'a>(
170         name: &'a [u8],
171         value: &[u8],
172         value_offs: u16,
173     ) -> Result<(Self, &'a [u8])> {
174         let (name_index, key_str) = Self::split_key_prefix(name);
175         let name_len = key_str.len() as u8;
176         let value_size = value.len() as u32;
177         Ok((
178             XattrEntry {
179                 name_len,
180                 name_index,
181                 value_offs,
182                 value_inum: 0,
183                 value_size,
184                 hash: 0,
185             },
186             key_str,
187         ))
188     }
189 
190     /// Split the given xatrr key string into it's prefix's name index and the remaining part.
191     /// e.g. "user.foo" -> (1, "foo") because the key prefix "user." has index 1.
split_key_prefix(name: &[u8]) -> (u8, &[u8])192     fn split_key_prefix(name: &[u8]) -> (u8, &[u8]) {
193         // ref. https://docs.kernel.org/filesystems/ext4/dynamic.html#attribute-name-indices
194         for (name_index, key_prefix) in [
195             (1, "user."),
196             (2, "system.posix_acl_access"),
197             (3, "system.posix_acl_default"),
198             (4, "trusted."),
199             // 5 is skipped
200             (6, "security."),
201             (7, "system."),
202             (8, "system.richacl"),
203         ] {
204             let prefix_bytes = key_prefix.as_bytes();
205             if name.starts_with(prefix_bytes) {
206                 return (name_index, &name[prefix_bytes.len()..]);
207             }
208         }
209         (0, name)
210     }
211 }
212 
213 /// Xattr data written into Inode's inline xattr space.
214 #[derive(Default, Debug, PartialEq, Eq)]
215 pub struct InlineXattrs {
216     pub entry_table: Vec<u8>,
217     pub values: Vec<u8>,
218 }
219 
align<T: Clone + Default>(mut v: Vec<T>, alignment: usize) -> Vec<T>220 fn align<T: Clone + Default>(mut v: Vec<T>, alignment: usize) -> Vec<T> {
221     let aligned = v.len().next_multiple_of(alignment);
222     v.extend(vec![T::default(); aligned - v.len()]);
223     v
224 }
225 
226 const XATTR_HEADER_MAGIC: u32 = 0xEA020000;
227 
228 impl InlineXattrs {
229     // Creates `InlineXattrs` for the given path.
from_path(path: &Path) -> Result<Self>230     pub fn from_path(path: &Path) -> Result<Self> {
231         let v = dump_xattrs(path).with_context(|| format!("failed to get xattr for {:?}", path))?;
232 
233         // Assume all the data are in inode record.
234         let mut entry_table = vec![];
235         let mut values = vec![];
236         // Data layout of the inline Inode record is as follows.
237         //
238         // | Inode struct | header | extra region |
239         //  <--------- Inode record  ------------>
240         //
241         // The value `val_offset` below is an offset from the beginning of the extra region and used
242         // to indicate the place where the next xattr value will be written. While we place
243         // attribute entries from the beginning of the extra region, we place values from the end of
244         // the region. So the initial value of `val_offset` indicates the end of the extra
245         // region.
246         //
247         // See Table 5.1. at https://www.nongnu.org/ext2-doc/ext2.html#extended-attribute-layout for the more details on data layout.
248         // Although this table is for xattr in a separate block, data layout is same.
249         let mut val_offset = Inode::INODE_RECORD_SIZE
250             - std::mem::size_of::<Inode>()
251             - std::mem::size_of_val(&XATTR_HEADER_MAGIC);
252 
253         entry_table.extend(XATTR_HEADER_MAGIC.to_le_bytes());
254         for (name, value) in v {
255             let aligned_val_len = value.len().next_multiple_of(4);
256 
257             if entry_table.len()
258                 + values.len()
259                 + std::mem::size_of::<XattrEntry>()
260                 + aligned_val_len
261                 > Inode::XATTR_AREA_SIZE
262             {
263                 bail!("Xattr entry is too large");
264             }
265 
266             val_offset -= aligned_val_len;
267             let (entry, name) = XattrEntry::new_with_name(&name, &value, val_offset as u16)?;
268             entry_table.extend(entry.as_bytes());
269             entry_table.extend(name);
270             entry_table = align(entry_table, 4);
271             values.push(align(value, 4));
272         }
273         let values = values.iter().rev().flatten().copied().collect::<Vec<_>>();
274 
275         Ok(Self {
276             entry_table,
277             values,
278         })
279     }
280 }
281 
282 #[cfg(test)]
283 pub(crate) mod tests {
284     use std::collections::BTreeMap;
285     use std::fs::File;
286 
287     use tempfile::tempdir;
288 
289     use super::*;
290 
to_char_array(s: &str) -> Vec<u8>291     fn to_char_array(s: &str) -> Vec<u8> {
292         s.bytes().collect()
293     }
294 
295     #[test]
test_attr_name_index()296     fn test_attr_name_index() {
297         assert_eq!(
298             XattrEntry::split_key_prefix(b"user.foo"),
299             (1, "foo".as_bytes())
300         );
301         assert_eq!(
302             XattrEntry::split_key_prefix(b"trusted.bar"),
303             (4, "bar".as_bytes())
304         );
305         assert_eq!(
306             XattrEntry::split_key_prefix(b"security.abcdefgh"),
307             (6, "abcdefgh".as_bytes())
308         );
309 
310         // "system."-prefix
311         assert_eq!(
312             XattrEntry::split_key_prefix(b"system.posix_acl_access"),
313             (2, "".as_bytes())
314         );
315         assert_eq!(
316             XattrEntry::split_key_prefix(b"system.posix_acl_default"),
317             (3, "".as_bytes())
318         );
319         assert_eq!(
320             XattrEntry::split_key_prefix(b"system.abcdefgh"),
321             (7, "abcdefgh".as_bytes())
322         );
323 
324         // unmatched prefix
325         assert_eq!(
326             XattrEntry::split_key_prefix(b"invalid.foo"),
327             (0, "invalid.foo".as_bytes())
328         );
329     }
330 
331     #[test]
test_get_xattr_empty()332     fn test_get_xattr_empty() {
333         let td = tempdir().unwrap();
334         let test_path = td.path().join("test.txt");
335 
336         // Don't set any extended attributes.
337         File::create(&test_path).unwrap();
338 
339         let kvs = dump_xattrs(&test_path).unwrap();
340         assert_eq!(kvs.len(), 0);
341     }
342 
343     #[test]
test_inline_xattr_from_path()344     fn test_inline_xattr_from_path() {
345         let td = tempdir().unwrap();
346         let test_path = td.path().join("test.txt");
347         File::create(&test_path).unwrap();
348 
349         let key = "key";
350         let xattr_key = &format!("user.{key}");
351         let value = "value";
352 
353         set_xattr(&test_path, xattr_key, value).unwrap();
354 
355         let xattrs = InlineXattrs::from_path(&test_path).unwrap();
356         let entry = XattrEntry {
357             name_len: key.len() as u8,
358             name_index: 1,
359             value_offs: (Inode::INODE_RECORD_SIZE
360                 - std::mem::size_of::<Inode>()
361                 - std::mem::size_of_val(&XATTR_HEADER_MAGIC)
362                 - value.len().next_multiple_of(4)) as u16,
363             value_size: value.len() as u32,
364             value_inum: 0,
365             ..Default::default()
366         };
367         assert_eq!(
368             xattrs.entry_table,
369             align(
370                 [
371                     XATTR_HEADER_MAGIC.to_le_bytes().to_vec(),
372                     entry.as_bytes().to_vec(),
373                     key.as_bytes().to_vec(),
374                 ]
375                 .concat(),
376                 4
377             ),
378         );
379         assert_eq!(xattrs.values, align(value.as_bytes().to_vec(), 4),);
380     }
381 
382     #[test]
test_too_many_values_for_inline_xattr()383     fn test_too_many_values_for_inline_xattr() {
384         let td = tempdir().unwrap();
385         let test_path = td.path().join("test.txt");
386         File::create(&test_path).unwrap();
387 
388         // Prepare 10 pairs of xattributes, which will not fit inline space.
389         let mut xattr_pairs = vec![];
390         for i in 0..10 {
391             xattr_pairs.push((format!("user.foo{i}"), "bar"));
392         }
393 
394         for (key, value) in &xattr_pairs {
395             set_xattr(&test_path, key, value).unwrap();
396         }
397 
398         // Must fail
399         InlineXattrs::from_path(&test_path).unwrap_err();
400     }
401 
402     #[test]
test_get_xattr()403     fn test_get_xattr() {
404         let td = tempdir().unwrap();
405         let test_path = td.path().join("test.txt");
406         File::create(&test_path).unwrap();
407 
408         let xattr_pairs = vec![
409             ("user.foo", "bar"),
410             ("user.hash", "09f7e02f1290be211da707a266f153b3"),
411             ("user.empty", ""),
412         ];
413 
414         for (key, value) in &xattr_pairs {
415             set_xattr(&test_path, key, value).unwrap();
416         }
417 
418         let kvs = dump_xattrs(&test_path).unwrap();
419         assert_eq!(kvs.len(), xattr_pairs.len());
420 
421         let xattr_map: BTreeMap<Vec<u8>, Vec<u8>> = kvs.into_iter().collect();
422 
423         for (orig_k, orig_v) in xattr_pairs {
424             let k = to_char_array(orig_k);
425             let v = to_char_array(orig_v);
426             let got = xattr_map.get(&k).unwrap();
427             assert_eq!(&v, got);
428         }
429     }
430 
431     #[test]
test_get_xattr_symlink()432     fn test_get_xattr_symlink() {
433         let td = tempdir().unwrap();
434 
435         // Set xattr on test.txt.
436         let test_path = td.path().join("test.txt");
437         File::create(&test_path).unwrap();
438         set_xattr(&test_path, "user.name", "user.test.txt").unwrap();
439 
440         // Create a symlink to test.txt.
441         let symlink_path = td.path().join("symlink");
442         std::os::unix::fs::symlink(&test_path, &symlink_path).unwrap();
443 
444         // dump_xattrs shouldn't follow a symlink.
445         let kvs = dump_xattrs(&symlink_path).unwrap();
446         assert_eq!(kvs, vec![]);
447     }
448 }
449