Skip to main content

agentos_kernel/
permissions.rs

1use crate::vfs::{
2    validate_path, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat,
3    VirtualUtimeSpec,
4};
5use std::collections::{BTreeMap, HashMap};
6use std::error::Error;
7use std::fmt;
8use std::path::Path;
9use std::sync::Arc;
10
11const IMMUTABLE_XATTR: &str = "user.agentos.immutable";
12
13pub type FsPermissionCheck = Arc<dyn Fn(&FsAccessRequest) -> PermissionDecision + Send + Sync>;
14pub type NetworkPermissionCheck =
15    Arc<dyn Fn(&NetworkAccessRequest) -> PermissionDecision + Send + Sync>;
16pub type CommandPermissionCheck =
17    Arc<dyn Fn(&CommandAccessRequest) -> PermissionDecision + Send + Sync>;
18pub type EnvironmentPermissionCheck =
19    Arc<dyn Fn(&EnvAccessRequest) -> PermissionDecision + Send + Sync>;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct PermissionDecision {
23    pub allow: bool,
24    pub reason: Option<String>,
25}
26
27impl PermissionDecision {
28    pub fn allow() -> Self {
29        Self {
30            allow: true,
31            reason: None,
32        }
33    }
34
35    pub fn deny(reason: impl Into<String>) -> Self {
36        Self {
37            allow: false,
38            reason: Some(reason.into()),
39        }
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct PermissionError {
45    code: &'static str,
46    message: String,
47}
48
49impl PermissionError {
50    pub fn code(&self) -> &'static str {
51        self.code
52    }
53
54    fn access_denied(subject: impl Into<String>, reason: Option<&str>) -> Self {
55        let subject = subject.into();
56        let message = match reason {
57            Some(reason) => format!("permission denied, {subject}: {reason}"),
58            None => format!("permission denied, {subject}"),
59        };
60
61        Self {
62            code: "EACCES",
63            message,
64        }
65    }
66}
67
68impl fmt::Display for PermissionError {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        write!(f, "{}: {}", self.code, self.message)
71    }
72}
73
74impl Error for PermissionError {}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum FsOperation {
78    Read,
79    Write,
80    Mkdir,
81    CreateDir,
82    ReadDir,
83    Stat,
84    Remove,
85    Rename,
86    Exists,
87    Symlink,
88    ReadLink,
89    Link,
90    Chmod,
91    Chown,
92    Utimes,
93    Truncate,
94    MountSensitive,
95}
96
97impl FsOperation {
98    fn as_str(self) -> &'static str {
99        match self {
100            Self::Read => "read",
101            Self::Write => "write",
102            Self::Mkdir => "mkdir",
103            Self::CreateDir => "createDir",
104            Self::ReadDir => "readdir",
105            Self::Stat => "stat",
106            Self::Remove => "rm",
107            Self::Rename => "rename",
108            Self::Exists => "exists",
109            Self::Symlink => "symlink",
110            Self::ReadLink => "readlink",
111            Self::Link => "link",
112            Self::Chmod => "chmod",
113            Self::Chown => "chown",
114            Self::Utimes => "utimes",
115            Self::Truncate => "truncate",
116            Self::MountSensitive => "mount",
117        }
118    }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct FsAccessRequest {
123    pub vm_id: String,
124    pub op: FsOperation,
125    pub path: String,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum NetworkOperation {
130    Fetch,
131    Http,
132    Dns,
133    Listen,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct NetworkAccessRequest {
138    pub vm_id: String,
139    pub op: NetworkOperation,
140    pub resource: String,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct CommandAccessRequest {
145    pub vm_id: String,
146    pub command: String,
147    pub args: Vec<String>,
148    pub cwd: Option<String>,
149    pub env: BTreeMap<String, String>,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum EnvironmentOperation {
154    Read,
155    Write,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct EnvAccessRequest {
160    pub vm_id: String,
161    pub op: EnvironmentOperation,
162    pub key: String,
163    pub value: Option<String>,
164}
165
166#[derive(Clone, Default)]
167pub struct Permissions {
168    pub filesystem: Option<FsPermissionCheck>,
169    /// Whether filesystem permission checks are unconditionally permissive.
170    ///
171    /// This avoids resolving every path solely to evaluate an already-known
172    /// allow decision. Rule-based policies must leave this disabled so their
173    /// checks continue to receive symlink-resolved paths.
174    pub filesystem_unrestricted: bool,
175    pub network: Option<NetworkPermissionCheck>,
176    pub child_process: Option<CommandPermissionCheck>,
177    pub environment: Option<EnvironmentPermissionCheck>,
178}
179
180impl Permissions {
181    pub fn allow_all() -> Self {
182        Self {
183            filesystem: Some(Arc::new(|_: &FsAccessRequest| PermissionDecision::allow())),
184            filesystem_unrestricted: true,
185            network: Some(Arc::new(|_: &NetworkAccessRequest| {
186                PermissionDecision::allow()
187            })),
188            child_process: Some(Arc::new(|_: &CommandAccessRequest| {
189                PermissionDecision::allow()
190            })),
191            environment: Some(Arc::new(|_: &EnvAccessRequest| PermissionDecision::allow())),
192        }
193    }
194}
195
196pub fn permission_glob_matches(pattern: &str, value: &str) -> bool {
197    fn matches(
198        pattern: &[u8],
199        value: &[u8],
200        pattern_index: usize,
201        value_index: usize,
202        memo: &mut HashMap<(usize, usize), bool>,
203    ) -> bool {
204        if let Some(result) = memo.get(&(pattern_index, value_index)) {
205            return *result;
206        }
207
208        let result = if pattern_index == pattern.len() {
209            value_index == value.len()
210        } else {
211            match pattern[pattern_index] {
212                b'?' => {
213                    value_index < value.len()
214                        && value[value_index] != b'/'
215                        && matches(pattern, value, pattern_index + 1, value_index + 1, memo)
216                }
217                b'*' => {
218                    let mut next_pattern_index = pattern_index;
219                    while next_pattern_index < pattern.len() && pattern[next_pattern_index] == b'*'
220                    {
221                        next_pattern_index += 1;
222                    }
223
224                    if matches(pattern, value, next_pattern_index, value_index, memo) {
225                        true
226                    } else {
227                        let crosses_separators = next_pattern_index - pattern_index > 1;
228                        let mut next_value_index = value_index;
229                        while next_value_index < value.len()
230                            && (crosses_separators || value[next_value_index] != b'/')
231                        {
232                            next_value_index += 1;
233                            if matches(pattern, value, next_pattern_index, next_value_index, memo) {
234                                return true;
235                            }
236                        }
237                        false
238                    }
239                }
240                expected => {
241                    value_index < value.len()
242                        && expected == value[value_index]
243                        && matches(pattern, value, pattern_index + 1, value_index + 1, memo)
244                }
245            }
246        };
247
248        memo.insert((pattern_index, value_index), result);
249        result
250    }
251
252    matches(
253        pattern.as_bytes(),
254        value.as_bytes(),
255        0,
256        0,
257        &mut HashMap::new(),
258    )
259}
260
261pub fn filter_env(
262    vm_id: &str,
263    env: &BTreeMap<String, String>,
264    permissions: &Permissions,
265) -> BTreeMap<String, String> {
266    let Some(check) = permissions.environment.as_ref() else {
267        return BTreeMap::new();
268    };
269
270    env.iter()
271        .filter_map(|(key, value)| {
272            let request = EnvAccessRequest {
273                vm_id: vm_id.to_owned(),
274                op: EnvironmentOperation::Read,
275                key: key.clone(),
276                value: Some(value.clone()),
277            };
278            let decision = check(&request);
279            decision.allow.then(|| (key.clone(), value.clone()))
280        })
281        .collect()
282}
283
284pub fn check_command_execution(
285    vm_id: &str,
286    permissions: &Permissions,
287    command: &str,
288    args: &[String],
289    cwd: Option<&str>,
290    env: &BTreeMap<String, String>,
291) -> Result<(), PermissionError> {
292    let Some(check) = permissions.child_process.as_ref() else {
293        return Err(PermissionError::access_denied(
294            format!("spawn '{command}'"),
295            None,
296        ));
297    };
298
299    let request = CommandAccessRequest {
300        vm_id: vm_id.to_owned(),
301        command: command.to_owned(),
302        args: args.to_vec(),
303        cwd: cwd.map(ToOwned::to_owned),
304        env: env.clone(),
305    };
306    let decision = check(&request);
307    if decision.allow {
308        Ok(())
309    } else {
310        Err(PermissionError::access_denied(
311            format!("spawn '{command}'"),
312            decision.reason.as_deref(),
313        ))
314    }
315}
316
317pub fn check_network_access(
318    vm_id: &str,
319    permissions: &Permissions,
320    op: NetworkOperation,
321    resource: &str,
322) -> Result<(), PermissionError> {
323    let Some(check) = permissions.network.as_ref() else {
324        return Err(PermissionError::access_denied(resource, None));
325    };
326
327    let request = NetworkAccessRequest {
328        vm_id: vm_id.to_owned(),
329        op,
330        resource: resource.to_owned(),
331    };
332    let decision = check(&request);
333    if decision.allow {
334        Ok(())
335    } else {
336        Err(PermissionError::access_denied(
337            resource,
338            decision.reason.as_deref(),
339        ))
340    }
341}
342
343#[derive(Clone)]
344pub struct PermissionedFileSystem<F> {
345    inner: F,
346    vm_id: String,
347    permissions: Permissions,
348}
349
350impl<F> PermissionedFileSystem<F> {
351    pub fn new(inner: F, vm_id: impl Into<String>, permissions: Permissions) -> Self {
352        Self {
353            inner,
354            vm_id: vm_id.into(),
355            permissions,
356        }
357    }
358
359    pub fn into_inner(self) -> F {
360        self.inner
361    }
362
363    pub fn inner(&self) -> &F {
364        &self.inner
365    }
366
367    pub fn inner_mut(&mut self) -> &mut F {
368        &mut self.inner
369    }
370
371    pub fn set_permissions(&mut self, permissions: Permissions) {
372        self.permissions = permissions;
373    }
374
375    fn check(&self, op: FsOperation, path: &str) -> VfsResult<()> {
376        validate_path(path)?;
377        // Standard emulated character devices (/dev/null, /dev/zero, /dev/urandom,
378        // /dev/std{in,out,err}) are world-accessible on Linux and have no host
379        // backing; the device layer enforces their fixed semantics. Exempt them from
380        // the VM file-permission policy so guest fs ops on them (readFileSync /
381        // existsSync / redirects) behave like native Linux regardless of policy.
382        if crate::device_layer::is_standard_device_path(path) {
383            return Ok(());
384        }
385        let Some(check) = self.permissions.filesystem.as_ref() else {
386            return Err(VfsError::access_denied(op.as_str(), path, None));
387        };
388
389        let request = FsAccessRequest {
390            vm_id: self.vm_id.clone(),
391            op,
392            path: path.to_owned(),
393        };
394        let decision = check(&request);
395        if decision.allow {
396            Ok(())
397        } else {
398            Err(VfsError::access_denied(
399                op.as_str(),
400                path,
401                decision.reason.as_deref(),
402            ))
403        }
404    }
405}
406
407impl<F: VirtualFileSystem> PermissionedFileSystem<F> {
408    fn check_not_immutable(&mut self, path: &str, op: &'static str) -> VfsResult<()> {
409        self.check_not_immutable_with_follow(path, op, true)
410    }
411
412    fn check_entry_not_immutable(&mut self, path: &str, op: &'static str) -> VfsResult<()> {
413        self.check_not_immutable_with_follow(path, op, false)
414    }
415
416    fn check_not_immutable_with_follow(
417        &mut self,
418        path: &str,
419        op: &'static str,
420        follow_symlinks: bool,
421    ) -> VfsResult<()> {
422        match self.inner.get_xattr(path, IMMUTABLE_XATTR, follow_symlinks) {
423            Ok(value) if value == b"1" => Err(VfsError::permission_denied(op, path)),
424            Ok(_) => Ok(()),
425            Err(error)
426                if matches!(
427                    error.code(),
428                    "ENODATA" | "ENOATTR" | "ENOENT" | "EOPNOTSUPP"
429                ) =>
430            {
431                Ok(())
432            }
433            Err(error) => Err(error),
434        }
435    }
436
437    fn resolved_existing_path(&self, path: &str) -> VfsResult<String> {
438        if self.permissions.filesystem_unrestricted {
439            validate_path(path)?;
440            return Ok(crate::vfs::normalize_path(path));
441        }
442        self.inner.realpath(path)
443    }
444
445    fn resolved_destination_path(&self, path: &str) -> VfsResult<String> {
446        if self.permissions.filesystem_unrestricted {
447            validate_path(path)?;
448            return Ok(crate::vfs::normalize_path(path));
449        }
450        let normalized = crate::vfs::normalize_path(path);
451        if normalized == "/" {
452            return Ok(normalized);
453        }
454
455        let parent = Path::new(&normalized)
456            .parent()
457            .unwrap_or_else(|| Path::new("/"))
458            .to_string_lossy()
459            .into_owned();
460        let basename = Path::new(&normalized)
461            .file_name()
462            .map(|value| value.to_string_lossy().into_owned())
463            .unwrap_or_default();
464
465        let mut candidate = parent;
466        let mut unresolved_segments = Vec::new();
467
468        let resolved_parent = loop {
469            match self.inner.realpath(&candidate) {
470                Ok(resolved) => break resolved,
471                Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => {
472                    if candidate == "/" {
473                        break String::from("/");
474                    }
475                    let candidate_path = Path::new(&candidate);
476                    if let Some(segment) = candidate_path.file_name() {
477                        unresolved_segments.push(segment.to_string_lossy().into_owned());
478                    }
479                    candidate = candidate_path
480                        .parent()
481                        .unwrap_or_else(|| Path::new("/"))
482                        .to_string_lossy()
483                        .into_owned();
484                }
485                Err(error) => return Err(error),
486            }
487        };
488
489        let mut resolved = resolved_parent;
490        for segment in unresolved_segments.iter().rev() {
491            if resolved == "/" {
492                resolved = format!("/{segment}");
493            } else {
494                resolved = format!("{resolved}/{segment}");
495            }
496        }
497
498        if resolved == "/" {
499            Ok(format!("/{basename}"))
500        } else {
501            Ok(format!("{resolved}/{basename}"))
502        }
503    }
504
505    fn permission_subject(&self, op: FsOperation, path: &str) -> VfsResult<String> {
506        validate_path(path)?;
507        match op {
508            FsOperation::Read
509            | FsOperation::ReadDir
510            | FsOperation::Stat
511            | FsOperation::ReadLink
512            | FsOperation::Chmod
513            | FsOperation::Chown
514            | FsOperation::Utimes
515            | FsOperation::Truncate => self.resolved_existing_path(path),
516            FsOperation::Exists | FsOperation::Write => self
517                .resolved_existing_path(path)
518                .or_else(|_| self.resolved_destination_path(path)),
519            FsOperation::Mkdir
520            | FsOperation::CreateDir
521            | FsOperation::Rename
522            | FsOperation::Symlink
523            | FsOperation::Link
524            | FsOperation::MountSensitive
525            | FsOperation::Remove => self.resolved_destination_path(path),
526        }
527    }
528
529    fn check_subject(&self, op: FsOperation, path: &str) -> VfsResult<()> {
530        let subject = self.permission_subject(op, path)?;
531        self.check(op, &subject)
532    }
533
534    fn check_existing_subject(&self, op: FsOperation, path: &str) -> VfsResult<()> {
535        validate_path(path)?;
536        let subject = self.resolved_existing_path(path)?;
537        self.check(op, &subject)
538    }
539
540    fn check_destination_subject(&self, op: FsOperation, path: &str) -> VfsResult<()> {
541        validate_path(path)?;
542        let subject = self.resolved_destination_path(path)?;
543        self.check(op, &subject)
544    }
545
546    pub fn check_path(&self, op: FsOperation, path: &str) -> VfsResult<()> {
547        self.check_subject(op, path)
548    }
549
550    pub fn check_virtual_path(&self, op: FsOperation, path: &str) -> VfsResult<()> {
551        self.check(op, path)
552    }
553
554    pub fn exists(&self, path: &str) -> VfsResult<bool> {
555        if let Err(error) = self.check_subject(FsOperation::Exists, path) {
556            if matches!(error.code(), "EACCES" | "ENOENT" | "ENOTDIR" | "ELOOP") {
557                return Ok(false);
558            }
559            return Err(error);
560        }
561        Ok(self.inner.exists(path))
562    }
563}
564
565impl<F: VirtualFileSystem> VirtualFileSystem for PermissionedFileSystem<F> {
566    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
567        self.check_subject(FsOperation::Read, path)?;
568        self.inner.read_file(path)
569    }
570
571    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
572        self.check_subject(FsOperation::ReadDir, path)?;
573        self.inner.read_dir(path)
574    }
575
576    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
577        self.check_subject(FsOperation::ReadDir, path)?;
578        self.inner.read_dir_limited(path, max_entries)
579    }
580
581    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
582        self.check_subject(FsOperation::ReadDir, path)?;
583        self.inner.read_dir_with_types(path)
584    }
585
586    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
587        self.check_subject(FsOperation::Write, path)?;
588        self.check_not_immutable(path, "write")?;
589        self.inner.write_file(path, content)
590    }
591
592    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
593        self.check_subject(FsOperation::Write, path)?;
594        self.inner.create_file_exclusive(path, content)
595    }
596
597    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
598        self.check_subject(FsOperation::Write, path)?;
599        self.check_not_immutable(path, "write")?;
600        self.inner.append_file(path, content)
601    }
602
603    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
604        self.check_subject(FsOperation::CreateDir, path)?;
605        self.inner.create_dir(path)
606    }
607
608    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
609        self.check_subject(FsOperation::Mkdir, path)?;
610        self.inner.mkdir(path, recursive)
611    }
612
613    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
614        self.check_subject(FsOperation::Write, path)?;
615        self.inner.mknod(path, mode, rdev)
616    }
617
618    fn exists(&self, path: &str) -> bool {
619        PermissionedFileSystem::exists(self, path).unwrap_or(false)
620    }
621
622    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
623        self.check_subject(FsOperation::Stat, path)
624            .map_err(|error| {
625                VfsError::new(
626                    error.code(),
627                    format!("permission path resolution for stat '{path}' failed: {error}"),
628                )
629            })?;
630        self.inner.stat(path).map_err(|error| {
631            VfsError::new(
632                error.code(),
633                format!("storage stat for '{path}' failed: {error}"),
634            )
635        })
636    }
637
638    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
639        self.check_subject(FsOperation::Remove, path)?;
640        self.check_entry_not_immutable(path, "unlink")?;
641        self.inner.remove_file(path)
642    }
643
644    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
645        self.check_subject(FsOperation::Remove, path)?;
646        self.check_entry_not_immutable(path, "rmdir")?;
647        self.inner.remove_dir(path)
648    }
649
650    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
651        self.check_subject(FsOperation::Rename, old_path)?;
652        self.check_subject(FsOperation::Rename, new_path)?;
653        self.check_entry_not_immutable(old_path, "rename")?;
654        self.check_entry_not_immutable(new_path, "rename")?;
655        self.inner.rename(old_path, new_path)
656    }
657
658    fn realpath(&self, path: &str) -> VfsResult<String> {
659        self.check_subject(FsOperation::Read, path)?;
660        self.inner.realpath(path)
661    }
662
663    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
664        self.check_subject(FsOperation::Symlink, link_path)?;
665        self.inner.symlink(target, link_path)
666    }
667
668    fn read_link(&self, path: &str) -> VfsResult<String> {
669        // Authorize the parent-symlink-resolved path (without following the
670        // final component, matching `lstat`/`readlink` semantics). A lexical
671        // check would let a symlink whose parent resolves into a denied prefix
672        // disclose link targets of permission-denied paths.
673        validate_path(path)?;
674        let subject = self.resolved_destination_path(path)?;
675        self.check(FsOperation::ReadLink, &subject)?;
676        self.inner.read_link(path)
677    }
678
679    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
680        // Authorize the parent-symlink-resolved path (see `read_link`); a
681        // lexical check would leak metadata (size/mode/mtime/inode) of files
682        // under a permission-denied prefix reached via a symlinked parent.
683        validate_path(path)?;
684        let subject = self.resolved_destination_path(path)?;
685        self.check(FsOperation::Stat, &subject)?;
686        self.inner.lstat(path)
687    }
688
689    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
690        self.check_existing_subject(FsOperation::Link, old_path)?;
691        self.check_destination_subject(FsOperation::Link, new_path)?;
692        self.check_not_immutable(old_path, "link")?;
693        self.inner.link(old_path, new_path)
694    }
695
696    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
697        self.check_subject(FsOperation::Chmod, path)?;
698        self.check_not_immutable(path, "chmod")?;
699        self.inner.chmod(path, mode)
700    }
701
702    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
703        self.check_subject(FsOperation::Chown, path)?;
704        self.check_not_immutable(path, "chown")?;
705        self.inner.chown(path, uid, gid)
706    }
707
708    fn chown_spec(
709        &mut self,
710        path: &str,
711        uid: u32,
712        gid: u32,
713        follow_symlinks: bool,
714    ) -> VfsResult<()> {
715        if follow_symlinks {
716            self.check_subject(FsOperation::Chown, path)?;
717        } else {
718            validate_path(path)?;
719            let subject = self.resolved_destination_path(path)?;
720            self.check(FsOperation::Chown, &subject)?;
721        }
722        self.inner.chown_spec(path, uid, gid, follow_symlinks)
723    }
724
725    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
726        self.chown_spec(path, uid, gid, false)
727    }
728
729    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
730        if follow_symlinks {
731            self.check_subject(FsOperation::Read, path)?;
732        } else {
733            validate_path(path)?;
734            let subject = self.resolved_destination_path(path)?;
735            self.check(FsOperation::Read, &subject)?;
736        }
737        self.inner.get_xattr(path, name, follow_symlinks)
738    }
739
740    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
741        if follow_symlinks {
742            self.check_subject(FsOperation::Read, path)?;
743        } else {
744            validate_path(path)?;
745            let subject = self.resolved_destination_path(path)?;
746            self.check(FsOperation::Read, &subject)?;
747        }
748        self.inner.list_xattrs(path, follow_symlinks)
749    }
750
751    fn set_xattr(
752        &mut self,
753        path: &str,
754        name: &str,
755        value: Vec<u8>,
756        flags: u32,
757        follow_symlinks: bool,
758    ) -> VfsResult<()> {
759        if follow_symlinks {
760            self.check_subject(FsOperation::Write, path)?;
761        } else {
762            validate_path(path)?;
763            let subject = self.resolved_destination_path(path)?;
764            self.check(FsOperation::Write, &subject)?;
765        }
766        self.check_not_immutable(path, "setxattr")?;
767        self.inner
768            .set_xattr(path, name, value, flags, follow_symlinks)
769    }
770
771    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
772        if follow_symlinks {
773            self.check_subject(FsOperation::Write, path)?;
774        } else {
775            validate_path(path)?;
776            let subject = self.resolved_destination_path(path)?;
777            self.check(FsOperation::Write, &subject)?;
778        }
779        if name != IMMUTABLE_XATTR {
780            self.check_not_immutable(path, "removexattr")?;
781        }
782        self.inner.remove_xattr(path, name, follow_symlinks)
783    }
784
785    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
786        self.check_subject(FsOperation::Utimes, path)?;
787        self.check_not_immutable(path, "utimes")?;
788        self.inner.utimes(path, atime_ms, mtime_ms)
789    }
790
791    fn utimes_spec(
792        &mut self,
793        path: &str,
794        atime: VirtualUtimeSpec,
795        mtime: VirtualUtimeSpec,
796        follow_symlinks: bool,
797    ) -> VfsResult<()> {
798        self.check_subject(FsOperation::Utimes, path)?;
799        self.check_not_immutable(path, "utimes")?;
800        self.inner.utimes_spec(path, atime, mtime, follow_symlinks)
801    }
802
803    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
804        self.check_subject(FsOperation::Truncate, path)?;
805        self.check_not_immutable(path, "truncate")?;
806        self.inner.truncate(path, length)
807    }
808
809    fn sync(&mut self, path: &str) -> VfsResult<()> {
810        self.inner.sync(path)
811    }
812
813    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
814        self.check_subject(FsOperation::Write, path)?;
815        self.check_not_immutable(path, "fallocate")?;
816        self.inner.allocate(path, offset, length)
817    }
818
819    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
820        self.check_subject(FsOperation::Write, path)?;
821        self.check_not_immutable(path, "fallocate")?;
822        self.inner.insert_range(path, offset, length)
823    }
824
825    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
826        self.check_subject(FsOperation::Write, path)?;
827        self.check_not_immutable(path, "fallocate")?;
828        self.inner.collapse_range(path, offset, length)
829    }
830
831    fn zero_range(
832        &mut self,
833        path: &str,
834        offset: u64,
835        length: u64,
836        keep_size: bool,
837    ) -> VfsResult<()> {
838        self.check_subject(FsOperation::Write, path)?;
839        self.check_not_immutable(path, "fallocate")?;
840        self.inner.zero_range(path, offset, length, keep_size)
841    }
842
843    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
844        self.check_subject(FsOperation::Write, path)?;
845        self.check_not_immutable(path, "fallocate")?;
846        self.inner.punch_hole(path, offset, length)
847    }
848
849    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
850        self.check_subject(FsOperation::Read, path)?;
851        self.inner.allocated_ranges(path)
852    }
853
854    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
855        self.check_subject(FsOperation::Read, path)?;
856        self.inner.unwritten_ranges(path)
857    }
858
859    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
860        self.check_subject(FsOperation::Read, path)?;
861        self.inner.pread(path, offset, length)
862    }
863
864    fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
865        self.check_subject(FsOperation::Write, path)?;
866        self.check_not_immutable(path, "write")?;
867        self.inner.pwrite(path, content, offset)
868    }
869}