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 let error = std::process::Command::new(program).arg0(arg0).args(args.iter().skip(3)).exec();
165 Err(error)
166}
167
168#[expect(
169 unsafe_code,
170 reason = "Fork children execute only async-signal-safe syscalls and exit without Rust destructors"
171)]
172fn spawn_watchdog(owner: &UnixStream) -> io::Result<()> {
173 let (mut ready_reader, ready_writer) = UnixStream::pair()?;
174 ready_reader.set_read_timeout(Some(Duration::from_secs(2)))?;
175 let owner_fd = owner.as_raw_fd();
176 let ready_fd = ready_writer.as_raw_fd();
177 if owner_fd <= 2 || ready_fd <= 2 || ready_reader.as_raw_fd() <= 2 {
178 return Err(io::Error::new(io::ErrorKind::InvalidInput, "owned command requires open standard streams"));
179 }
180 let group = nix::unistd::getpgrp().as_raw();
181 let intermediate = unsafe { libc::fork() };
184 if intermediate == -1 {
185 return Err(io::Error::last_os_error());
186 }
187 if intermediate == 0 {
188 let monitor = unsafe { libc::fork() };
191 if monitor == -1 {
192 unsafe { libc::_exit(1) };
194 }
195 if monitor != 0 {
196 unsafe { libc::_exit(0) };
198 }
199 unsafe { libc::close(ready_reader.as_raw_fd()) };
201 unsafe { run_watchdog(owner_fd, ready_fd, group) };
204 }
205 drop(ready_writer);
206 let mut status = 0;
207 loop {
208 let waited = unsafe { libc::waitpid(intermediate, &mut status, 0) };
212 if waited == intermediate {
213 break;
214 }
215 let error = io::Error::last_os_error();
216 if error.kind() != io::ErrorKind::Interrupted {
217 return Err(error);
218 }
219 }
220 if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 {
221 return Err(io::Error::other("failed to start command lifetime watchdog"));
222 }
223 let mut ready = [0];
224 ready_reader.read_exact(&mut ready)?;
225 if ready != [READY] {
226 return Err(io::Error::other("command lifetime watchdog did not initialize"));
227 }
228 Ok(())
229}
230
231#[expect(
235 unsafe_code,
236 reason = "Minimal fork-child watchdog uses only async-signal-safe POSIX calls"
237)]
238unsafe fn run_watchdog(owner_fd: libc::c_int, ready_fd: libc::c_int, group: libc::pid_t) -> ! {
239 let mut ignore: libc::sigaction = unsafe { std::mem::zeroed() };
242 ignore.sa_sigaction = libc::SIG_IGN;
243 unsafe { libc::sigemptyset(&mut ignore.sa_mask) };
245 for signal in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGPIPE] {
246 if unsafe { libc::sigaction(signal, &ignore, std::ptr::null_mut()) } == -1 {
249 unsafe { libc::_exit(1) };
251 }
252 }
253 for fd in 0..=2 {
254 unsafe { libc::close(fd) };
257 }
258 let ready = READY;
259 if unsafe { libc::write(ready_fd, std::ptr::addr_of!(ready).cast(), 1) } != 1 {
262 unsafe { libc::_exit(1) };
264 }
265 unsafe { libc::close(ready_fd) };
267 let mut byte = 0u8;
268 loop {
269 let read = unsafe { libc::read(owner_fd, std::ptr::addr_of_mut!(byte).cast(), 1) };
271 if read == -1 && nix::errno::Errno::last() == nix::errno::Errno::EINTR {
272 continue;
273 }
274 unsafe { libc::kill(-group, libc::SIGKILL) };
278 unsafe { libc::_exit(1) };
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use assert_fs::TempDir;
287 use std::os::unix::fs::{PermissionsExt, symlink};
288 use std::os::unix::net::UnixListener;
289
290 fn private_directory(root: &Path, name: &str) -> PathBuf {
291 let path = root.join(name);
292 std::fs::create_dir(&path).expect("create private directory");
293 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).expect("owner-only directory");
294 path
295 }
296
297 fn bind_endpoint(directory: &Path, name: &str) -> PathBuf {
298 let socket = directory.join(name);
299 drop(UnixListener::bind(&socket).expect("bind endpoint"));
301 socket
302 }
303
304 #[test]
305 fn accepts_a_supervisor_shaped_endpoint() {
306 let root = TempDir::new().expect("temp root");
307 let directory = private_directory(root.path(), "vtc-accepted");
308 let socket = bind_endpoint(&directory, OWNER_SOCKET_FILE);
309
310 assert!(is_owned_endpoint(&socket));
311 assert!(is_owned_endpoint_under(&socket, &std::env::temp_dir()));
312 }
313
314 #[test]
315 fn ignores_a_stale_or_invalid_endpoint() {
316 let root = TempDir::new().expect("temp root");
317 let missing = root.path().join("vtc-stale").join(OWNER_SOCKET_FILE);
318 assert!(!is_owned_endpoint(&missing));
320
321 let directory = private_directory(root.path(), "vtc-stale");
322 std::fs::write(&missing, b"not a socket").expect("regular file");
323 assert!(!is_owned_endpoint(&missing));
324
325 let _foreign = bind_endpoint(&directory, "endpoint");
326 assert!(!is_owned_endpoint(&directory.join("endpoint")));
327 assert!(!is_owned_endpoint(Path::new(OWNER_SOCKET_FILE)));
328 }
329
330 #[test]
331 fn ignores_an_endpoint_outside_a_private_directory() {
332 let root = TempDir::new().expect("temp root");
333 let directory = root.path().join("vtc-public");
334 std::fs::create_dir(&directory).expect("public directory");
335 std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o755)).expect("world-accessible");
336 let socket = bind_endpoint(&directory, OWNER_SOCKET_FILE);
337
338 assert!(!is_owned_endpoint(&socket));
339 }
340
341 #[test]
342 fn ignores_a_directory_the_supervisor_did_not_name() {
343 let root = TempDir::new().expect("temp root");
344 let directory = private_directory(root.path(), "other-owner");
345 let socket = bind_endpoint(&directory, OWNER_SOCKET_FILE);
346
347 assert!(!is_owned_endpoint(&socket));
348 }
349
350 #[test]
351 fn ignores_a_symlinked_endpoint() {
352 let root = TempDir::new().expect("temp root");
353 let target = bind_endpoint(root.path(), "target.sock");
354 let directory = private_directory(root.path(), "vtc-symlinked");
355 let socket = directory.join(OWNER_SOCKET_FILE);
356 symlink(&target, &socket).expect("symlink endpoint");
357
358 assert!(!is_owned_endpoint(&socket));
359 }
360
361 #[test]
362 fn ignores_an_endpoint_outside_the_temp_root() {
363 let root = TempDir::new().expect("temp root");
364 let other_root = TempDir::new().expect("other root");
365 let directory = private_directory(root.path(), "vtc-containment");
366 let socket = bind_endpoint(&directory, OWNER_SOCKET_FILE);
367
368 assert!(is_owned_endpoint(&socket));
369 assert!(!is_owned_endpoint_under(&socket, other_root.path()));
370 }
371}