1use std::{
12 collections::HashSet,
13 ffi::{
14 OsStr,
15 OsString,
16 },
17 io::{
18 ErrorKind,
19 Read,
20 Write,
21 },
22 os::{
23 linux::net::SocketAddrExt,
24 unix::{
25 ffi::{
26 OsStrExt,
27 OsStringExt,
28 },
29 net::{
30 SocketAddr,
31 UnixListener,
32 UnixStream,
33 },
34 process::ExitStatusExt,
35 },
36 },
37 path::Path,
38 process::{
39 Child,
40 Command,
41 ExitStatus,
42 },
43 sync::LazyLock,
44 time::{
45 Duration,
46 Instant,
47 },
48};
49
50use color_eyre::eyre::bail;
51use nix::{
52 sys::socket::{
53 getsockopt,
54 sockopt,
55 },
56 unistd::Uid,
57};
58use rand::distr::{
59 Alphanumeric,
60 SampleString,
61};
62
63pub type EnvVars = Vec<(OsString, OsString)>;
64
65const ENV_REQUEST_MAGIC: &[u8] = b"tracexec-env-v1";
66const ENV_SOCKET_ACCEPT_TIMEOUT: Duration = Duration::from_secs(180);
67
68pub static RESTORED_ENV_ALLOWLIST: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
79 HashSet::from([
80 "NO_COLOR",
81 "RUST_LOG",
82 "TRACEXEC_LOG_LEVEL",
83 "TRACEXEC_NO_SLEEP",
84 "TRACEXEC_USE_FENTRY",
85 "TRACEXEC_USE_KPROBE",
86 ])
87});
88
89#[derive(Debug, Clone)]
91pub struct PreElevationCreds {
92 pub username: String,
93 pub uid: u32,
94 pub gid: u32,
95}
96
97impl PreElevationCreds {
98 pub fn capture() -> color_eyre::Result<Self> {
100 let uid = nix::unistd::getuid();
101 let gid = nix::unistd::getgid();
102 let user = crate::account::user_from_uid(uid)?
103 .ok_or_else(|| color_eyre::eyre::eyre!("Failed to look up current user (uid={uid})"))?;
104 Ok(Self {
105 username: user.name,
106 uid: uid.as_raw(),
107 gid: gid.as_raw(),
108 })
109 }
110}
111
112pub fn env_value<'a>(env: &'a [(OsString, OsString)], key: &str) -> Option<&'a OsStr> {
113 let key = OsStr::new(key);
114 env
115 .iter()
116 .rev()
117 .find_map(|(candidate, value)| (candidate == key).then_some(value.as_os_str()))
118}
119
120pub fn env_var_os(env: Option<&[(OsString, OsString)]>, key: &str) -> Option<OsString> {
121 match env {
122 Some(env) => env_value(env, key).map(OsStr::to_owned),
123 None => std::env::var_os(key),
124 }
125}
126
127pub fn env_var_string(env: Option<&[(OsString, OsString)]>, key: &str) -> Option<String> {
128 match env {
129 Some(env) => env_value(env, key).map(|value| value.to_string_lossy().into_owned()),
130 None => std::env::var(key).ok(),
131 }
132}
133
134pub fn filter_allowlisted_env_from(
135 vars: impl IntoIterator<Item = (OsString, OsString)>,
136) -> EnvVars {
137 vars
138 .into_iter()
139 .filter(|(key, _)| {
140 key
141 .to_str()
142 .is_some_and(|key| RESTORED_ENV_ALLOWLIST.contains(key))
143 })
144 .collect()
145}
146
147pub fn filter_allowlisted_env(env: &[(OsString, OsString)]) -> EnvVars {
148 filter_allowlisted_env_from(env.iter().cloned())
149}
150
151fn collect_original_env() -> EnvVars {
152 std::env::vars_os().collect()
153}
154
155fn serialize_env(env: &[(OsString, OsString)]) -> Vec<u8> {
159 let mut buf = Vec::new();
160 for (key, value) in env {
161 buf.extend_from_slice(key.as_bytes());
162 buf.push(b'=');
163 buf.extend_from_slice(value.as_bytes());
164 buf.push(0);
165 }
166 buf
167}
168
169fn deserialize_env(data: &[u8]) -> EnvVars {
171 let mut result = Vec::new();
172 for entry in data.split(|&b| b == 0) {
173 if entry.is_empty() {
174 continue;
175 }
176 if let Some(eq_pos) = entry.iter().position(|&b| b == b'=') {
177 let key = OsString::from_vec(entry[..eq_pos].to_vec());
178 let value = OsString::from_vec(entry[eq_pos + 1..].to_vec());
179 result.push((key, value));
180 }
181 }
182 result
183}
184
185fn abstract_socket_addr(socket_name: &str) -> color_eyre::Result<SocketAddr> {
186 Ok(SocketAddr::from_abstract_name(socket_name.as_bytes())?)
187}
188
189fn random_socket_name() -> String {
190 let suffix = Alphanumeric.sample_string(&mut rand::rng(), 32);
191 format!("tracexec-env-{}-{suffix}", std::process::id())
192}
193
194fn bind_env_socket(socket_name: &str) -> color_eyre::Result<UnixListener> {
195 Ok(UnixListener::bind_addr(&abstract_socket_addr(
196 socket_name,
197 )?)?)
198}
199
200fn exit_code_from_status(status: ExitStatus) -> i32 {
201 status
202 .code()
203 .or_else(|| status.signal().map(|signal| 128 + signal))
204 .unwrap_or(1)
205}
206
207fn handle_env_request(
208 mut stream: UnixStream,
209 env: &[(OsString, OsString)],
210 required_uid: u32,
211) -> color_eyre::Result<()> {
212 let creds = getsockopt(&stream, sockopt::PeerCredentials)?;
213 if creds.uid() != required_uid {
214 bail!(
215 "Refusing to send environment variables to uid {} (expected uid {required_uid})",
216 creds.uid()
217 );
218 }
219
220 let mut request = vec![0; ENV_REQUEST_MAGIC.len()];
221 stream.read_exact(&mut request)?;
222 if request != ENV_REQUEST_MAGIC {
223 bail!("Invalid request");
224 }
225
226 let payload = serialize_env(env);
227 stream.write_all(&(payload.len() as u32).to_be_bytes())?;
228 stream.write_all(&payload)?;
229 stream.flush()?;
230 Ok(())
231}
232
233fn serve_env_to_child(
234 listener: &UnixListener,
235 child: &mut Child,
236 env: &[(OsString, OsString)],
237 timeout: Duration,
238) -> color_eyre::Result<Option<ExitStatus>> {
239 listener.set_nonblocking(true)?;
240 let deadline = Instant::now() + timeout;
241
242 loop {
243 if let Some(status) = child.try_wait()? {
244 return Ok(Some(status));
245 }
246
247 match listener.accept() {
248 Ok((stream, _)) => {
249 handle_env_request(stream, env, 0)?;
250 return Ok(None);
251 }
252 Err(e) if e.kind() == ErrorKind::WouldBlock => {
253 if Instant::now() >= deadline {
254 bail!("Timed out waiting for the elevated subprocess to request environment variables");
255 }
256 std::thread::sleep(Duration::from_millis(50));
257 }
258 Err(e) => return Err(e.into()),
259 }
260 }
261}
262
263pub fn request_env_from_parent(socket_name: &str) -> color_eyre::Result<EnvVars> {
265 let mut stream = UnixStream::connect_addr(&abstract_socket_addr(socket_name)?)?;
266 stream.write_all(ENV_REQUEST_MAGIC)?;
267 stream.flush()?;
268
269 let mut len = [0; 4];
270 stream.read_exact(&mut len)?;
271 let len = u32::from_be_bytes(len) as usize;
272 let mut payload = vec![0; len];
273 stream.read_exact(&mut payload)?;
274 Ok(deserialize_env(&payload))
275}
276
277fn build_elevated_args(
289 creds: &PreElevationCreds,
290 socket_name: &str,
291 config_dir: Option<&Path>,
292 data_dir: Option<&Path>,
293 data_local_dir: Option<&Path>,
294) -> Vec<OsString> {
295 build_elevated_args_from(
296 std::env::args_os(),
297 creds,
298 socket_name,
299 config_dir,
300 data_dir,
301 data_local_dir,
302 )
303}
304
305fn build_elevated_args_from(
309 args: impl IntoIterator<Item = impl Into<OsString>>,
310 creds: &PreElevationCreds,
311 socket_name: &str,
312 config_dir: Option<&Path>,
313 data_dir: Option<&Path>,
314 data_local_dir: Option<&Path>,
315) -> Vec<OsString> {
316 let mut result = Vec::new();
317 let mut replaced = false;
318 let mut past_delimiter = false;
319
320 for arg in args.into_iter().skip(1) {
321 let arg: OsString = arg.into();
322 if past_delimiter {
323 result.push(arg);
325 continue;
326 }
327 if arg == "--" {
328 past_delimiter = true;
329 result.push(arg);
330 continue;
331 }
332 if !replaced && arg == "--elevate" {
333 replaced = true;
335 result.push(OsString::from("--user"));
336 result.push(OsString::from(&creds.username));
337 result.push(OsString::from("--restore-env-socket"));
338 result.push(OsString::from(socket_name));
339 if let Some(dir) = config_dir {
340 result.push(OsString::from("--elevated-config-dir"));
341 result.push(dir.as_os_str().to_owned());
342 }
343 if let Some(dir) = data_dir {
344 result.push(OsString::from("--elevated-data-dir"));
345 result.push(dir.as_os_str().to_owned());
346 }
347 if let Some(dir) = data_local_dir {
348 result.push(OsString::from("--elevated-data-local-dir"));
349 result.push(dir.as_os_str().to_owned());
350 }
351 } else {
352 result.push(arg);
353 }
354 }
355
356 result
357}
358
359pub fn elevate_and_reexec() -> color_eyre::Result<std::convert::Infallible> {
364 if Uid::effective().is_root() {
365 color_eyre::eyre::bail!("--elevate is not needed when already running as root");
366 }
367
368 let creds = PreElevationCreds::capture()?;
369 let socket_name = random_socket_name();
370 let listener = bind_env_socket(&socket_name)?;
371 let env = collect_original_env();
372 let exe = std::env::current_exe()?;
373
374 let proj_dirs = crate::cli::config::project_directory();
377 let config_dir = proj_dirs.as_ref().map(|d| d.config_dir().to_path_buf());
378 let data_dir = proj_dirs.as_ref().map(|d| d.data_dir().to_path_buf());
379 let data_local_dir = proj_dirs.as_ref().map(|d| d.data_local_dir().to_path_buf());
380 let elevated_args = build_elevated_args(
381 &creds,
382 &socket_name,
383 config_dir.as_deref(),
384 data_dir.as_deref(),
385 data_local_dir.as_deref(),
386 );
387
388 tracing::debug!(
389 "Elevating: sudo {} {}",
390 exe.display(),
391 elevated_args
392 .iter()
393 .map(|a| a.to_string_lossy().to_string())
394 .collect::<Vec<_>>()
395 .join(" ")
396 );
397
398 let mut child = Command::new("sudo")
399 .arg(&exe)
400 .args(&elevated_args)
401 .spawn()?;
402 match serve_env_to_child(&listener, &mut child, &env, ENV_SOCKET_ACCEPT_TIMEOUT) {
403 Ok(Some(status)) => std::process::exit(exit_code_from_status(status)),
404 Ok(None) => {
405 drop(listener);
406 let status = child.wait()?;
407 std::process::exit(exit_code_from_status(status));
408 }
409 Err(e) => {
410 let _ = child.kill();
411 let _ = child.wait();
412 Err(e)
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use std::{
420 os::unix::ffi::OsStringExt,
421 thread,
422 };
423
424 use super::*;
425
426 fn test_creds() -> PreElevationCreds {
427 PreElevationCreds {
428 username: "testuser".to_string(),
429 uid: 1000,
430 gid: 1000,
431 }
432 }
433
434 #[test]
435 fn test_capture_creds() {
436 let creds = PreElevationCreds::capture().unwrap();
437 assert_eq!(creds.uid, nix::unistd::getuid().as_raw());
438 assert_eq!(creds.gid, nix::unistd::getgid().as_raw());
439 assert!(!creds.username.is_empty());
440 }
441
442 #[test]
443 fn test_allowlist_filters_unneeded_env_vars() {
444 let filtered = filter_allowlisted_env_from([
445 (OsString::from("TRACEXEC_NO_SLEEP"), OsString::from("1")),
446 (
447 OsString::from("TRACEXEC_TEST_MARKER"),
448 OsString::from("secret"),
449 ),
450 (OsString::from("PATH"), OsString::from("/usr/bin")),
451 ]);
452
453 assert_eq!(
454 filtered,
455 vec![(OsString::from("TRACEXEC_NO_SLEEP"), OsString::from("1"),),]
456 );
457 }
458
459 #[test]
460 fn test_serialize_deserialize_roundtrip() {
461 let original = vec![
462 (OsString::from("TRACEXEC_NO_SLEEP"), OsString::from("1")),
463 (OsString::from("PATH"), OsString::from("/usr/bin:/bin")),
464 (OsString::from("EMPTY"), OsString::from("")),
465 (OsString::from("MULTI_EQ"), OsString::from("a=b=c")),
466 ];
467
468 let data = serialize_env(&original);
469 let deserialized = deserialize_env(&data);
470 assert_eq!(deserialized, original);
471 }
472
473 #[test]
474 fn test_deserialize_env_ignores_malformed() {
475 let data = b"GOOD=value\0BAD_NO_EQUAL\0ALSO_GOOD=\0";
477 let result = deserialize_env(data);
478 assert_eq!(
479 result,
480 vec![
481 (OsString::from("GOOD"), OsString::from("value")),
482 (OsString::from("ALSO_GOOD"), OsString::from("")),
483 ]
484 );
485 }
486
487 #[test]
488 fn test_deserialize_env_handles_non_utf8() {
489 let mut data = Vec::new();
491 data.extend_from_slice(b"KEY=");
492 data.extend_from_slice(&[0xff, 0xfe]); data.push(0);
494
495 let result = deserialize_env(&data);
496 assert_eq!(result.len(), 1);
497 assert_eq!(result[0].0, OsString::from("KEY"));
498 assert_eq!(result[0].1, OsString::from_vec(vec![0xff, 0xfe]));
499 }
500
501 #[test]
502 fn test_env_socket_roundtrip() {
503 let socket_name = random_socket_name();
504 let listener = bind_env_socket(&socket_name).unwrap();
505 let env = vec![
506 (OsString::from("TRACEXEC_NO_SLEEP"), OsString::from("1")),
507 (OsString::from("PATH"), OsString::from("/usr/bin")),
508 ];
509 let expected = env.clone();
510 let uid = nix::unistd::getuid().as_raw();
511
512 let server = thread::spawn(move || {
513 let (stream, _) = listener.accept().unwrap();
514 handle_env_request(stream, &env, uid).unwrap();
515 });
516
517 let received = request_env_from_parent(&socket_name).unwrap();
518 server.join().unwrap();
519 assert_eq!(received, expected);
520 }
521
522 #[test]
523 fn test_build_elevated_args_replaces_elevate() {
524 let creds = test_creds();
525 let input_args = vec![
526 OsString::from("tracexec"),
527 OsString::from("--elevate"),
528 OsString::from("tui"),
529 OsString::from("-t"),
530 OsString::from("--"),
531 OsString::from("sudo"),
532 OsString::from("ls"),
533 ];
534
535 let result = build_elevated_args_from(input_args, &creds, "sock-name", None, None, None);
536
537 assert_eq!(
538 result,
539 vec![
540 OsString::from("--user"),
541 OsString::from("testuser"),
542 OsString::from("--restore-env-socket"),
543 OsString::from("sock-name"),
544 OsString::from("tui"),
545 OsString::from("-t"),
546 OsString::from("--"),
547 OsString::from("sudo"),
548 OsString::from("ls"),
549 ]
550 );
551 }
552
553 #[test]
554 fn test_build_elevated_args_preserves_other_flags() {
555 let creds = PreElevationCreds {
556 username: "bob".to_string(),
557 uid: 1002,
558 gid: 1002,
559 };
560 let input_args = vec![
561 OsString::from("tracexec"),
562 OsString::from("--color=always"),
563 OsString::from("--elevate"),
564 OsString::from("-C"),
565 OsString::from("/tmp"),
566 OsString::from("log"),
567 OsString::from("--"),
568 OsString::from("ls"),
569 ];
570
571 let result = build_elevated_args_from(input_args, &creds, "sock-999", None, None, None);
572
573 assert_eq!(
574 result,
575 vec![
576 OsString::from("--color=always"),
577 OsString::from("--user"),
578 OsString::from("bob"),
579 OsString::from("--restore-env-socket"),
580 OsString::from("sock-999"),
581 OsString::from("-C"),
582 OsString::from("/tmp"),
583 OsString::from("log"),
584 OsString::from("--"),
585 OsString::from("ls"),
586 ]
587 );
588 }
589
590 #[test]
591 fn test_build_elevated_args_passes_project_dirs() {
592 let creds = test_creds();
593 let config_dir = Path::new("/home/testuser/.config/tracexec");
594 let data_dir = Path::new("/home/testuser/.local/share/tracexec");
595 let data_local_dir = Path::new("/home/testuser/.local/share/tracexec");
596
597 let input_args = vec![
598 OsString::from("tracexec"),
599 OsString::from("--elevate"),
600 OsString::from("log"),
601 OsString::from("--"),
602 OsString::from("ls"),
603 ];
604
605 let result = build_elevated_args_from(
606 input_args,
607 &creds,
608 "sock-abc",
609 Some(config_dir),
610 Some(data_dir),
611 Some(data_local_dir),
612 );
613
614 assert_eq!(
615 result,
616 vec![
617 OsString::from("--user"),
618 OsString::from("testuser"),
619 OsString::from("--restore-env-socket"),
620 OsString::from("sock-abc"),
621 OsString::from("--elevated-config-dir"),
622 OsString::from("/home/testuser/.config/tracexec"),
623 OsString::from("--elevated-data-dir"),
624 OsString::from("/home/testuser/.local/share/tracexec"),
625 OsString::from("--elevated-data-local-dir"),
626 OsString::from("/home/testuser/.local/share/tracexec"),
627 OsString::from("log"),
628 OsString::from("--"),
629 OsString::from("ls"),
630 ]
631 );
632 }
633
634 #[test]
635 fn test_build_elevated_args_ignores_elevate_after_delimiter() {
636 let creds = test_creds();
637 let input_args = vec![
638 OsString::from("tracexec"),
639 OsString::from("--elevate"),
640 OsString::from("log"),
641 OsString::from("--"),
642 OsString::from("cmd"),
643 OsString::from("--elevate"),
644 ];
645
646 let result = build_elevated_args_from(input_args, &creds, "sock-abc", None, None, None);
647
648 assert_eq!(
649 result,
650 vec![
651 OsString::from("--user"),
652 OsString::from("testuser"),
653 OsString::from("--restore-env-socket"),
654 OsString::from("sock-abc"),
655 OsString::from("log"),
656 OsString::from("--"),
657 OsString::from("cmd"),
658 OsString::from("--elevate"),
659 ]
660 );
661 }
662}