Skip to main content

detcore/syscalls/
namespace.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Deterministic views of kernel namespace metadata.
10
11use std::ffi::OsStr;
12use std::os::unix::ffi::OsStrExt;
13use std::path::Component;
14use std::path::Path;
15use std::path::PathBuf;
16
17use reverie::Errno;
18use reverie::Error;
19use reverie::Guest;
20use reverie::Stack;
21use reverie::syscalls;
22use reverie::syscalls::AddrMut;
23use reverie::syscalls::MemoryAccess;
24use reverie::syscalls::PathPtr;
25use reverie::syscalls::ReadAddr;
26use reverie::syscalls::Syscall;
27
28use super::deterministic_stdio_inode;
29use super::deterministic_stdio_inode_for_resource;
30use crate::record_or_replay::RecordOrReplay;
31use crate::tool_global::determinize_inode;
32use crate::tool_local::Detcore;
33use crate::types::DetInode;
34use crate::types::RawInode;
35
36// AUTONOMOUS-BOT-IMPLEMENTED
37// TODO-HUMAN-REVIEW(#877)
38fn is_proc_id(component: &OsStr) -> bool {
39    component == "self"
40        || component == "thread-self"
41        || component.to_str().is_some_and(|value| {
42            !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())
43        })
44}
45
46// AUTONOMOUS-BOT-IMPLEMENTED
47// TODO-HUMAN-REVIEW(#877)
48fn canonical_namespace_name(name: &OsStr) -> Option<&'static [u8]> {
49    match name.to_str()? {
50        "cgroup" => Some(b"cgroup:[4026531835]"),
51        "ipc" => Some(b"ipc:[4026531839]"),
52        "mnt" => Some(b"mnt:[4026531841]"),
53        "net" => Some(b"net:[4026531840]"),
54        "pid" | "pid_for_children" => Some(b"pid:[4026531836]"),
55        "time" | "time_for_children" => Some(b"time:[4026531834]"),
56        "user" => Some(b"user:[4026531837]"),
57        "uts" => Some(b"uts:[4026531838]"),
58        _ => None,
59    }
60}
61
62// AUTONOMOUS-BOT-IMPLEMENTED
63// TODO-HUMAN-REVIEW(#877)
64fn canonical_namespace_target(path: &Path) -> Option<&'static [u8]> {
65    if !path.is_absolute() {
66        return None;
67    }
68
69    let mut parts = Vec::new();
70    for component in path.components() {
71        match component {
72            Component::RootDir => {}
73            Component::Normal(part) => parts.push(part),
74            Component::CurDir | Component::ParentDir | Component::Prefix(_) => return None,
75        }
76    }
77
78    let namespace = match parts.as_slice() {
79        [proc, subject, ns, namespace] if *proc == "proc" && is_proc_id(subject) && *ns == "ns" => {
80            namespace
81        }
82        [proc, subject, task, tid, ns, namespace]
83            if *proc == "proc"
84                && is_proc_id(subject)
85                && *task == "task"
86                && is_proc_id(tid)
87                && *ns == "ns" =>
88        {
89            namespace
90        }
91        _ => return None,
92    };
93    canonical_namespace_name(namespace)
94}
95
96// AUTONOMOUS-BOT-IMPLEMENTED
97// TODO-HUMAN-REVIEW(PR-972): Review proc-fd path alias coverage.
98fn normalized_absolute_parts(path: &Path) -> Option<Vec<&OsStr>> {
99    if !path.is_absolute() {
100        return None;
101    }
102
103    let mut parts = Vec::new();
104    for component in path.components() {
105        match component {
106            Component::RootDir | Component::CurDir => {}
107            Component::Normal(part) => parts.push(part),
108            Component::ParentDir => {
109                parts.pop()?;
110            }
111            Component::Prefix(_) => return None,
112        }
113    }
114    Some(parts)
115}
116
117fn decimal_u32(component: &OsStr) -> Option<u32> {
118    let value = component.to_str()?;
119    (!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
120        .then(|| value.parse().ok())?
121}
122
123fn decimal_fd(component: &OsStr) -> Option<i32> {
124    let value = component.to_str()?;
125    (!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
126        .then(|| value.parse().ok())?
127}
128
129/// Returns the optional numeric proc subject and the descriptor number.
130fn proc_fd_target(path: &Path) -> Option<(Option<u32>, i32)> {
131    let parts = normalized_absolute_parts(path)?;
132    match parts.as_slice() {
133        [dev, fd_dir, fd] if *dev == "dev" && *fd_dir == "fd" => Some((None, decimal_fd(fd)?)),
134        [proc, subject, fd_dir, fd] if *proc == "proc" && *fd_dir == "fd" => {
135            let subject = match subject.to_str()? {
136                "self" | "thread-self" => None,
137                _ => Some(decimal_u32(subject)?),
138            };
139            Some((subject, decimal_fd(fd)?))
140        }
141        _ => None,
142    }
143}
144
145// TODO-HUMAN-REVIEW(PR-1079): Review numeric virtual-self proc-fd path rewriting.
146fn host_self_proc_fd_alias(path: &Path, current_pid: i64) -> Option<PathBuf> {
147    let (Some(subject), fd) = proc_fd_target(path)? else {
148        return None;
149    };
150    (i64::from(subject) == current_pid).then(|| PathBuf::from(format!("/proc/self/fd/{fd}")))
151}
152
153#[derive(Debug, Eq, PartialEq)]
154struct AnonymousProcFdIdentity {
155    kind: &'static str,
156    raw_inode: RawInode,
157}
158
159// Long enough for `socket:[` + every decimal u64 inode + `]`.
160const ANONYMOUS_PROC_FD_TARGET_CAPACITY: usize = 32;
161
162fn needs_anonymous_proc_fd_scratch(buffer_present: bool, buffer_len: usize) -> bool {
163    buffer_present && buffer_len != 0 && buffer_len < ANONYMOUS_PROC_FD_TARGET_CAPACITY
164}
165
166/// Recognize only a complete kernel pipe/socket symlink target.
167fn anonymous_proc_fd_identity(target: &[u8]) -> Option<AnonymousProcFdIdentity> {
168    for (kind, prefix) in [
169        ("pipe", b"pipe:[".as_slice()),
170        ("socket", b"socket:[".as_slice()),
171    ] {
172        let Some(digits) = target
173            .strip_prefix(prefix)
174            .and_then(|rest| rest.strip_suffix(b"]"))
175        else {
176            continue;
177        };
178        if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) {
179            continue;
180        }
181        let raw_inode = std::str::from_utf8(digits).ok()?.parse().ok()?;
182        return Some(AnonymousProcFdIdentity { kind, raw_inode });
183    }
184    None
185}
186
187/// Match a raw identity against cached current-process inherited-stdio identities.
188/// Callers exclude ordinary replacement objects before filling this array.
189///
190/// Iterating in fd order deliberately matches the last-insert-wins behavior of
191/// the maps sanitizer's `stdio_by_raw_inode` table when stdio descriptors alias.
192fn deterministic_stdio_inode_for_raw(
193    raw_inode: RawInode,
194    stdio_raw_inodes: &[Option<RawInode>; 3],
195) -> Option<DetInode> {
196    let mut matched = None;
197    for (fd, cached) in stdio_raw_inodes.iter().enumerate() {
198        if *cached == Some(raw_inode) {
199            matched = deterministic_stdio_inode(fd as i32);
200        }
201    }
202    matched
203}
204
205fn canonical_anonymous_proc_fd_target(
206    identity: &AnonymousProcFdIdentity,
207    inode: DetInode,
208    buffer_len: usize,
209) -> Vec<u8> {
210    let mut target = format!("{}:[{}]", identity.kind, inode.as_raw()).into_bytes();
211    target.truncate(buffer_len);
212    target
213}
214
215impl<T: RecordOrReplay> Detcore<T> {
216    async fn canonicalize_other_proc_fd_target<G>(
217        &self,
218        guest: &mut G,
219        raw_target: &[u8],
220        buffer: Option<AddrMut<'_, libc::c_char>>,
221        buffer_len: usize,
222    ) -> Result<Option<i64>, Error>
223    where
224        G: Guest<Self>,
225    {
226        let Some(identity) = anonymous_proc_fd_identity(raw_target) else {
227            return Ok(None);
228        };
229        let mut stdio_raw_inodes = [None; 3];
230        for fd in libc::STDIN_FILENO..=libc::STDERR_FILENO {
231            stdio_raw_inodes[fd as usize] = guest
232                .thread_state()
233                .with_detfd(fd, |detfd| {
234                    deterministic_stdio_inode_for_resource(fd, detfd.resource())?;
235                    detfd.stat().map(|stat| stat.inode)
236                })
237                .ok()
238                .flatten();
239        }
240        let inode = match deterministic_stdio_inode_for_raw(identity.raw_inode, &stdio_raw_inodes) {
241            Some(inode) => inode,
242            None => determinize_inode(guest, identity.raw_inode).await.0,
243        };
244        let target = canonical_anonymous_proc_fd_target(&identity, inode, buffer_len);
245        let buffer = buffer.ok_or(Errno::EFAULT)?;
246        guest.memory().write_exact(buffer.cast(), &target)?;
247        Ok(Some(target.len() as i64))
248    }
249
250    /// Canonicalize a pipe/socket link belonging to another virtual process.
251    /// The target descriptor is not in the caller's table, so its raw readlink
252    /// bytes are the only safe identity evidence available here.
253    async fn canonicalize_other_proc_fd_readlink<G>(
254        &self,
255        guest: &mut G,
256        buffer: Option<AddrMut<'_, libc::c_char>>,
257        buffer_len: usize,
258        result: i64,
259    ) -> Result<i64, Error>
260    where
261        G: Guest<Self>,
262    {
263        let buffer = buffer.expect("a successful readlink requires a non-null buffer");
264        let observed_len = usize::try_from(result)
265            .expect("a positive readlink result must fit usize")
266            .min(buffer_len);
267        let mut observed = vec![0; observed_len];
268        guest.memory().read_exact(buffer.cast(), &mut observed)?;
269        Ok(self
270            .canonicalize_other_proc_fd_target(guest, &observed, Some(buffer), buffer_len)
271            .await?
272            .unwrap_or(result))
273    }
274
275    async fn canonicalize_namespace_readlink_result<G>(
276        &self,
277        guest: &mut G,
278        path: PathBuf,
279        buffer: Option<AddrMut<'_, libc::c_char>>,
280        buffer_len: usize,
281        result: i64,
282    ) -> Result<i64, Error>
283    where
284        G: Guest<Self>,
285    {
286        if result <= 0 {
287            return Ok(result);
288        }
289
290        let target = if let Some(target) = canonical_namespace_target(&path) {
291            target.to_vec()
292        } else if let Some((subject, fd)) = proc_fd_target(&path) {
293            if let Some(subject) = subject {
294                let current_pid = guest.inject(syscalls::Getpid::new()).await?;
295                if current_pid != i64::from(subject) {
296                    return self
297                        .canonicalize_other_proc_fd_readlink(guest, buffer, buffer_len, result)
298                        .await;
299                }
300            }
301
302            let stat = self.inject_fstat(guest, fd).await?;
303            let kind = match stat.st_mode & libc::S_IFMT {
304                libc::S_IFIFO => "pipe",
305                libc::S_IFSOCK => "socket",
306                _ => return Ok(result),
307            };
308            let inode_override = guest
309                .thread_state()
310                .with_detfd(fd, |detfd| {
311                    deterministic_stdio_inode_for_resource(fd, detfd.resource())
312                })
313                .ok()
314                .flatten();
315            let inode = match inode_override {
316                Some(inode) => inode,
317                None => determinize_inode(guest, stat.st_ino).await.0,
318            };
319            format!("{kind}:[{inode}]").into_bytes()
320        } else {
321            return Ok(result);
322        };
323
324        let written = target.len().min(buffer_len);
325        let buffer = buffer.expect("a successful readlink requires a non-null buffer");
326        guest
327            .memory()
328            .write_exact(buffer.cast(), &target[..written])?;
329        Ok(written as i64)
330    }
331    async fn write_other_proc_fd_target<G>(
332        &self,
333        guest: &mut G,
334        raw_target: &[u8],
335        buffer: Option<AddrMut<'_, libc::c_char>>,
336        buffer_len: usize,
337    ) -> Result<i64, Error>
338    where
339        G: Guest<Self>,
340    {
341        if let Some(result) = self
342            .canonicalize_other_proc_fd_target(guest, raw_target, buffer, buffer_len)
343            .await?
344        {
345            return Ok(result);
346        }
347        let written = raw_target.len().min(buffer_len);
348        let buffer = buffer.ok_or(Errno::EFAULT)?;
349        guest
350            .memory()
351            .write_exact(buffer.cast(), &raw_target[..written])?;
352        Ok(written as i64)
353    }
354
355    async fn finish_other_proc_fd_readlink<G>(
356        &self,
357        guest: &mut G,
358        call: syscalls::Readlink,
359    ) -> Result<i64, Error>
360    where
361        G: Guest<Self>,
362    {
363        let buffer = call.buf();
364        let buffer_len = call.bufsize();
365        if !needs_anonymous_proc_fd_scratch(buffer.is_some(), buffer_len) {
366            let result = self.record_or_replay(guest, call).await?;
367            if result <= 0 {
368                return Ok(result);
369            }
370            return self
371                .canonicalize_other_proc_fd_readlink(guest, buffer, buffer_len, result)
372                .await;
373        }
374
375        let mut stack = guest.stack().await;
376        let scratch = stack
377            .reserve::<[u8; ANONYMOUS_PROC_FD_TARGET_CAPACITY]>()
378            .cast::<libc::c_char>();
379        let guard = stack.commit()?;
380        let physical_call = call
381            .with_buf(Some(scratch))
382            .with_bufsize(ANONYMOUS_PROC_FD_TARGET_CAPACITY);
383        let result = self.record_or_replay(guest, physical_call).await?;
384        let length = usize::try_from(result)
385            .expect("a successful readlink result must fit usize")
386            .min(ANONYMOUS_PROC_FD_TARGET_CAPACITY);
387        let mut raw_target = vec![0; length];
388        guest.memory().read_exact(scratch.cast(), &mut raw_target)?;
389        drop(guard);
390        self.write_other_proc_fd_target(guest, &raw_target, buffer, buffer_len)
391            .await
392    }
393
394    async fn finish_other_proc_fd_readlinkat<G>(
395        &self,
396        guest: &mut G,
397        call: syscalls::Readlinkat,
398    ) -> Result<i64, Error>
399    where
400        G: Guest<Self>,
401    {
402        let buffer = call.buf();
403        let buffer_len = call.buf_len();
404        if !needs_anonymous_proc_fd_scratch(buffer.is_some(), buffer_len) {
405            let result = self.record_or_replay(guest, call).await?;
406            if result <= 0 {
407                return Ok(result);
408            }
409            return self
410                .canonicalize_other_proc_fd_readlink(guest, buffer, buffer_len, result)
411                .await;
412        }
413
414        let mut stack = guest.stack().await;
415        let scratch = stack
416            .reserve::<[u8; ANONYMOUS_PROC_FD_TARGET_CAPACITY]>()
417            .cast::<libc::c_char>();
418        let guard = stack.commit()?;
419        let physical_call = call
420            .with_buf(Some(scratch))
421            .with_buf_len(ANONYMOUS_PROC_FD_TARGET_CAPACITY);
422        let result = self.record_or_replay(guest, physical_call).await?;
423        let length = usize::try_from(result)
424            .expect("a successful readlinkat result must fit usize")
425            .min(ANONYMOUS_PROC_FD_TARGET_CAPACITY);
426        let mut raw_target = vec![0; length];
427        guest.memory().read_exact(scratch.cast(), &mut raw_target)?;
428        drop(guard);
429        self.write_other_proc_fd_target(guest, &raw_target, buffer, buffer_len)
430            .await
431    }
432
433    // AUTONOMOUS-BOT-IMPLEMENTED
434    // TODO-HUMAN-REVIEW(#877)
435    async fn finish_namespace_readlink<G, S>(
436        &self,
437        guest: &mut G,
438        path: PathBuf,
439        buffer: Option<AddrMut<'_, libc::c_char>>,
440        buffer_len: usize,
441        syscall: S,
442    ) -> Result<i64, Error>
443    where
444        G: Guest<Self>,
445        S: Into<Syscall>,
446    {
447        let result = self.record_or_replay(guest, syscall).await?;
448        self.canonicalize_namespace_readlink_result(guest, path, buffer, buffer_len, result)
449            .await
450    }
451
452    // AUTONOMOUS-BOT-IMPLEMENTED
453    // TODO-HUMAN-REVIEW(#877)
454    /// Preserve Linux readlink errors and canonicalize procfs namespace identities.
455    pub async fn handle_readlink<G: Guest<Self>>(
456        &self,
457        guest: &mut G,
458        call: syscalls::Readlink,
459    ) -> Result<i64, Error> {
460        let path: PathBuf = call.path().ok_or(Errno::EFAULT)?.read(&guest.memory())?;
461        let (host_path, other_proc_fd) = if let Some((Some(subject), _)) = proc_fd_target(&path) {
462            let current_pid = guest.inject(syscalls::Getpid::new()).await?;
463            (
464                host_self_proc_fd_alias(&path, current_pid),
465                current_pid != i64::from(subject),
466            )
467        } else {
468            (None, false)
469        };
470        if let Some(host_path) = host_path {
471            let bytes = host_path.as_os_str().as_bytes();
472            let mut path_buffer = [0_u8; 64];
473            path_buffer[..bytes.len()].copy_from_slice(bytes);
474            let mut stack = guest.stack().await;
475            let path_address = stack.push(path_buffer).cast::<libc::c_char>();
476            let stack_guard = stack.commit()?;
477            let physical_call = call.with_path(PathPtr::from_ptr(
478                path_address.as_raw() as *const libc::c_char
479            ));
480            let result = self.record_or_replay(guest, physical_call).await?;
481            drop(stack_guard);
482            return self
483                .canonicalize_namespace_readlink_result(
484                    guest,
485                    path,
486                    call.buf(),
487                    call.bufsize(),
488                    result,
489                )
490                .await;
491        }
492        if other_proc_fd {
493            return self.finish_other_proc_fd_readlink(guest, call).await;
494        }
495        self.finish_namespace_readlink(guest, path, call.buf(), call.bufsize(), call)
496            .await
497    }
498
499    // AUTONOMOUS-BOT-IMPLEMENTED
500    // TODO-HUMAN-REVIEW(#877)
501    /// Preserve Linux readlinkat errors and canonicalize absolute procfs namespace identities.
502    pub async fn handle_readlinkat<G: Guest<Self>>(
503        &self,
504        guest: &mut G,
505        call: syscalls::Readlinkat,
506    ) -> Result<i64, Error> {
507        let path: PathBuf = call.path().ok_or(Errno::EFAULT)?.read(&guest.memory())?;
508        let observed_path = if path.is_absolute() || call.dirfd() == libc::AT_FDCWD {
509            path
510        } else {
511            guest
512                .thread_state()
513                .with_detfd(call.dirfd(), |detfd| detfd.path())?
514                .map_or(path.clone(), |directory| directory.join(path))
515        };
516        if let Some((Some(subject), _)) = proc_fd_target(&observed_path) {
517            let current_pid = guest.inject(syscalls::Getpid::new()).await?;
518            if current_pid != i64::from(subject) {
519                return self.finish_other_proc_fd_readlinkat(guest, call).await;
520            }
521        }
522        self.finish_namespace_readlink(guest, observed_path, call.buf(), call.buf_len(), call)
523            .await
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn recognizes_process_and_thread_namespace_links() {
533        assert_eq!(
534            canonical_namespace_target(Path::new("/proc/self/ns/mnt")),
535            Some(b"mnt:[4026531841]".as_slice())
536        );
537        assert_eq!(
538            canonical_namespace_target(Path::new("/proc/123/task/456/ns/user")),
539            Some(b"user:[4026531837]".as_slice())
540        );
541        assert_eq!(
542            canonical_namespace_target(Path::new("/proc/thread-self/ns/pid_for_children")),
543            Some(b"pid:[4026531836]".as_slice())
544        );
545    }
546
547    #[test]
548    fn leaves_non_namespace_and_relative_links_untouched() {
549        assert_eq!(
550            canonical_namespace_target(Path::new("/proc/self/exe")),
551            None
552        );
553        assert_eq!(canonical_namespace_target(Path::new("/tmp/ns/mnt")), None);
554        assert_eq!(
555            canonical_namespace_target(Path::new("proc/self/ns/mnt")),
556            None
557        );
558        assert_eq!(
559            canonical_namespace_target(Path::new("/proc/self/ns/unknown")),
560            None
561        );
562    }
563
564    #[test]
565    fn recognizes_proc_fd_aliases_and_lexical_normalization() {
566        for (path, expected) in [
567            ("/proc/self/fd/1", (None, 1)),
568            ("/proc/thread-self/fd/20", (None, 20)),
569            ("/proc/123/fd/7", (Some(123), 7)),
570            ("/proc/self/fd/../fd/9", (None, 9)),
571            ("/dev/fd/3", (None, 3)),
572        ] {
573            assert_eq!(proc_fd_target(Path::new(path)), Some(expected), "{path}");
574        }
575    }
576
577    #[test]
578    fn rewrites_only_numeric_virtual_self_proc_fd_aliases() {
579        assert_eq!(
580            host_self_proc_fd_alias(Path::new("/proc/123/fd/7"), 123),
581            Some(PathBuf::from("/proc/self/fd/7"))
582        );
583        assert_eq!(
584            host_self_proc_fd_alias(Path::new("/proc/124/fd/7"), 123),
585            None
586        );
587        assert_eq!(
588            host_self_proc_fd_alias(Path::new("/proc/self/fd/7"), 123),
589            None
590        );
591    }
592
593    #[test]
594    fn rejects_non_proc_fd_targets() {
595        for path in [
596            "/proc/self/fd/",
597            "/proc/self/fd/stdout",
598            "/proc/self/fd/1/status",
599            "/proc/not-a-pid/fd/1",
600            "/dev/fd/-1",
601            "proc/self/fd/1",
602        ] {
603            assert_eq!(proc_fd_target(Path::new(path)), None, "{path}");
604        }
605    }
606
607    #[test]
608    fn recognizes_only_anonymous_pipe_and_socket_targets() {
609        assert_eq!(
610            anonymous_proc_fd_identity(b"pipe:[987654321]"),
611            Some(AnonymousProcFdIdentity {
612                kind: "pipe",
613                raw_inode: 987_654_321,
614            })
615        );
616        assert_eq!(
617            anonymous_proc_fd_identity(b"socket:[42]"),
618            Some(AnonymousProcFdIdentity {
619                kind: "socket",
620                raw_inode: 42,
621            })
622        );
623
624        for target in [
625            b"pip".as_slice(),
626            b"socket:[12345".as_slice(),
627            b"/tmp/regular-file".as_slice(),
628            b"anon_inode:[eventpoll]".as_slice(),
629            b"pipe:[]".as_slice(),
630            b"pipe:[12]suffix".as_slice(),
631            b"socket:[not-a-number]".as_slice(),
632        ] {
633            assert_eq!(anonymous_proc_fd_identity(target), None, "{target:?}");
634        }
635    }
636
637    #[test]
638    fn short_buffers_use_scratch_large_enough_for_every_anonymous_target() {
639        assert!(!needs_anonymous_proc_fd_scratch(false, 1));
640        assert!(!needs_anonymous_proc_fd_scratch(true, 0));
641        assert!(needs_anonymous_proc_fd_scratch(true, 1));
642        assert!(needs_anonymous_proc_fd_scratch(true, 31));
643        assert!(!needs_anonymous_proc_fd_scratch(true, 32));
644
645        let maximum = format!("socket:[{}]", RawInode::MAX);
646        assert!(maximum.len() < ANONYMOUS_PROC_FD_TARGET_CAPACITY);
647        assert_eq!(
648            anonymous_proc_fd_identity(maximum.as_bytes()),
649            Some(AnonymousProcFdIdentity {
650                kind: "socket",
651                raw_inode: RawInode::MAX,
652            })
653        );
654    }
655
656    #[test]
657    fn stdio_identity_requires_a_raw_inode_match_and_preserves_alias_precedence() {
658        let stdio = [Some(11), Some(22), Some(33)];
659        assert_eq!(
660            deterministic_stdio_inode_for_raw(22, &stdio),
661            Some(DetInode::mint(1001))
662        );
663        assert_eq!(deterministic_stdio_inode_for_raw(44, &stdio), None);
664
665        let aliased = [None, Some(55), Some(55)];
666        assert_eq!(
667            deterministic_stdio_inode_for_raw(55, &aliased),
668            Some(DetInode::mint(1002))
669        );
670    }
671
672    #[test]
673    fn anonymous_target_rewrite_ignores_raw_inode_width_and_truncates_to_buffer() {
674        let short_raw = anonymous_proc_fd_identity(b"pipe:[42]").unwrap();
675        let long_raw = anonymous_proc_fd_identity(b"pipe:[987654321]").unwrap();
676        let short_rewrite =
677            canonical_anonymous_proc_fd_target(&short_raw, DetInode::mint(1001), usize::MAX);
678        let long_rewrite =
679            canonical_anonymous_proc_fd_target(&long_raw, DetInode::mint(1001), usize::MAX);
680        assert_eq!(short_rewrite, b"pipe:[1001]");
681        assert_eq!(long_rewrite, short_rewrite);
682
683        assert_eq!(
684            canonical_anonymous_proc_fd_target(&long_raw, DetInode::mint(1001), 8),
685            b"pipe:[10"
686        );
687    }
688}