vtcode_bash_runner/
process_owner.rs1use std::ffi::{OsStr, OsString};
7use std::io::{self, Read};
8use std::os::fd::AsRawFd;
9use std::os::unix::net::UnixStream;
10use std::os::unix::process::CommandExt;
11use std::path::{Path, PathBuf};
12use std::time::Duration;
13
14pub const OWNER_SOCKET_ENV: &str = "VTCODE_COMPANION_OWNER_SOCKET";
16pub const HELPER_MODE: &str = "--vtcode-owned-exec";
18pub const OWNER_DIRECTORY_PREFIX: &str = "vtc-";
20pub const OWNER_SOCKET_FILE: &str = "owner";
22const READY: u8 = 1;
23
24pub struct OwnedCommand {
27 pub executable: PathBuf,
28 pub arguments: Vec<OsString>,
29 pub owner_socket: PathBuf,
30}
31
32impl OwnedCommand {
33 pub fn new(
34 executable: &Path,
35 owner_socket: &Path,
36 program: &OsStr,
37 args: impl IntoIterator<Item = impl AsRef<OsStr>>,
38 arg0: Option<&OsStr>,
39 ) -> Self {
40 let mut arguments = vec![
41 OsString::from(HELPER_MODE),
42 arg0.unwrap_or(program).to_owned(),
43 program.to_owned(),
44 ];
45 arguments.extend(args.into_iter().map(|arg| arg.as_ref().to_owned()));
46 Self {
47 executable: executable.to_owned(),
48 arguments,
49 owner_socket: owner_socket.to_owned(),
50 }
51 }
52
53 pub fn from_environment(
63 program: &OsStr,
64 args: impl IntoIterator<Item = impl AsRef<OsStr>>,
65 arg0: Option<&OsStr>,
66 ) -> io::Result<Option<Self>> {
67 let Some(socket) = std::env::var_os(OWNER_SOCKET_ENV) else {
68 return Ok(None);
69 };
70 let socket = PathBuf::from(socket);
71 if !is_owned_endpoint(&socket) {
72 return Ok(None);
73 }
74 Ok(Some(Self::new(&std::env::current_exe()?, &socket, program, args, arg0)))
75 }
76}
77
78pub fn is_owned_endpoint(socket: &Path) -> bool {
89 is_owned_endpoint_under(socket, &std::env::temp_dir())
90}
91
92fn is_owned_endpoint_under(socket: &Path, temp_root: &Path) -> bool {
95 use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
96
97 if !socket.is_absolute() || socket.file_name() != Some(OsStr::new(OWNER_SOCKET_FILE)) {
98 return false;
99 }
100 let Ok(endpoint) = std::fs::symlink_metadata(socket) else {
102 return false;
103 };
104 if !endpoint.file_type().is_socket() {
105 return false;
106 }
107 let Some(directory) = socket.parent() else {
108 return false;
109 };
110 let Ok(directory_metadata) = std::fs::metadata(directory) else {
111 return false;
112 };
113 let owner_only = directory_metadata.permissions().mode() & 0o077 == 0;
114 let owned_by_self = directory_metadata.is_dir() && directory_metadata.uid() == nix::unistd::geteuid().as_raw();
115 if !owner_only || !owned_by_self {
116 return false;
117 }
118 if !directory
119 .file_name()
120 .and_then(OsStr::to_str)
121 .is_some_and(|name| name.starts_with(OWNER_DIRECTORY_PREFIX))
122 {
123 return false;
124 }
125 let Ok(directory) = vtcode_commons::canonicalize(directory) else {
126 return false;
127 };
128 let temp_root = vtcode_commons::canonicalize(temp_root).unwrap_or_else(|_| temp_root.to_path_buf());
129 directory.starts_with(temp_root)
130}
131
132pub fn maybe_run_owned_command() -> io::Result<bool> {
136 let args: Vec<_> = std::env::args_os().skip(1).collect();
137 if args.first().is_none_or(|arg| arg != HELPER_MODE) {
138 return Ok(false);
139 }
140 let arg0 = args
141 .get(1)
142 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing target argv[0]"))?;
143 let program = args
144 .get(2)
145 .filter(|p| !p.is_empty())
146 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing target program"))?;
147 if nix::unistd::getpgrp() != nix::unistd::getpid() {
148 return Err(io::Error::new(io::ErrorKind::InvalidInput, "owned command requires an isolated process group"));
149 }
150 let socket = std::env::var_os(OWNER_SOCKET_ENV)
151 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing task owner endpoint"))?;
152 let mut owner = UnixStream::connect(socket)?;
153 owner.set_read_timeout(Some(Duration::from_secs(2)))?;
154 let mut acknowledgement = [0];
155 owner.read_exact(&mut acknowledgement)?;
156 if acknowledgement != [READY] {
157 return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid task owner acknowledgement"));
158 }
159 owner.set_read_timeout(None)?;
162 spawn_watchdog(&owner)?;
163 drop(owner);
164 close_all_above_stdio();
170 let error = std::process::Command::new(program).arg0(arg0).args(args.iter().skip(3)).exec();
171 Err(error)
172}
173
174#[expect(
183 unsafe_code,
184 reason = "getrlimit/close take validated arguments; no Rust-owned fds above stdio are live here"
185)]
186fn close_all_above_stdio() {
187 let mut limit = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
188 let ceiling = if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) } == 0 {
190 limit.rlim_cur.clamp(256, 65536) as libc::c_int
191 } else {
192 8192
193 };
194 for fd in 3..ceiling {
195 unsafe {
198 libc::close(fd);
199 }
200 }
201}
202
203#[expect(
204 unsafe_code,
205 reason = "Fork children execute only async-signal-safe syscalls and exit without Rust destructors"
206)]
207fn spawn_watchdog(owner: &UnixStream) -> io::Result<()> {
208 let (mut ready_reader, ready_writer) = UnixStream::pair()?;
209 ready_reader.set_read_timeout(Some(Duration::from_secs(2)))?;
210 let owner_fd = owner.as_raw_fd();
211 let ready_fd = ready_writer.as_raw_fd();
212 if owner_fd <= 2 || ready_fd <= 2 || ready_reader.as_raw_fd() <= 2 {
213 return Err(io::Error::new(io::ErrorKind::InvalidInput, "owned command requires open standard streams"));
214 }
215 let group = nix::unistd::getpgrp().as_raw();
216 let intermediate = unsafe { libc::fork() };
219 if intermediate == -1 {
220 return Err(io::Error::last_os_error());
221 }
222 if intermediate == 0 {
223 let monitor = unsafe { libc::fork() };
226 if monitor == -1 {
227 unsafe { libc::_exit(1) };
229 }
230 if monitor != 0 {
231 unsafe { libc::_exit(0) };
233 }
234 unsafe { libc::close(ready_reader.as_raw_fd()) };
236 unsafe { run_watchdog(owner_fd, ready_fd, group) };
239 }
240 drop(ready_writer);
241 let mut status = 0;
242 loop {
243 let waited = unsafe { libc::waitpid(intermediate, &mut status, 0) };
247 if waited == intermediate {
248 break;
249 }
250 let error = io::Error::last_os_error();
251 if error.kind() != io::ErrorKind::Interrupted {
252 return Err(error);
253 }
254 }
255 if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 {
256 return Err(io::Error::other("failed to start command lifetime watchdog"));
257 }
258 let mut ready = [0];
259 ready_reader.read_exact(&mut ready)?;
260 if ready != [READY] {
261 return Err(io::Error::other("command lifetime watchdog did not initialize"));
262 }
263 Ok(())
264}
265
266#[expect(
270 unsafe_code,
271 reason = "Minimal fork-child watchdog uses only async-signal-safe POSIX calls"
272)]
273unsafe fn run_watchdog(owner_fd: libc::c_int, ready_fd: libc::c_int, group: libc::pid_t) -> ! {
274 let mut ignore: libc::sigaction = unsafe { std::mem::zeroed() };
277 ignore.sa_sigaction = libc::SIG_IGN;
278 unsafe { libc::sigemptyset(&mut ignore.sa_mask) };
280 for signal in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGPIPE] {
281 if unsafe { libc::sigaction(signal, &ignore, std::ptr::null_mut()) } == -1 {
284 unsafe { libc::_exit(1) };
286 }
287 }
288 for fd in 0..=2 {
289 unsafe { libc::close(fd) };
292 }
293 let ready = READY;
294 if unsafe { libc::write(ready_fd, std::ptr::addr_of!(ready).cast(), 1) } != 1 {
297 unsafe { libc::_exit(1) };
299 }
300 unsafe { libc::close(ready_fd) };
302 let mut byte = 0u8;
303 loop {
304 let read = unsafe { libc::read(owner_fd, std::ptr::addr_of_mut!(byte).cast(), 1) };
306 if read == -1 && nix::errno::Errno::last() == nix::errno::Errno::EINTR {
307 continue;
308 }
309 unsafe { libc::kill(-group, libc::SIGKILL) };
313 unsafe { libc::_exit(1) };
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use assert_fs::TempDir;
322 use std::os::unix::fs::{PermissionsExt, symlink};
323 use std::os::unix::net::UnixListener;
324
325 fn private_directory(root: &Path, name: &str) -> PathBuf {
326 let path = root.join(name);
327 std::fs::create_dir(&path).expect("create private directory");
328 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).expect("owner-only directory");
329 path
330 }
331
332 fn bind_endpoint(directory: &Path, name: &str) -> PathBuf {
333 let socket = directory.join(name);
334 drop(UnixListener::bind(&socket).expect("bind endpoint"));
336 socket
337 }
338
339 #[test]
340 fn accepts_a_supervisor_shaped_endpoint() {
341 let root = TempDir::new().expect("temp root");
342 let directory = private_directory(root.path(), "vtc-accepted");
343 let socket = bind_endpoint(&directory, OWNER_SOCKET_FILE);
344
345 assert!(is_owned_endpoint(&socket));
346 assert!(is_owned_endpoint_under(&socket, &std::env::temp_dir()));
347 }
348
349 #[test]
350 fn ignores_a_stale_or_invalid_endpoint() {
351 let root = TempDir::new().expect("temp root");
352 let missing = root.path().join("vtc-stale").join(OWNER_SOCKET_FILE);
353 assert!(!is_owned_endpoint(&missing));
355
356 let directory = private_directory(root.path(), "vtc-stale");
357 std::fs::write(&missing, b"not a socket").expect("regular file");
358 assert!(!is_owned_endpoint(&missing));
359
360 let _foreign = bind_endpoint(&directory, "endpoint");
361 assert!(!is_owned_endpoint(&directory.join("endpoint")));
362 assert!(!is_owned_endpoint(Path::new(OWNER_SOCKET_FILE)));
363 }
364
365 #[test]
366 fn ignores_an_endpoint_outside_a_private_directory() {
367 let root = TempDir::new().expect("temp root");
368 let directory = root.path().join("vtc-public");
369 std::fs::create_dir(&directory).expect("public directory");
370 std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o755)).expect("world-accessible");
371 let socket = bind_endpoint(&directory, OWNER_SOCKET_FILE);
372
373 assert!(!is_owned_endpoint(&socket));
374 }
375
376 #[test]
377 fn ignores_a_directory_the_supervisor_did_not_name() {
378 let root = TempDir::new().expect("temp root");
379 let directory = private_directory(root.path(), "other-owner");
380 let socket = bind_endpoint(&directory, OWNER_SOCKET_FILE);
381
382 assert!(!is_owned_endpoint(&socket));
383 }
384
385 #[test]
386 fn ignores_a_symlinked_endpoint() {
387 let root = TempDir::new().expect("temp root");
388 let target = bind_endpoint(root.path(), "target.sock");
389 let directory = private_directory(root.path(), "vtc-symlinked");
390 let socket = directory.join(OWNER_SOCKET_FILE);
391 symlink(&target, &socket).expect("symlink endpoint");
392
393 assert!(!is_owned_endpoint(&socket));
394 }
395
396 #[test]
397 fn ignores_an_endpoint_outside_the_temp_root() {
398 let root = TempDir::new().expect("temp root");
399 let other_root = TempDir::new().expect("other root");
400 let directory = private_directory(root.path(), "vtc-containment");
401 let socket = bind_endpoint(&directory, OWNER_SOCKET_FILE);
402
403 assert!(is_owned_endpoint(&socket));
404 assert!(!is_owned_endpoint_under(&socket, other_root.path()));
405 }
406}