ext2/
xattr.rs

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
7use std::ffi::c_char;
8use std::ffi::CString;
9use std::os::unix::ffi::OsStrExt;
10use std::path::Path;
11
12use anyhow::bail;
13use anyhow::Context;
14use anyhow::Result;
15use zerocopy::FromBytes;
16use zerocopy::Immutable;
17use zerocopy::IntoBytes;
18use zerocopy::KnownLayout;
19
20use crate::inode::Inode;
21
22fn 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 = "aarch64", target_arch = "riscv64"),
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
63fn 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.
98pub 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`.
119pub 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)]
157pub(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
167impl XattrEntry {
168    /// Creates a new `XattrEntry` instance with the name as a byte sequence that follows.
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.
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)]
215pub struct InlineXattrs {
216    pub entry_table: Vec<u8>,
217    pub values: Vec<u8>,
218}
219
220fn 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
226const XATTR_HEADER_MAGIC: u32 = 0xEA020000;
227
228impl InlineXattrs {
229    // Creates `InlineXattrs` for the given path.
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)]
283pub(crate) mod tests {
284    use std::collections::BTreeMap;
285    use std::fs::File;
286
287    use tempfile::tempdir;
288
289    use super::*;
290
291    fn to_char_array(s: &str) -> Vec<u8> {
292        s.bytes().collect()
293    }
294
295    #[test]
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]
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]
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]
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]
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]
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}