1use std::ffi::OsString;
2use std::fs::File;
3use std::future::Future;
4use std::path::Path;
5use std::path::PathBuf;
6
7use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
8#[cfg(unix)]
9use codex_exec_server::CODEX_ARG0_EXEC_HELPER_ARG1;
10use codex_exec_server::CODEX_FS_HELPER_ARG1;
11use codex_install_context::InstallContext;
12use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
13use codex_utils_home_dir::find_codex_home;
14#[cfg(target_os = "windows")]
15use codex_windows_sandbox::CODEX_WINDOWS_SANDBOX_ARG1;
16#[cfg(unix)]
17use std::os::unix::fs::symlink;
18use tempfile::TempDir;
19
20const APPLY_PATCH_ARG0: &str = "apply_patch";
21const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch";
22#[cfg(unix)]
23const EXECVE_WRAPPER_ARG0: &str = "codex-execve-wrapper";
24const LOCK_FILENAME: &str = ".lock";
25const TOKIO_WORKER_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024;
26
27#[derive(Clone, Debug, Default, Eq, PartialEq)]
28pub struct Arg0DispatchPaths {
29 pub codex_self_exe: Option<PathBuf>,
35 pub codex_linux_sandbox_exe: Option<PathBuf>,
36 pub main_execve_wrapper_exe: Option<PathBuf>,
37}
38
39pub struct Arg0PathEntryGuard {
41 _temp_dir: TempDir,
42 _lock_file: File,
43 paths: Arg0DispatchPaths,
44}
45
46impl Arg0PathEntryGuard {
47 fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self {
48 Self {
49 _temp_dir: temp_dir,
50 _lock_file: lock_file,
51 paths,
52 }
53 }
54
55 pub fn paths(&self) -> &Arg0DispatchPaths {
56 &self.paths
57 }
58}
59
60pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
61 let mut args = std::env::args_os();
63 let argv0 = args.next().unwrap_or_default();
64 let exe_name = Path::new(&argv0)
65 .file_name()
66 .and_then(|s| s.to_str())
67 .unwrap_or("");
68
69 #[cfg(unix)]
70 if exe_name == EXECVE_WRAPPER_ARG0 {
71 let mut args = std::env::args();
72 let _ = args.next();
73 let file = match args.next() {
74 Some(file) => file,
75 None => std::process::exit(1),
76 };
77 let argv = args.collect::<Vec<_>>();
78
79 let runtime = match tokio::runtime::Builder::new_current_thread()
80 .enable_all()
81 .build()
82 {
83 Ok(runtime) => runtime,
84 Err(_) => std::process::exit(1),
85 };
86 let exit_code = runtime.block_on(
87 codex_shell_escalation::run_shell_escalation_execve_wrapper(file, argv),
88 );
89 match exit_code {
90 Ok(exit_code) => std::process::exit(exit_code),
91 Err(_) => std::process::exit(1),
92 }
93 }
94
95 if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
96 codex_linux_sandbox::run_main();
98 } else if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 {
99 codex_apply_patch::main();
100 }
101
102 let argv1 = args.next().unwrap_or_default();
103 #[cfg(unix)]
104 if argv1 == CODEX_ARG0_EXEC_HELPER_ARG1 {
105 codex_exec_server::run_arg0_exec_helper_main();
106 }
107 if argv1 == CODEX_FS_HELPER_ARG1 {
108 codex_exec_server::run_fs_helper_main();
109 }
110 #[cfg(target_os = "windows")]
111 if argv1 == CODEX_WINDOWS_SANDBOX_ARG1 {
112 codex_windows_sandbox::run_windows_sandbox_wrapper_main();
113 }
114 if argv1 == CODEX_CORE_APPLY_PATCH_ARG1 {
115 let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned));
116 let exit_code = match patch_arg {
117 Some(patch_arg) => {
118 let mut stdout = std::io::stdout();
119 let mut stderr = std::io::stderr();
120 let cwd = match codex_utils_absolute_path::AbsolutePathBuf::current_dir() {
121 Ok(cwd) => cwd,
122 Err(_) => std::process::exit(1),
123 };
124 let runtime = match tokio::runtime::Builder::new_current_thread()
125 .enable_all()
126 .build()
127 {
128 Ok(runtime) => runtime,
129 Err(_) => std::process::exit(1),
130 };
131 let cwd = cwd.into();
132 match runtime.block_on(codex_apply_patch::apply_patch(
133 &patch_arg,
134 &cwd,
135 &mut stdout,
136 &mut stderr,
137 codex_exec_server::LOCAL_FS.as_ref(),
138 None,
139 )) {
140 Ok(_) => 0,
141 Err(_) => 1,
142 }
143 }
144 None => {
145 eprintln!("Error: {CODEX_CORE_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
146 1
147 }
148 };
149 std::process::exit(exit_code);
150 }
151
152 load_dotenv();
155
156 let (path_entry_guard, updated_path_env_var) = prepare_path_env_var_with_aliases(
157 InstallContext::current(),
158 std::env::var_os("PATH"),
159 prepare_path_entry_for_codex_aliases,
160 );
161 if let Some(updated_path_env_var) = updated_path_env_var {
162 unsafe {
165 std::env::set_var("PATH", updated_path_env_var);
166 }
167 }
168 path_entry_guard
169}
170
171fn prepare_path_env_var_with_aliases(
172 install_context: &InstallContext,
173 existing_path: Option<OsString>,
174 prepare_aliases: impl FnOnce(Option<OsString>) -> std::io::Result<(Arg0PathEntryGuard, OsString)>,
175) -> (Option<Arg0PathEntryGuard>, Option<OsString>) {
176 let package_path = path_env_with_package_path_dir(install_context, existing_path.clone());
177 let path_for_aliases = package_path.clone().or(existing_path);
178
179 match prepare_aliases(path_for_aliases) {
180 Ok((path_entry, updated_path_env_var)) => (Some(path_entry), Some(updated_path_env_var)),
181 Err(err) => {
182 eprintln!("WARNING: proceeding, even though we could not create PATH aliases: {err}");
185 (None, package_path)
186 }
187 }
188}
189
190pub fn arg0_dispatch_or_else<F, Fut>(main_fn: F) -> anyhow::Result<()>
215where
216 F: FnOnce(Arg0DispatchPaths) -> Fut + Send + 'static,
217 Fut: Future<Output = anyhow::Result<()>>,
218{
219 let path_entry_guard = arg0_dispatch();
223 let current_exe = std::env::current_exe().ok();
224
225 let handle = std::thread::Builder::new()
229 .name("codex-main".to_string())
230 .stack_size(TOKIO_WORKER_STACK_SIZE_BYTES)
231 .spawn(move || {
232 let runtime = build_runtime()?;
233 runtime.block_on(run_main_with_arg0_guard(
234 path_entry_guard,
235 current_exe,
236 main_fn,
237 ))
238 })?;
239 match handle.join() {
240 Ok(result) => result,
241 Err(payload) => std::panic::resume_unwind(payload),
242 }
243}
244
245async fn run_main_with_arg0_guard<F, Fut>(
246 path_entry_guard: Option<Arg0PathEntryGuard>,
247 current_exe: Option<PathBuf>,
248 main_fn: F,
249) -> anyhow::Result<()>
250where
251 F: FnOnce(Arg0DispatchPaths) -> Fut,
252 Fut: Future<Output = anyhow::Result<()>>,
253{
254 let paths = Arg0DispatchPaths {
255 codex_self_exe: current_exe.clone(),
256 codex_linux_sandbox_exe: if cfg!(target_os = "linux") {
257 linux_sandbox_exe_path(path_entry_guard.as_ref(), current_exe)
258 } else {
259 None
260 },
261 main_execve_wrapper_exe: path_entry_guard
262 .as_ref()
263 .and_then(|path_entry| path_entry.paths().main_execve_wrapper_exe.clone()),
264 };
265
266 let result = main_fn(paths).await;
267 drop(path_entry_guard);
270 result
271}
272
273fn linux_sandbox_exe_path(
274 path_entry_guard: Option<&Arg0PathEntryGuard>,
275 current_exe: Option<PathBuf>,
276) -> Option<PathBuf> {
277 path_entry_guard
281 .and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone())
282 .or(current_exe)
283}
284
285fn build_runtime() -> anyhow::Result<tokio::runtime::Runtime> {
286 let mut builder = tokio::runtime::Builder::new_multi_thread();
287 builder.enable_all();
288 builder.thread_stack_size(TOKIO_WORKER_STACK_SIZE_BYTES);
289 Ok(builder.build()?)
290}
291
292const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";
293
294fn load_dotenv() {
299 if let Ok(codex_home) = find_codex_home()
300 && let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env"))
301 {
302 set_filtered(iter);
303 }
304}
305
306fn set_filtered<I>(iter: I)
308where
309 I: IntoIterator<Item = Result<(String, String), dotenvy::Error>>,
310{
311 for (key, value) in iter.into_iter().flatten() {
312 if !key.to_ascii_uppercase().starts_with(ILLEGAL_ENV_VAR_PREFIX) {
313 unsafe { std::env::set_var(&key, &value) };
316 }
317 }
318}
319
320fn prepare_path_entry_for_codex_aliases(
334 existing_path: Option<OsString>,
335) -> std::io::Result<(Arg0PathEntryGuard, OsString)> {
336 let codex_home = find_codex_home()?;
337 #[cfg(not(debug_assertions))]
338 {
339 let temp_root = std::env::temp_dir();
341 if codex_home.starts_with(&temp_root) {
342 return Err(std::io::Error::new(
343 std::io::ErrorKind::InvalidInput,
344 format!(
345 "Refusing to create helper binaries under temporary dir {temp_root:?} (codex_home: {codex_home:?})"
346 ),
347 ));
348 }
349 }
350
351 std::fs::create_dir_all(&codex_home)?;
352 let temp_root = codex_home.join("tmp").join("arg0");
354 std::fs::create_dir_all(&temp_root)?;
355 #[cfg(unix)]
356 {
357 use std::os::unix::fs::PermissionsExt;
358
359 std::fs::set_permissions(&temp_root, std::fs::Permissions::from_mode(0o700))?;
361 }
362
363 if let Err(err) = janitor_cleanup(&temp_root) {
365 eprintln!("WARNING: failed to clean up stale arg0 temp dirs: {err}");
366 }
367
368 let temp_dir = tempfile::Builder::new()
369 .prefix("codex-arg0")
370 .tempdir_in(&temp_root)?;
371 let path = temp_dir.path();
372
373 let lock_path = path.join(LOCK_FILENAME);
374 let lock_file = File::options()
375 .read(true)
376 .write(true)
377 .create(true)
378 .truncate(false)
379 .open(&lock_path)?;
380 lock_file.try_lock()?;
381
382 for filename in &[
383 APPLY_PATCH_ARG0,
384 MISSPELLED_APPLY_PATCH_ARG0,
385 #[cfg(target_os = "linux")]
386 CODEX_LINUX_SANDBOX_ARG0,
387 #[cfg(unix)]
388 EXECVE_WRAPPER_ARG0,
389 ] {
390 let exe = std::env::current_exe()?;
391
392 #[cfg(unix)]
393 {
394 let link = path.join(filename);
395 symlink(&exe, &link)?;
396 }
397
398 #[cfg(windows)]
399 {
400 let batch_script = path.join(format!("{filename}.bat"));
401 let exe = exe.display();
402 std::fs::write(
403 &batch_script,
404 format!(
405 r#"@echo off
406"{exe}" {CODEX_CORE_APPLY_PATCH_ARG1} %*
407"#,
408 ),
409 )?;
410 }
411 }
412
413 let updated_path_env_var = path_env_with_entry(path, existing_path);
414
415 let paths = Arg0DispatchPaths {
416 codex_self_exe: std::env::current_exe().ok(),
417 codex_linux_sandbox_exe: {
418 #[cfg(target_os = "linux")]
419 {
420 Some(path.join(CODEX_LINUX_SANDBOX_ARG0))
421 }
422 #[cfg(not(target_os = "linux"))]
423 {
424 None
425 }
426 },
427 main_execve_wrapper_exe: {
428 #[cfg(unix)]
429 {
430 Some(path.join(EXECVE_WRAPPER_ARG0))
431 }
432 #[cfg(not(unix))]
433 {
434 None
435 }
436 },
437 };
438
439 Ok((
440 Arg0PathEntryGuard::new(temp_dir, lock_file, paths),
441 updated_path_env_var,
442 ))
443}
444
445fn path_env_with_package_path_dir(
446 install_context: &InstallContext,
447 existing_path: Option<OsString>,
448) -> Option<OsString> {
449 let path_dir = install_context
450 .package_layout
451 .as_ref()
452 .and_then(|package_layout| package_layout.path_dir.as_ref())?;
453 Some(path_env_with_entry(path_dir.as_path(), existing_path))
454}
455
456fn path_env_with_entry(path_entry: &Path, existing_path: Option<OsString>) -> OsString {
457 #[cfg(unix)]
458 const PATH_SEPARATOR: &str = ":";
459
460 #[cfg(windows)]
461 const PATH_SEPARATOR: &str = ";";
462
463 let capacity = path_entry.as_os_str().len()
464 + existing_path
465 .as_ref()
466 .map_or(0, |existing_path| 1 + existing_path.len());
467 let mut path_env_var = OsString::with_capacity(capacity);
468 path_env_var.push(path_entry);
469 if let Some(existing_path) = existing_path {
470 path_env_var.push(PATH_SEPARATOR);
471 path_env_var.push(existing_path);
472 }
473 path_env_var
474}
475
476fn janitor_cleanup(temp_root: &Path) -> std::io::Result<()> {
477 let entries = match std::fs::read_dir(temp_root) {
478 Ok(entries) => entries,
479 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
480 Err(err) => return Err(err),
481 };
482
483 for entry in entries.flatten() {
484 let path = entry.path();
485 if !path.is_dir() {
486 continue;
487 }
488
489 let Some(_lock_file) = try_lock_dir(&path)? else {
491 continue;
492 };
493
494 match std::fs::remove_dir_all(&path) {
495 Ok(()) => {}
496 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
498 Err(err) => return Err(err),
499 }
500 }
501
502 Ok(())
503}
504
505fn try_lock_dir(dir: &Path) -> std::io::Result<Option<File>> {
506 let lock_path = dir.join(LOCK_FILENAME);
507 let lock_file = match File::options().read(true).write(true).open(&lock_path) {
508 Ok(file) => file,
509 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
510 Err(err) => return Err(err),
511 };
512
513 match lock_file.try_lock() {
514 Ok(()) => Ok(Some(lock_file)),
515 Err(std::fs::TryLockError::WouldBlock) => Ok(None),
516 Err(err) => Err(err.into()),
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::Arg0DispatchPaths;
523 use super::Arg0PathEntryGuard;
524 use super::LOCK_FILENAME;
525 use super::janitor_cleanup;
526 use super::linux_sandbox_exe_path;
527 #[cfg(unix)]
528 use super::run_main_with_arg0_guard;
529 #[cfg(unix)]
530 use anyhow::ensure;
531 use codex_install_context::CodexPackageLayout;
532 use codex_install_context::InstallContext;
533 use codex_install_context::InstallMethod;
534 use codex_utils_absolute_path::AbsolutePathBuf;
535 use pretty_assertions::assert_eq;
536 use std::fs;
537 use std::fs::File;
538 use std::path::Path;
539 use std::path::PathBuf;
540 use tempfile::TempDir;
541
542 struct PackagePathTestFixture {
543 _temp_dir: TempDir,
544 arg0_dir: PathBuf,
545 existing_dir: PathBuf,
546 install_context: InstallContext,
547 path_dir: AbsolutePathBuf,
548 }
549
550 fn create_lock(dir: &Path) -> std::io::Result<File> {
551 let lock_path = dir.join(LOCK_FILENAME);
552 File::options()
553 .read(true)
554 .write(true)
555 .create(true)
556 .truncate(false)
557 .open(lock_path)
558 }
559
560 fn package_path_test_fixture() -> anyhow::Result<PackagePathTestFixture> {
561 let temp_dir = TempDir::new()?;
562 let arg0_dir = temp_dir.path().join("arg0");
563 let package_dir = temp_dir.path().join("package");
564 let bin_dir = package_dir.join("bin");
565 let path_dir = package_dir.join("codex-path");
566 let existing_dir = temp_dir.path().join("existing-bin");
567 fs::create_dir_all(&arg0_dir)?;
568 fs::create_dir_all(&bin_dir)?;
569 fs::create_dir_all(&path_dir)?;
570 fs::create_dir_all(&existing_dir)?;
571 let path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?;
572 let install_context = InstallContext {
573 method: InstallMethod::Other,
574 package_layout: Some(CodexPackageLayout {
575 package_dir: AbsolutePathBuf::from_absolute_path(package_dir.canonicalize()?)?,
576 bin_dir: AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?,
577 resources_dir: None,
578 path_dir: Some(path_dir.clone()),
579 }),
580 };
581
582 Ok(PackagePathTestFixture {
583 _temp_dir: temp_dir,
584 arg0_dir,
585 existing_dir,
586 install_context,
587 path_dir,
588 })
589 }
590
591 #[test]
592 fn linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias() -> std::io::Result<()> {
593 let temp_dir = TempDir::new()?;
594 let lock_file = create_lock(temp_dir.path())?;
595 let alias_path = temp_dir.path().join("codex-linux-sandbox");
596 let path_entry = Arg0PathEntryGuard::new(
597 temp_dir,
598 lock_file,
599 Arg0DispatchPaths {
600 codex_self_exe: Some(PathBuf::from("/usr/bin/codex")),
601 codex_linux_sandbox_exe: Some(alias_path.clone()),
602 main_execve_wrapper_exe: None,
603 },
604 );
605
606 assert_eq!(
607 linux_sandbox_exe_path(Some(&path_entry), Some(PathBuf::from("/usr/bin/codex"))),
608 Some(alias_path),
609 );
610 Ok(())
611 }
612
613 #[test]
614 fn path_env_can_prepend_package_path_before_arg0_alias_dir() -> anyhow::Result<()> {
615 let fixture = package_path_test_fixture()?;
616
617 let package_path = super::path_env_with_package_path_dir(
618 &fixture.install_context,
619 Some(fixture.existing_dir.as_os_str().to_owned()),
620 )
621 .expect("package path dir should update PATH");
622 let updated_path = super::path_env_with_entry(&fixture.arg0_dir, Some(package_path));
623
624 assert_eq!(
625 std::env::split_paths(&updated_path).collect::<Vec<_>>(),
626 vec![
627 fixture.arg0_dir,
628 fixture.path_dir.as_path().to_path_buf(),
629 fixture.existing_dir
630 ],
631 );
632 Ok(())
633 }
634
635 #[test]
636 fn package_path_survives_arg0_alias_setup_failure() -> anyhow::Result<()> {
637 let fixture = package_path_test_fixture()?;
638
639 let (path_entry_guard, updated_path_env_var) = super::prepare_path_env_var_with_aliases(
640 &fixture.install_context,
641 Some(fixture.existing_dir.as_os_str().to_owned()),
642 |path_for_aliases| {
643 assert_eq!(
644 std::env::split_paths(
645 &path_for_aliases.expect("package PATH should be passed to alias setup")
646 )
647 .collect::<Vec<_>>(),
648 vec![
649 fixture.path_dir.as_path().to_path_buf(),
650 fixture.existing_dir.clone()
651 ],
652 );
653 Err(std::io::Error::other("alias setup failed"))
654 },
655 );
656
657 assert!(path_entry_guard.is_none());
658 let updated_path_env_var =
659 updated_path_env_var.expect("package PATH should survive alias setup failure");
660 assert_eq!(
661 std::env::split_paths(&updated_path_env_var).collect::<Vec<_>>(),
662 vec![
663 fixture.path_dir.as_path().to_path_buf(),
664 fixture.existing_dir
665 ],
666 );
667 Ok(())
668 }
669
670 #[cfg(unix)]
671 #[test]
672 fn run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns() -> anyhow::Result<()> {
673 let temp_dir = TempDir::new()?;
674 let alias_path = temp_dir.path().join("codex-helper-alias");
675 fs::write(&alias_path, b"")?;
676 let lock_file = create_lock(temp_dir.path())?;
677 let path_entry = Arg0PathEntryGuard::new(
678 temp_dir,
679 lock_file,
680 Arg0DispatchPaths {
681 codex_self_exe: Some(PathBuf::from("/usr/bin/codex")),
682 codex_linux_sandbox_exe: Some(alias_path.clone()),
683 main_execve_wrapper_exe: Some(alias_path),
684 },
685 );
686
687 super::build_runtime()?.block_on(run_main_with_arg0_guard(
688 Some(path_entry),
689 Some(PathBuf::from("/usr/bin/codex")),
690 |paths| async move {
691 let alias_path = paths
692 .codex_linux_sandbox_exe
693 .or(paths.main_execve_wrapper_exe)
694 .expect("unix dispatch should create at least one alias path");
695 ensure!(
696 alias_path.exists(),
697 "alias path disappeared before main future was polled: {}",
698 alias_path.display()
699 );
700
701 tokio::task::yield_now().await;
702
703 ensure!(
704 alias_path.exists(),
705 "alias path disappeared while main future was running: {}",
706 alias_path.display()
707 );
708 Ok(())
709 },
710 ))
711 }
712
713 #[test]
714 fn janitor_skips_dirs_without_lock_file() -> std::io::Result<()> {
715 let root = tempfile::tempdir()?;
716 let dir = root.path().join("no-lock");
717 fs::create_dir(&dir)?;
718
719 janitor_cleanup(root.path())?;
720
721 assert!(dir.exists());
722 Ok(())
723 }
724
725 #[test]
726 fn janitor_skips_dirs_with_held_lock() -> std::io::Result<()> {
727 let root = tempfile::tempdir()?;
728 let dir = root.path().join("locked");
729 fs::create_dir(&dir)?;
730 let lock_file = create_lock(&dir)?;
731 lock_file.try_lock()?;
732
733 janitor_cleanup(root.path())?;
734
735 assert!(dir.exists());
736 Ok(())
737 }
738
739 #[test]
740 fn janitor_removes_dirs_with_unlocked_lock() -> std::io::Result<()> {
741 let root = tempfile::tempdir()?;
742 let dir = root.path().join("stale");
743 fs::create_dir(&dir)?;
744 create_lock(&dir)?;
745
746 janitor_cleanup(root.path())?;
747
748 assert!(!dir.exists());
749 Ok(())
750 }
751}