Skip to main content

fs_ext4/
acl.rs

1//! POSIX ACL decoder for ext4's compact on-disk ACL format.
2//!
3//! ext4 stores ACLs in xattrs under `system.posix_acl_access` and
4//! `system.posix_acl_default`. The value is NOT the generic POSIX ACL xattr
5//! format — the kernel packs it into a compact ext4-specific layout (version 1)
6//! when storing and expands back to POSIX format on getxattr. When we read the
7//! xattr value directly from disk we see the compact form.
8//!
9//! The kernel.org format documentation does not describe this layout. It
10//! is established by the oracles: the kernel reads back, with `getfacl`,
11//! ACLs this crate wrote (tests/kernel_readback.rs), and this crate reads
12//! the ACLs the kernel set on the fixtures (test-disks/ext4-acl.img).
13//!
14//! Layout (all little-endian):
15//!   u32 version = 0x0001 (EXT4_ACL_VERSION)
16//!   Then entries back-to-back:
17//!     short entry { u16 tag; u16 perm; }          (4 bytes)
18//!     full entry  { u16 tag; u16 perm; u32 id; }  (8 bytes)
19//!
20//! Short form (no id) is used for USER_OBJ, GROUP_OBJ, MASK, OTHER.
21//! Full form (with id) is used for USER and GROUP entries.
22
23use crate::block_io::BlockDevice;
24use crate::error::{Error, Result};
25use crate::inode::Inode;
26use crate::xattr;
27
28pub const EXT4_ACL_VERSION: u32 = 0x0001;
29
30/// Which ACL we're reading. ext4 stores access ACL under
31/// `system.posix_acl_access`; the default ACL (inherited by new children in
32/// a directory) is under `system.posix_acl_default`.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum AclKind {
35    Access,
36    Default,
37}
38
39impl AclKind {
40    pub fn xattr_name(self) -> &'static str {
41        match self {
42            AclKind::Access => "system.posix_acl_access",
43            AclKind::Default => "system.posix_acl_default",
44        }
45    }
46}
47
48/// POSIX ACL entry tag. Values match `<linux/posix_acl.h>`.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[repr(u16)]
51pub enum AclTag {
52    UserObj = 0x01,
53    User = 0x02,
54    GroupObj = 0x04,
55    Group = 0x08,
56    Mask = 0x10,
57    Other = 0x20,
58}
59
60impl AclTag {
61    pub fn from_u16(v: u16) -> Option<Self> {
62        match v {
63            0x01 => Some(AclTag::UserObj),
64            0x02 => Some(AclTag::User),
65            0x04 => Some(AclTag::GroupObj),
66            0x08 => Some(AclTag::Group),
67            0x10 => Some(AclTag::Mask),
68            0x20 => Some(AclTag::Other),
69            _ => None,
70        }
71    }
72
73    /// True when this tag's entry carries an id (USER or GROUP).
74    pub fn has_id(self) -> bool {
75        matches!(self, AclTag::User | AclTag::Group)
76    }
77}
78
79pub const ACL_READ: u16 = 4;
80pub const ACL_WRITE: u16 = 2;
81pub const ACL_EXECUTE: u16 = 1;
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct AclEntry {
85    pub tag: AclTag,
86    /// Permission bits: bit 0=execute, bit 1=write, bit 2=read.
87    pub perm: u16,
88    /// uid for `User`, gid for `Group`, otherwise `None`.
89    pub id: Option<u32>,
90}
91
92/// Parse an ext4 compact ACL xattr value into a list of entries.
93pub fn decode(buf: &[u8]) -> Result<Vec<AclEntry>> {
94    if buf.len() < 4 {
95        return Err(Error::Corrupt("acl too short for version header"));
96    }
97    let version = u32::from_le_bytes(buf[0..4].try_into().unwrap());
98    if version != EXT4_ACL_VERSION {
99        return Err(Error::Corrupt("acl version mismatch"));
100    }
101
102    let mut entries = Vec::new();
103    let mut pos = 4;
104    while pos < buf.len() {
105        if pos + 4 > buf.len() {
106            return Err(Error::Corrupt("acl entry truncated"));
107        }
108        let e_tag = u16::from_le_bytes(buf[pos..pos + 2].try_into().unwrap());
109        let e_perm = u16::from_le_bytes(buf[pos + 2..pos + 4].try_into().unwrap());
110        let tag = AclTag::from_u16(e_tag).ok_or(Error::Corrupt("acl entry has unknown tag"))?;
111
112        let (id, size) = if tag.has_id() {
113            if pos + 8 > buf.len() {
114                return Err(Error::Corrupt("acl full entry truncated"));
115            }
116            let e_id = u32::from_le_bytes(buf[pos + 4..pos + 8].try_into().unwrap());
117            (Some(e_id), 8)
118        } else {
119            (None, 4)
120        };
121
122        entries.push(AclEntry {
123            tag,
124            perm: e_perm,
125            id,
126        });
127        pos += size;
128    }
129    Ok(entries)
130}
131
132/// Read the ACL attached to an inode via xattr lookup + decode.
133///
134/// Returns `Ok(None)` when the xattr is absent (inode has no ACL of that kind);
135/// `Ok(Some(entries))` when present. Errors propagate from the xattr layer or
136/// from decoding an ill-formed ACL value.
137pub fn read(
138    dev: &dyn BlockDevice,
139    inode: &Inode,
140    inode_raw: &[u8],
141    inode_size: u16,
142    block_size: u32,
143    kind: AclKind,
144) -> Result<Option<Vec<AclEntry>>> {
145    let value = xattr::get(
146        dev,
147        inode,
148        inode_raw,
149        inode_size,
150        block_size,
151        kind.xattr_name(),
152    )?;
153    match value {
154        Some(bytes) => Ok(Some(decode(&bytes)?)),
155        None => Ok(None),
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    fn le32(v: u32) -> [u8; 4] {
164        v.to_le_bytes()
165    }
166    fn le16(v: u16) -> [u8; 2] {
167        v.to_le_bytes()
168    }
169
170    /// Build a minimal valid ACL blob in ext4 compact format.
171    fn build(entries: &[(u16, u16, Option<u32>)]) -> Vec<u8> {
172        let mut v = Vec::new();
173        v.extend_from_slice(&le32(EXT4_ACL_VERSION));
174        for (tag, perm, id) in entries {
175            v.extend_from_slice(&le16(*tag));
176            v.extend_from_slice(&le16(*perm));
177            if let Some(id) = id {
178                v.extend_from_slice(&le32(*id));
179            }
180        }
181        v
182    }
183
184    #[test]
185    fn decodes_minimal_acl() {
186        // Standard mode-mapped acl: USER_OBJ(rwx), GROUP_OBJ(r-x), OTHER(r--)
187        let buf = build(&[(0x01, 7, None), (0x04, 5, None), (0x20, 4, None)]);
188        let entries = decode(&buf).unwrap();
189        assert_eq!(entries.len(), 3);
190        assert_eq!(
191            entries[0],
192            AclEntry {
193                tag: AclTag::UserObj,
194                perm: 7,
195                id: None
196            }
197        );
198        assert_eq!(
199            entries[1],
200            AclEntry {
201                tag: AclTag::GroupObj,
202                perm: 5,
203                id: None
204            }
205        );
206        assert_eq!(
207            entries[2],
208            AclEntry {
209                tag: AclTag::Other,
210                perm: 4,
211                id: None
212            }
213        );
214    }
215
216    #[test]
217    fn decodes_named_user_and_group() {
218        let buf = build(&[
219            (0x01, 7, None),       // USER_OBJ rwx
220            (0x02, 6, Some(1000)), // named USER uid=1000 rw-
221            (0x04, 5, None),       // GROUP_OBJ r-x
222            (0x08, 4, Some(2000)), // named GROUP gid=2000 r--
223            (0x10, 7, None),       // MASK rwx
224            (0x20, 0, None),       // OTHER ---
225        ]);
226        let entries = decode(&buf).unwrap();
227        assert_eq!(entries.len(), 6);
228        assert_eq!(
229            entries[1],
230            AclEntry {
231                tag: AclTag::User,
232                perm: 6,
233                id: Some(1000)
234            }
235        );
236        assert_eq!(
237            entries[3],
238            AclEntry {
239                tag: AclTag::Group,
240                perm: 4,
241                id: Some(2000)
242            }
243        );
244        assert_eq!(
245            entries[4],
246            AclEntry {
247                tag: AclTag::Mask,
248                perm: 7,
249                id: None
250            }
251        );
252    }
253
254    #[test]
255    fn rejects_wrong_version() {
256        let mut buf = build(&[(0x01, 7, None)]);
257        buf[0] = 0x02; // bump version
258        assert!(decode(&buf).is_err());
259    }
260
261    #[test]
262    fn rejects_unknown_tag() {
263        let buf = build(&[(0x99, 7, None)]);
264        assert!(decode(&buf).is_err());
265    }
266
267    #[test]
268    fn rejects_truncated_header() {
269        assert!(decode(&[0x01, 0x00]).is_err());
270    }
271
272    #[test]
273    fn rejects_truncated_full_entry() {
274        // USER entry needs 8 bytes but only 4 provided after version
275        let mut v = Vec::new();
276        v.extend_from_slice(&le32(EXT4_ACL_VERSION));
277        v.extend_from_slice(&le16(0x02)); // USER
278        v.extend_from_slice(&le16(7));
279        // missing 4-byte id
280        assert!(decode(&v).is_err());
281    }
282
283    #[test]
284    fn xattr_names_correct() {
285        assert_eq!(AclKind::Access.xattr_name(), "system.posix_acl_access");
286        assert_eq!(AclKind::Default.xattr_name(), "system.posix_acl_default");
287    }
288
289    #[test]
290    fn tag_has_id_matches_spec() {
291        assert!(AclTag::User.has_id());
292        assert!(AclTag::Group.has_id());
293        assert!(!AclTag::UserObj.has_id());
294        assert!(!AclTag::GroupObj.has_id());
295        assert!(!AclTag::Mask.has_id());
296        assert!(!AclTag::Other.has_id());
297    }
298}