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