Skip to main content

agentos_kernel/
device_layer.rs

1use crate::vfs::{
2    VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, VirtualUtimeSpec,
3};
4use getrandom::getrandom;
5use web_time::{SystemTime, UNIX_EPOCH};
6
7const DEVICE_PATHS: &[&str] = &[
8    "/dev/null",
9    "/dev/zero",
10    "/dev/stdin",
11    "/dev/stdout",
12    "/dev/stderr",
13    "/dev/urandom",
14];
15
16const DEVICE_DIRS: &[&str] = &["/dev/fd", "/dev/pts"];
17const DEFAULT_STREAM_DEVICE_READ_BYTES: usize = 4096;
18const DEV_DIR_ENTRIES: &[(&str, bool)] = &[
19    ("null", false),
20    ("zero", false),
21    ("stdin", false),
22    ("stdout", false),
23    ("stderr", false),
24    ("urandom", false),
25    ("fd", true),
26];
27
28#[derive(Debug, Clone)]
29pub struct DeviceLayer<V> {
30    inner: V,
31}
32
33pub fn create_device_layer<V>(vfs: V) -> DeviceLayer<V> {
34    DeviceLayer { inner: vfs }
35}
36
37impl<V> DeviceLayer<V> {
38    pub fn into_inner(self) -> V {
39        self.inner
40    }
41
42    pub fn inner(&self) -> &V {
43        &self.inner
44    }
45
46    pub fn inner_mut(&mut self) -> &mut V {
47        &mut self.inner
48    }
49}
50
51impl<V: VirtualFileSystem> VirtualFileSystem for DeviceLayer<V> {
52    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
53        if let Some(bytes) = read_stream_device(path, DEFAULT_STREAM_DEVICE_READ_BYTES) {
54            return bytes;
55        }
56
57        if self
58            .inner
59            .stat(path)
60            .is_ok_and(|stat| is_null_character_device(&stat))
61        {
62            return Ok(Vec::new());
63        }
64
65        self.inner.read_file(path)
66    }
67
68    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
69        if path == "/dev" {
70            return Ok(DEV_DIR_ENTRIES
71                .iter()
72                .map(|(name, _)| String::from(*name))
73                .collect());
74        }
75        if DEVICE_DIRS.contains(&path) {
76            return Ok(Vec::new());
77        }
78        self.inner.read_dir(path)
79    }
80
81    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
82        if path == "/dev" {
83            let entries = DEV_DIR_ENTRIES
84                .iter()
85                .map(|(name, _)| String::from(*name))
86                .collect::<Vec<_>>();
87            if entries.len() > max_entries {
88                return Err(VfsError::new(
89                    "ENOMEM",
90                    format!(
91                        "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
92                    ),
93                ));
94            }
95            return Ok(entries);
96        }
97        if DEVICE_DIRS.contains(&path) {
98            return Ok(Vec::new());
99        }
100        self.inner.read_dir_limited(path, max_entries)
101    }
102
103    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
104        if path == "/dev" {
105            return Ok(DEV_DIR_ENTRIES
106                .iter()
107                .map(|(name, is_directory)| VirtualDirEntry {
108                    name: String::from(*name),
109                    is_directory: *is_directory,
110                    is_symbolic_link: false,
111                })
112                .collect());
113        }
114        if DEVICE_DIRS.contains(&path) {
115            return Ok(Vec::new());
116        }
117        self.inner.read_dir_with_types(path)
118    }
119
120    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
121        if is_sink_device_path(path)
122            || self
123                .inner
124                .stat(path)
125                .is_ok_and(|stat| is_null_character_device(&stat))
126        {
127            let _ = content.into();
128            return Ok(());
129        }
130        self.inner.write_file(path, content)
131    }
132
133    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
134        if is_device_path(path) || is_device_dir(path) {
135            let _ = content.into();
136            return Err(VfsError::new(
137                "EEXIST",
138                format!("file already exists, open '{path}'"),
139            ));
140        }
141        self.inner.create_file_exclusive(path, content)
142    }
143
144    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
145        if is_sink_device_path(path)
146            || self
147                .inner
148                .stat(path)
149                .is_ok_and(|stat| is_null_character_device(&stat))
150        {
151            return Ok(content.into().len() as u64);
152        }
153        self.inner.append_file(path, content)
154    }
155
156    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
157        if is_device_dir(path) {
158            return Ok(());
159        }
160        self.inner.create_dir(path)
161    }
162
163    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
164        if is_device_dir(path) {
165            return Ok(());
166        }
167        self.inner.mkdir(path, recursive)
168    }
169
170    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
171        if is_device_path(path) || is_device_dir(path) {
172            return Err(VfsError::new(
173                "EEXIST",
174                format!("device already exists: {path}"),
175            ));
176        }
177        self.inner.mknod(path, mode, rdev)
178    }
179
180    fn exists(&self, path: &str) -> bool {
181        if is_device_path(path) || is_device_dir(path) {
182            return true;
183        }
184        self.inner.exists(path)
185    }
186
187    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
188        if is_device_path(path) {
189            return Ok(device_stat(path));
190        }
191        if is_device_dir(path) {
192            return Ok(device_dir_stat(path));
193        }
194        self.inner.stat(path)
195    }
196
197    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
198        if is_device_path(path) {
199            return Err(VfsError::permission_denied("unlink", path));
200        }
201        self.inner.remove_file(path)
202    }
203
204    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
205        if is_device_dir(path) {
206            return Err(VfsError::permission_denied("rmdir", path));
207        }
208        self.inner.remove_dir(path)
209    }
210
211    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
212        if is_device_path(old_path) || is_device_path(new_path) {
213            return Err(VfsError::permission_denied("rename", old_path));
214        }
215        self.inner.rename(old_path, new_path)
216    }
217
218    fn realpath(&self, path: &str) -> VfsResult<String> {
219        if is_device_path(path) || is_device_dir(path) {
220            return Ok(String::from(path));
221        }
222        self.inner.realpath(path)
223    }
224
225    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
226        self.inner.symlink(target, link_path)
227    }
228
229    fn read_link(&self, path: &str) -> VfsResult<String> {
230        self.inner.read_link(path)
231    }
232
233    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
234        if is_device_path(path) {
235            return Ok(device_stat(path));
236        }
237        if is_device_dir(path) {
238            return Ok(device_dir_stat(path));
239        }
240        self.inner.lstat(path)
241    }
242
243    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
244        if is_device_path(old_path) {
245            return Err(VfsError::permission_denied("link", old_path));
246        }
247        self.inner.link(old_path, new_path)
248    }
249
250    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
251        if is_device_path(path) {
252            return Ok(());
253        }
254        self.inner.chmod(path, mode)
255    }
256
257    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
258        if is_device_path(path) {
259            return Ok(());
260        }
261        self.inner.chown(path, uid, gid)
262    }
263
264    fn chown_spec(
265        &mut self,
266        path: &str,
267        uid: u32,
268        gid: u32,
269        follow_symlinks: bool,
270    ) -> VfsResult<()> {
271        if is_device_path(path) {
272            return Ok(());
273        }
274        self.inner.chown_spec(path, uid, gid, follow_symlinks)
275    }
276
277    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
278        if is_device_path(path) || is_device_dir(path) {
279            return Err(VfsError::new(
280                "EOPNOTSUPP",
281                format!("extended attributes are not supported for device {path}"),
282            ));
283        }
284        self.inner.get_xattr(path, name, follow_symlinks)
285    }
286
287    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
288        if is_device_path(path) || is_device_dir(path) {
289            return Err(VfsError::new(
290                "EOPNOTSUPP",
291                format!("extended attributes are not supported for device {path}"),
292            ));
293        }
294        self.inner.list_xattrs(path, follow_symlinks)
295    }
296
297    fn set_xattr(
298        &mut self,
299        path: &str,
300        name: &str,
301        value: Vec<u8>,
302        flags: u32,
303        follow_symlinks: bool,
304    ) -> VfsResult<()> {
305        if is_device_path(path) || is_device_dir(path) {
306            return Err(VfsError::new(
307                "EOPNOTSUPP",
308                format!("extended attributes are not supported for device {path}"),
309            ));
310        }
311        self.inner
312            .set_xattr(path, name, value, flags, follow_symlinks)
313    }
314
315    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
316        if is_device_path(path) || is_device_dir(path) {
317            return Err(VfsError::new(
318                "EOPNOTSUPP",
319                format!("extended attributes are not supported for device {path}"),
320            ));
321        }
322        self.inner.remove_xattr(path, name, follow_symlinks)
323    }
324
325    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
326        if is_device_path(path) {
327            return Ok(());
328        }
329        self.inner.utimes(path, atime_ms, mtime_ms)
330    }
331
332    fn utimes_spec(
333        &mut self,
334        path: &str,
335        atime: VirtualUtimeSpec,
336        mtime: VirtualUtimeSpec,
337        follow_symlinks: bool,
338    ) -> VfsResult<()> {
339        if is_device_path(path) {
340            return Ok(());
341        }
342        self.inner.utimes_spec(path, atime, mtime, follow_symlinks)
343    }
344
345    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
346        if is_sink_device_path(path)
347            || self
348                .inner
349                .stat(path)
350                .is_ok_and(|stat| is_null_character_device(&stat))
351        {
352            let _ = length;
353            return Ok(());
354        }
355        self.inner.truncate(path, length)
356    }
357
358    fn sync(&mut self, path: &str) -> VfsResult<()> {
359        if is_device_path(path) || is_device_dir(path) {
360            return Err(VfsError::new(
361                "EINVAL",
362                format!("device does not support sync: {path}"),
363            ));
364        }
365        self.inner.sync(path)
366    }
367
368    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
369        if is_device_path(path) || is_device_dir(path) {
370            return Err(VfsError::new(
371                "EINVAL",
372                format!("device does not support allocation: {path}"),
373            ));
374        }
375        self.inner.allocate(path, offset, length)
376    }
377
378    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
379        if is_device_path(path) || is_device_dir(path) {
380            return Err(VfsError::new(
381                "EINVAL",
382                format!("device does not support insert range: {path}"),
383            ));
384        }
385        self.inner.insert_range(path, offset, length)
386    }
387
388    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
389        if is_device_path(path) || is_device_dir(path) {
390            return Err(VfsError::new(
391                "EINVAL",
392                format!("device does not support collapse range: {path}"),
393            ));
394        }
395        self.inner.collapse_range(path, offset, length)
396    }
397
398    fn zero_range(
399        &mut self,
400        path: &str,
401        offset: u64,
402        length: u64,
403        keep_size: bool,
404    ) -> VfsResult<()> {
405        if is_device_path(path) || is_device_dir(path) {
406            return Err(VfsError::new(
407                "EINVAL",
408                format!("device does not support zero range: {path}"),
409            ));
410        }
411        self.inner.zero_range(path, offset, length, keep_size)
412    }
413
414    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
415        if is_device_path(path) || is_device_dir(path) {
416            return Err(VfsError::new(
417                "EINVAL",
418                format!("device does not support hole punching: {path}"),
419            ));
420        }
421        self.inner.punch_hole(path, offset, length)
422    }
423
424    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
425        if is_device_path(path) || is_device_dir(path) {
426            return Err(VfsError::new(
427                "EINVAL",
428                format!("device does not support extent mapping: {path}"),
429            ));
430        }
431        self.inner.allocated_ranges(path)
432    }
433
434    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
435        if is_device_path(path) || is_device_dir(path) {
436            return Err(VfsError::new(
437                "EINVAL",
438                format!("device does not support extent mapping: {path}"),
439            ));
440        }
441        self.inner.unwritten_ranges(path)
442    }
443
444    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
445        if let Some(bytes) = read_stream_device(path, length) {
446            return bytes;
447        }
448
449        if self
450            .inner
451            .stat(path)
452            .is_ok_and(|stat| is_null_character_device(&stat))
453        {
454            return Ok(Vec::new());
455        }
456
457        self.inner.pread(path, offset, length)
458    }
459
460    fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
461        if is_sink_device_path(path)
462            || self
463                .inner
464                .stat(path)
465                .is_ok_and(|stat| is_null_character_device(&stat))
466        {
467            let _ = (content.into(), offset);
468            return Ok(());
469        }
470        self.inner.pwrite(path, content, offset)
471    }
472}
473
474fn is_device_path(path: &str) -> bool {
475    DEVICE_PATHS.contains(&path) || path.starts_with("/dev/fd/") || path.starts_with("/dev/pts/")
476}
477
478/// Standard emulated character devices (`/dev/null`, `/dev/zero`, `/dev/urandom`,
479/// `/dev/std{in,out,err}`). On Linux these are world-readable/writable and have no
480/// host backing — they are pure kernel emulations whose semantics this device layer
481/// already enforces (read/write for the stream devices, `EPERM` for unlink/rename).
482/// The permission layer therefore treats them as always accessible, so guest fs ops
483/// (`readFileSync`/`existsSync`/redirects on `/dev/null`, …) behave like native Linux
484/// regardless of the VM file-permission policy. Excludes `/dev/fd` and `/dev/pts`,
485/// which carry process-specific semantics the policy may legitimately govern.
486pub fn is_standard_device_path(path: &str) -> bool {
487    DEVICE_PATHS.contains(&path)
488}
489
490fn is_sink_device_path(path: &str) -> bool {
491    matches!(
492        path,
493        "/dev/null" | "/dev/zero" | "/dev/stdout" | "/dev/stderr" | "/dev/urandom"
494    )
495}
496
497fn is_device_dir(path: &str) -> bool {
498    path == "/dev" || DEVICE_DIRS.contains(&path)
499}
500
501fn device_stat(path: &str) -> VirtualStat {
502    let now = now_ms();
503    VirtualStat {
504        mode: 0o020666,
505        size: 0,
506        blocks: 0,
507        dev: 2,
508        rdev: device_rdev(path),
509        is_directory: false,
510        is_symbolic_link: false,
511        atime_ms: now,
512        atime_nsec: 0,
513        mtime_ms: now,
514        mtime_nsec: 0,
515        ctime_ms: now,
516        ctime_nsec: 0,
517        birthtime_ms: now,
518        ino: device_ino(path),
519        nlink: 1,
520        uid: 0,
521        gid: 0,
522    }
523}
524
525fn device_dir_stat(path: &str) -> VirtualStat {
526    let now = now_ms();
527    VirtualStat {
528        mode: 0o040755,
529        size: 0,
530        blocks: 0,
531        dev: 2,
532        rdev: 0,
533        is_directory: true,
534        is_symbolic_link: false,
535        atime_ms: now,
536        atime_nsec: 0,
537        mtime_ms: now,
538        mtime_nsec: 0,
539        ctime_ms: now,
540        ctime_nsec: 0,
541        birthtime_ms: now,
542        ino: device_ino(path),
543        nlink: 2,
544        uid: 0,
545        gid: 0,
546    }
547}
548
549fn device_ino(path: &str) -> u64 {
550    match path {
551        "/dev/null" => 0xffff_0001,
552        "/dev/zero" => 0xffff_0002,
553        "/dev/stdin" => 0xffff_0003,
554        "/dev/stdout" => 0xffff_0004,
555        "/dev/stderr" => 0xffff_0005,
556        "/dev/urandom" => 0xffff_0006,
557        _ => 0xffff_0000,
558    }
559}
560
561fn device_rdev(path: &str) -> u64 {
562    match path {
563        "/dev/null" => encode_device_id(1, 3),
564        "/dev/zero" => encode_device_id(1, 5),
565        "/dev/stdin" => encode_device_id(5, 0),
566        "/dev/stdout" => encode_device_id(5, 1),
567        "/dev/stderr" => encode_device_id(5, 2),
568        "/dev/urandom" => encode_device_id(1, 9),
569        _ => 0,
570    }
571}
572
573fn encode_device_id(major: u64, minor: u64) -> u64 {
574    (major << 8) | minor
575}
576
577fn is_null_character_device(stat: &VirtualStat) -> bool {
578    stat.mode & 0o170000 == 0o020000 && stat.rdev == encode_device_id(1, 3)
579}
580
581fn random_bytes(length: usize) -> VfsResult<Vec<u8>> {
582    let mut buffer = vec![0; length];
583    getrandom(&mut buffer)
584        .map_err(|error| VfsError::io(format!("failed to read system random bytes: {error}")))?;
585    Ok(buffer)
586}
587
588fn read_stream_device(path: &str, length: usize) -> Option<VfsResult<Vec<u8>>> {
589    match path {
590        "/dev/null" => Some(Ok(Vec::new())),
591        "/dev/zero" => Some(Ok(vec![0; length])),
592        "/dev/urandom" => Some(random_bytes(length)),
593        _ => None,
594    }
595}
596
597fn now_ms() -> u64 {
598    SystemTime::now()
599        .duration_since(UNIX_EPOCH)
600        .unwrap_or_default()
601        .as_millis() as u64
602}