Skip to main content

proot/syscall/
traced.rs

1//! Handlers for syscalls that take a ptrace stop: the path (or socket
2//! address) is rewritten in the tracee's memory and the syscall proceeds.
3
4use crate::exec::{self, shim};
5use crate::fs::Fs;
6use crate::fs::translate::translate_path;
7use crate::note;
8use crate::state::Ctx;
9use crate::sys::Result;
10use crate::syscall::nr::*;
11use crate::tracee::regs::Regs;
12
13/// Dispatches a seccomp-trace stop to the right handler.
14///
15/// On error the syscall number is replaced so the kernel returns `ENOSYS`;
16/// precise errnos need an exit stop we do not take.  A process running the
17/// exec trampoline (a nested launch) has its syscalls passed through, and
18/// its exec of the real target completes the pending exec.
19///
20/// # Arguments
21/// * `ctx` - Shared supervisor state.
22/// * `regs` - Registers at the stop; edits are committed by the caller.
23pub fn handle(ctx: &Ctx, regs: &mut Regs) {
24    let pid = regs.pid();
25    if let Some(pending) = ctx.procs.shim_pending(ctx.tgid(pid)) {
26        let tgid = ctx.tgid(pid);
27        if shim::running(pid as u32) {
28            let is_exec = matches!(
29                regs.abi().to_canonical(regs.sysnum()),
30                Some((EXECVE, _)) | Some((EXECVEAT, _))
31            );
32            if is_exec {
33                ctx.procs.shim_leave(tgid);
34                exec::finish_pending(ctx, tgid, pending);
35            }
36            return;
37        }
38        ctx.procs.shim_leave(tgid);
39    }
40    let view = ctx.view(pid);
41    let fs = &view.fs;
42    if regs.abi().socketcall_nr() == Some(regs.sysnum()) {
43        if let Err(e) = rewrite_socketcall(ctx, fs, regs) {
44            note!(
45                ctx,
46                1,
47                "pid {} socketcall: {}",
48                regs.pid(),
49                crate::sys::strerror(e)
50            );
51            regs.fail(e);
52        }
53        return;
54    }
55    // Handlers think in x86_64 terms; other ABIs report their own numbers.
56    let Some((nr, _)) = regs.abi().to_canonical(regs.sysnum()) else {
57        return;
58    };
59    let r = match nr {
60        EXECVE | EXECVEAT => crate::exec::handle_execve(ctx, regs),
61        CHDIR | CHROOT => rewrite_path_arg(ctx, fs, regs, 0, true),
62        CONNECT | BIND => rewrite_sockaddr(ctx, fs, regs, nr == CONNECT),
63        // Only O_PATH opens arrive here (the filter inspects the flags): their
64        // descriptors cannot be injected, so the filename is rewritten in place.
65        OPEN => {
66            let deref = regs.abi().open_flags_to_host(regs.arg(1) as i64) & libc::O_NOFOLLOW as i64 == 0;
67            rewrite_path_arg_at(ctx, fs, regs, None, 0, deref)
68        }
69        OPENAT => {
70            let deref = regs.abi().open_flags_to_host(regs.arg(2) as i64) & libc::O_NOFOLLOW as i64 == 0;
71            rewrite_path_arg_at(ctx, fs, regs, Some(0), 1, deref)
72        }
73        INOTIFY_ADD_WATCH => rewrite_path_arg(ctx, fs, regs, 1, true),
74        _ => Ok(()),
75    };
76    if let Err(e) = r {
77        note!(
78            ctx,
79            1,
80            "pid {} {}: {}",
81            regs.pid(),
82            name(nr),
83            crate::sys::strerror(e)
84        );
85        regs.fail(e);
86    }
87}
88
89/// Writes `bytes` plus a NUL onto the tracee's stack and returns the address.
90///
91/// # Arguments
92/// * `ctx` - Shared supervisor state.
93/// * `regs` - Registers to allocate the stack slot from.
94/// * `bytes` - String contents, without NUL.
95pub fn push_cstr(ctx: &Ctx, regs: &mut Regs, bytes: &[u8]) -> Result<u64> {
96    let mut buf = bytes.to_vec();
97    buf.push(0);
98    let addr = regs.alloc_stack(buf.len() as u64);
99    ctx.mem.write(regs.pid() as u32, addr, &buf)?;
100    Ok(addr)
101}
102
103/// Translates a `dirfd`-relative path argument and points it at the host path.
104///
105/// After the rewrite the path is absolute, so the `dirfd` argument (if any) is
106/// set to `AT_FDCWD`.
107///
108/// # Arguments
109/// * `ctx` - Shared supervisor state.
110/// * `fs` - The tracee's filesystem view.
111/// * `regs` - Registers at the stop.
112/// * `dirfd_index` - Argument index of the directory descriptor, if the syscall has one.
113/// * `path_index` - Argument index holding the path pointer.
114/// * `deref_final` - Follow a trailing symlink.
115pub fn rewrite_path_arg_at(
116    ctx: &Ctx,
117    fs: &Fs,
118    regs: &mut Regs,
119    dirfd_index: Option<usize>,
120    path_index: usize,
121    deref_final: bool,
122) -> Result<()> {
123    let pid = regs.pid() as u32;
124    let dirfd = dirfd_index.map(|i| regs.arg(i) as i32).unwrap_or(libc::AT_FDCWD);
125    let path = ctx.mem.read_cstr(pid, regs.arg(path_index))?;
126    if path.is_empty() {
127        return Ok(()); // AT_EMPTY_PATH: the descriptor itself, nothing to translate
128    }
129    let host = translate_path(fs, pid, dirfd, &path, deref_final)?;
130    if host != path {
131        let addr = push_cstr(ctx, regs, &host)?;
132        regs.set_arg(path_index, addr);
133        if let Some(i) = dirfd_index {
134            regs.set_arg(i, libc::AT_FDCWD as u64);
135        }
136    }
137    Ok(())
138}
139
140/// Translates the path in argument `index` and points it at the host path.
141///
142/// An empty path is left as it is, for the kernel to refuse with `ENOENT`.
143///
144/// # Arguments
145/// * `ctx` - Shared supervisor state.
146/// * `fs` - The tracee's filesystem view.
147/// * `regs` - Registers at the stop.
148/// * `index` - Argument index holding the path pointer.
149/// * `deref_final` - Follow a trailing symlink.
150pub fn rewrite_path_arg(ctx: &Ctx, fs: &Fs, regs: &mut Regs, index: usize, deref_final: bool) -> Result<()> {
151    let pid = regs.pid() as u32;
152    let path = ctx.mem.read_cstr(pid, regs.arg(index))?;
153    if path.is_empty() {
154        return Ok(());
155    }
156    let host = translate_path(fs, pid, libc::AT_FDCWD, &path, deref_final)?;
157    if host != path {
158        let addr = push_cstr(ctx, regs, &host)?;
159        regs.set_arg(index, addr);
160    }
161    Ok(())
162}
163
164/// Translates the `sun_path` of an `AF_UNIX` address in `connect`/`bind`.
165///
166/// Abstract sockets and other families are left alone.  A translated path
167/// that does not fit in `sun_path` makes the syscall fail with `ENOSYS`.
168///
169/// # Arguments
170/// * `ctx` - Shared supervisor state.
171/// * `fs` - The tracee's filesystem view.
172/// * `regs` - Registers at the stop (fd, addr, len).
173/// * `deref_final` - Follow a trailing symlink (`connect`), or not (`bind`).
174fn rewrite_sockaddr(ctx: &Ctx, fs: &Fs, regs: &mut Regs, deref_final: bool) -> Result<()> {
175    if let Some((addr, len)) =
176        translate_sockaddr(ctx, fs, regs, regs.arg(1), regs.arg(2) as usize, deref_final)?
177    {
178        regs.set_arg(1, addr);
179        regs.set_arg(2, len);
180    }
181    Ok(())
182}
183
184/// Translates an `AF_UNIX` address in tracee memory, returning a rewritten copy.
185///
186/// # Arguments
187/// * `ctx` - Shared supervisor state.
188/// * `fs` - The tracee's filesystem view.
189/// * `regs` - Registers at the stop, for scratch space.
190/// * `addr` - Address of the `sockaddr` in the tracee.
191/// * `len` - Its length.
192/// * `deref_final` - Follow a trailing symlink in `sun_path`.
193///
194/// # Returns
195/// `Some((new_addr, new_len))` when the path needed translating, else `None`.
196fn translate_sockaddr(
197    ctx: &Ctx,
198    fs: &Fs,
199    regs: &mut Regs,
200    addr: u64,
201    len: usize,
202    deref_final: bool,
203) -> Result<Option<(u64, u64)>> {
204    let pid = regs.pid() as u32;
205    if addr == 0 || len <= 2 || len > 110 {
206        return Ok(None);
207    }
208    let mut sa = vec![0u8; len];
209    ctx.mem.read(pid, addr, &mut sa)?;
210    if u16::from_ne_bytes([sa[0], sa[1]]) != libc::AF_UNIX as u16 || sa[2] == 0 {
211        return Ok(None);
212    }
213    let end = sa[2..].iter().position(|&b| b == 0).map(|p| p + 2).unwrap_or(len);
214    let path = &sa[2..end];
215    let host = translate_path(fs, pid, libc::AT_FDCWD, path, deref_final)?;
216    if host == path {
217        return Ok(None);
218    }
219    if host.len() >= 108 {
220        return Err(libc::ENAMETOOLONG);
221    }
222    let mut new = vec![0u8; 2 + host.len() + 1];
223    new[0..2].copy_from_slice(&(libc::AF_UNIX as u16).to_ne_bytes());
224    new[2..2 + host.len()].copy_from_slice(&host);
225    let slot = regs.alloc_stack(new.len() as u64);
226    ctx.mem.write(pid, slot, &new)?;
227    Ok(Some((slot, new.len() as u64)))
228}
229
230/// Translates `bind`/`connect` issued through the i386 `socketcall` multiplexer.
231///
232/// `socketcall(call, args)` takes its arguments as an array of 32-bit words in
233/// tracee memory.  The filter only routes calls 2 and 3 here.  A rewritten
234/// address gets a fresh argument array in scratch space.
235///
236/// # Arguments
237/// * `ctx` - Shared supervisor state.
238/// * `fs` - The tracee's filesystem view.
239/// * `regs` - Registers at the stop (call, args pointer).
240fn rewrite_socketcall(ctx: &Ctx, fs: &Fs, regs: &mut Regs) -> Result<()> {
241    let pid = regs.pid() as u32;
242    let call = regs.arg(0);
243    let args_ptr = regs.arg(1);
244    let mut raw = [0u8; 12];
245    ctx.mem.read(pid, args_ptr, &mut raw)?;
246    let word = |i: usize| u32::from_ne_bytes(raw[i * 4..i * 4 + 4].try_into().unwrap()) as u64;
247    let (fd, addr, len) = (word(0), word(1), word(2) as usize);
248    let deref_final = call == 3;
249    if let Some((new_addr, new_len)) = translate_sockaddr(ctx, fs, regs, addr, len, deref_final)? {
250        let mut new_args = [0u8; 12];
251        new_args[0..4].copy_from_slice(&(fd as u32).to_ne_bytes());
252        new_args[4..8].copy_from_slice(&(new_addr as u32).to_ne_bytes());
253        new_args[8..12].copy_from_slice(&(new_len as u32).to_ne_bytes());
254        let slot = regs.alloc_stack(12);
255        ctx.mem.write(pid, slot, &new_args)?;
256        regs.set_arg(1, slot);
257    }
258    Ok(())
259}