Skip to content

Prevent mutating the global environment pointer in CommandExt::exec and opt to use execve and resolve path manually - #157144

Open
asder8215 wants to merge 11 commits into
rust-lang:mainfrom
asder8215:commandext_exec_bug_fix
Open

asder8215 wants to merge 11 commits into
rust-lang:mainfrom
asder8215:commandext_exec_bug_fix

Conversation

@asder8215

@asder8215 asder8215 commented May 30, 2026 •

Copy link
Copy Markdown
Contributor

View all comments

This PR fixes the bug described in #156951 where CommandExt::exec mutating the global environment pointer could cause correctness issue with concurrent exec calls (also this function holds an environment read lock, so it shouldn't cause any writes to the global environment pointer anyways). We do everything within CommandExt::exec via execve than execvp, which means we have to resolve the path manually.

I took reference to what was done in an archived (Feb 2026) upstream mirror of glibc's execvp and execvpe. I also took reference to #55359 on how they implemented the concerned portion of CommandExt::exec with execve. There's also this OpenBSD libc implementation of execvpe that I saw, but I didn't deeply take a look at this one and saw how it differ.

Other than that, I'm unsure how to handle the error ETIMEOUT/TimedOut as glibc breaks parsing through the PATH environment variable value and returns the error while #55359 continues parsing.

Let me know if you also want me to put some of execve code in a helper function or refactor it in other ways.

From @joboet comment in #157144 (comment), this table contain platform behavior on the default fallback path with execvp and whether it invokes shell or not on ENOEXEC.

Target process_spawnp execvp fallback PATH fallback is PATH_DEFPATH posix_spawnp calls shell on ENOEXEC execvp calls shell on ENOEXEC
linux-glibc link link CS_PATH = /bin:/usr/bin (same as confstr(_CS_PATH), but different from _PATH_DEFPATH = /usr/bin:/bin) no, guaranteed to include confstr(_CS_PATH) before glibc 2.15 (below our minimum) yes
macos link link _PATH_DEFPATH = /usr/bin:/bin (notably, this is different from confstr(_CS_PATH) = sysctl(..) = /usr/bin:/bin:/usr/sbin:/sbin) guaranteed by documentation yes yes
linux-musl link link /usr/local/bin:/bin:/usr/bin = _PATH_DEFPATH (not equal to confstr(_CS_PATH) = /bin:/usr/bin yes forwards to __execvpe no
freebsd link link _PATH_DEFPATH = /sbin:/bin:/usr/sbin:/usr/bin: (not equal to confstr(_CS_PATH) = _PATH_STDPATH = /usr/bin:/bin:/usr/sbin:/sbin) guaranteed by documentation forwards to execvpe yes
openbsd link link _PATH_DEFPATH = /usr/bin:/bin:/usr/sbin:/sbin:/usr/X11R6/bin:/usr/local/bin:/usr/local/sbin (currently equal to confstr(_CS_PATH) = _PATH_STDPATH) guaranteed by documentation forwards to execvpe yes
netbsd link link _PATH_DEFPATH = /usr/bin:/bin:/usr/pkg/bin:/usr/local/bin (not equal to confstr(_CS_PATH = sysctl(..) = _PATH_STDPATH = /usr/bin:/bin:/usr/sbin:/sbin:/usr/pkg/bin:/usr/pkg/sbin:/usr/local/bin:/usr/local/sbin) guaranteed by documentation no yes

See #157144 (comment) for the remaining Unix platforms and their behavior with _PATH_DEFPATH/_PATH_BSHELL

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels May 30, 2026
@rustbot

rustbot commented May 30, 2026

Copy link
Copy Markdown
Collaborator

r? @joboet

rustbot has assigned @joboet.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @ChrisDenton, libs
  • @ChrisDenton, libs expanded to 8 candidates
  • Random selection from Mark-Simulacrum, joboet

@rust-log-analyzer

This comment has been minimized.

@joboet joboet left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I highly recommend having a look at the POSIX specification – GNU/Linux mostly follows that, but the specification sometimes leaves some things unspecified and we should be careful not to break those.

Also, this is currently unsound, we cannot perform any memory allocation in do_exec, since it's called after fork for the standard spawn path. You probably need to preallocate some memory when creating the Command (or when setting certain parameters).

View changes since this review

None => c"/bin:/usr/bin".to_bytes_with_nul(),
}
}
None => parent_path.as_bytes_with_nul(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend moving the environment variable lookup here so that it's only performed when actually needed.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels May 30, 2026
@rustbot

rustbot commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@rustbot rustbot added the O-unix Operating system: Unix-like label May 31, 2026
@asder8215
asder8215 requested a review from joboet May 31, 2026 05:02
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels May 31, 2026
Comment thread library/std/src/sys/pal/unix/conf.rs Resolved
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 6, 2026
@asder8215
asder8215 requested a review from joboet June 8, 2026 08:36
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jun 8, 2026
@rust-log-analyzer

This comment has been minimized.

@asder8215
asder8215 force-pushed the commandext_exec_bug_fix branch from 213bb02 to 8319b09 Compare June 8, 2026 09:23
@rust-log-analyzer

This comment has been minimized.

Comment thread library/std/src/sys/pal/unix/conf.rs Resolved
Comment thread library/std/src/sys/pal/unix/conf.rs Resolved
assert_failed(index, len);
}

self.ptrs.insert(index, item.into_raw());

@joboet joboet Jun 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaks the string if insert panics (see #155748, I made this mistake before).

View changes since the review

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 8, 2026
@asder8215
asder8215 requested a review from joboet June 9, 2026 22:31
@joboet joboet added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Oct 20, 2026
@asder8215

asder8215 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Yep, I saw. I was actually working on it before the notif I received for the with_native_path refactoring.

I have to check a couple of other platforms that wasn't listed earlier to see how execvp works.

  • DragonFlyBSD: uses _PATH_DEFPATH and invokes shell via _PATH_BSHELL
  • Android: uses _PATH_DEFPATH and invokes shell via _PATH_BSHELL.
  • uClibc, which seems to use ":/bin:/usr/bin" different from its _PATH_DEFPATH value of "/usr/bin:/bin". I know the comment above that value says it tried to mirror glibc, which leans on confstr(_CS_PATH), so I'm wondering if that's the same on uClibc?
  • emscripten, which uses musl, so same as you mentioned earlier.
  • Haiku: It looks like it doesn't use _PATH_DEFPATH (sets errno to B_BAD_VALUE when path is null), but from lines 89-90, it seems to use _PATH_BSHELL (values are the same) on ENOEXEC error
  • Cygwin: Does not have a default fallback path or handles ENOEXEC
  • Hurd: I couldn't understand Hurd's exec implementation that well, so I might need help parsing through what they're doing here.
  • [Redox]: This one has a _PATH_BSHELL constant, but I mean this is all I found from their exec.c file (pretty barebones, though the value it does pass for the first argument of execv is _PATH_BSHELL). There's also this exec.rs, but I'm unsure if that's the right area we're supposed to look for exec implementation.
  • nuttx/NTO/QNX: This one doesn't have _PATH_DEFPATH or _PATH_BSHELL constant (couldn't find a include/paths.h file); I don't think we have to do anything special for these platforms?
  • Solaris/AIX: Also, doesn't have _PATH_DEFPATH/_PATH_BSHELL and I believe it's closed source? Can't tell if they have a default fallback path or handle ENOEXEC.

Do we know what we want to do for certain platforms like Solaris/AIX? I think those platforms are closed-source, so I couldn't find info about _PATH_DEFPATH/_PATH_BSHELL. I know Solaris is listed as tier 2 with host tools, so being able to build the standard library is supposed to be guaranteed.

Also, if possible, could you put that table of default path/ENOEXEC behavior in the PR description? 🙏 (Nvm, I realized there was a copy markdown option, so copying it to the PR description).

@asder8215
asder8215 force-pushed the commandext_exec_bug_fix branch from 71e457f to 0289d85 Compare October 21, 2026 17:36
@rustbot

rustbot commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@rust-log-analyzer

This comment has been minimized.

@asder8215
asder8215 force-pushed the commandext_exec_bug_fix branch from 0289d85 to 4c2289e Compare October 21, 2026 18:03
@rust-log-analyzer

This comment has been minimized.

@asder8215
asder8215 force-pushed the commandext_exec_bug_fix branch from 4c2289e to 78a1404 Compare October 21, 2026 18:12
@rust-log-analyzer

This comment has been minimized.

…H on Apple, Android, OpenBSD, FreeBSD, and DragonflyBSD as the default fallback path when the PATH environment variable was not found from maybe_envp/environ(); furthermore, added support for invoking the shell when execve returns an ENOEXEC error on Android, all Linux platforms (except musl), OpenBSD, NetBSD (for exec, but not posix_spawn), Apple, DragonFlyBSD, and Haiku.
@asder8215
asder8215 force-pushed the commandext_exec_bug_fix branch from 78a1404 to 4359483 Compare October 21, 2026 18:39
@asder8215

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Oct 21, 2026
@asder8215
asder8215 force-pushed the commandext_exec_bug_fix branch from 2c3f041 to 07cf726 Compare October 27, 2026 17:39
@@ -83,6 +83,40 @@ cfg_select! {
}
}

/// Retrieves the fallback path *specifically* used by each platform's `execvp`
/// implementation.
fn get_default_path() -> CString {

@asder8215 asder8215 Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Apologies for the repeat ping, previous message is label outdated due to new commits)

@joboet see my comment earlier #157144 (comment)

Unresolved question here is should we just directly use ":/bin:/usr/bin" for uclibc's execvp default fallback path? This is different from its _PATH_DEFPATH value which is "/usr/bin:/bin"

View changes since the review

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know what we want to do for certain platforms like Solaris/AIX? I think those platforms are closed-source, so I couldn't find info about _PATH_DEFPATH/_PATH_BSHELL. I know Solaris is listed as tier 2 with host tools, so being able to build the standard library is supposed to be guaranteed.

@psumbera for Solaris, @xingxue-ibm for AIX: Do these platforms have constants like _PATH_DEFPATH that are used by execvp when no PATH is available? Or is there a better way to emulate execvp behavior?

Probably doing a libc release in 1-2 weeks, as a heads up if there's something to add there.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AIX's paths.h defines _PATH_BSHELL but not _PATH_DEFPATH. I opened PR rust-lang/libc#5676 to add _PATH_BSHELL to libc for AIX.

@asder8215 asder8215 Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xingxue-ibm does AIX invoke the shell (e.g. if not i.e. _PATH_BSHELL) on ENOEXEC in their execvp implementation? (something similar to what glibc does here) Follow up question I have is does posix_spawn (or equivalent name) forward to execvpe? If it does, I would like to put up a FIXME comment in those areas.

(To note for reviewers, if _PATH_BSHELL or some equivalent path is used in ENOEXEC behavior or if _PATH_DEFPATH is used, we can put up a different/future PR to add these behaviors in since the code is written in a way that you can easily insert the correct behavior in Command::get_shell_argv or get_default_path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the current Solaris ON libc sources.

Solaris defines _PATH_BSHELL in <paths.h> as "/usr/bin/sh", but it does not define _PATH_DEFPATH.

When PATH is unset, Solaris execvp does not use a fixed fallback constant. Its get_PATH() helper calls confstr(_CS_PATH) and then:

  • appends ":/usr/sbin" when either the real or effective UID is 0;
  • otherwise appends ":", causing the current directory to be searched last.

Therefore, the closest emulation is to obtain _CS_PATH before the fork and implement the UID-dependent suffix, rather than hard-coding a _PATH_DEFPATH. The suffix should be selected after any requested credential change has been applied.

Solaris does handle ENOEXEC by invoking a shell. The executable is _PATH_BSHELL (/usr/bin/sh) normally, or /usr/xpg4/bin/sh for an XPG4-mode caller; its argv[0] is "sh". posix_spawnp() uses the same path and shell helpers.

For rust-lang/libc, it would be useful to add both Solaris _PATH_BSHELL and _CS_PATH (65). One caveat: the existing Rust Solaris confstr binding links to __confstr_xpg7, whereas Solaris execvp uses the mode-sensitive base confstr. Thus simply adding Solaris to the existing libc::confstr branch would select the XPG7 path and would not exactly reproduce current execvp behavior.

Oracle's documentation also confirms that the ENOEXEC shell depends on the caller's conformance mode: execvp(2) and standards(7).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xingxue-ibm does AIX invoke the shell (e.g. if not i.e. _PATH_BSHELL) on ENOEXEC in their execvp implementation? (something similar to what glibc does here) Follow up question I have is does posix_spawn (or equivalent name) forward to execvpe? If it does, I would like to put up a FIXME comment in those areas.

I don't have access to the AIX system source code, but I checked on AIX 7.2 by running tests and disassembling some routines. Here are my findings:
• ENOEXEC: execvp and execvpe run execv("/usr/bin/sh", {"sh", "--", <script>, <args...>}). The shell path is the literal /usr/bin/sh, not _PATH_BSHELL, which AIX defines as /usr/bin/bsh in /usr/include/paths.h.
• No PATH: The fallback is :/bin:/usr/bin, with the current directory searched first (the same string you found for uClibc). AIX's paths.h has no _PATH_DEFPATH.
• posix_spawnp: It forwards to execvpe. It forks, and the child calls execvpe (or execvp if envp is NULL).

A FIXME in both places works for me, with the AIX handling in a follow-up PR.

@asder8215 asder8215 Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xingxue-ibm Got you and thank you for testing execvp/posix_spawnp behavior on AIX!

If there's no reliance on _PATH_BSHELL (as well as no existence of _PATH_DEFPATH) and AIX forwards posix_spawnp to execvpe, then I'll hardcode the values here (should be self explanatory since it would be cfg gated on AIX). Will update the PR in the morning once I get some rest.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am not suggesting removing the existing #[link_name = "__confstr_xpg7"] from rust-lang/libc. That mapping from libc#4035 is intentional and should remain.

On Solaris, when C code is compiled with _XOPEN_SOURCE=700, <unistd.h> redirects the source-level name confstr to the ELF symbol __confstr_xpg7. The libc test configuration uses _XOPEN_SOURCE=700, which is why the Rust libc::confstr declaration needs that link_name to represent the XPG7-facing API correctly.

This is separate from what the already-compiled Solaris libc implementation of execvp does internally. Its path is effectively:

execvp -> get_PATH -> base ELF symbol "confstr"

The base confstr symbol is itself mode-sensitive: it reads the process's __xpg4/__xpg6 state and returns the corresponding path. Therefore, Rust does not need a new compile-time cfg analogous to a musl-version cfg, and it does not need to determine the XPG mode itself.

To preserve the behavior of the old execvp call, std can keep the public libc::confstr binding unchanged and declare a separate private Solaris-only binding with a different Rust name:

unsafe extern "C" {
    #[link_name = "confstr"]
    fn solaris_confstr_base(
        name: libc::c_int,
        buf: *mut libc::c_char,
        len: libc::size_t,
    ) -> libc::size_t;
}

That private function and libc::confstr refer to two different Solaris ELF symbols, so there is no conflict and no breaking change to rust-lang/libc.

The shell choice is a separate issue. For an ordinary Rust process, where __xpg4 remains at its default value of 0, _PATH_BSHELL (/usr/bin/sh) matches execvp. If exact behavior for a process explicitly linked into XPG mode is also required, the shell must likewise be selected from that process mode; globally replacing _PATH_BSHELL with /usr/xpg4/bin/sh would instead break the default mode.

@asder8215 asder8215 Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think I understand how this would work, and thank you for taking the time to clarify this with me!

In regards to this:

The shell choice is a separate issue. For an ordinary Rust process, where `__xpg4` remains at its default value of 0, `_PATH_BSHELL` (`/usr/bin/sh`) matches `execvp`. If exact behavior for a process explicitly linked into XPG mode is also required, the shell must likewise be selected from that process mode; globally replacing `_PATH_BSHELL` with `/usr/xpg4/bin/sh` would instead break the default mode.

I suppose here, would we do something similar as done for confstr for __xpg4? Like:

unsafe extern "C" {
    static __xpg4: i32;
}

From reading illumos' code, I'm assuming __xpg4 is also an int/i32 on Solaris. Is __xpg4 something I could retrieve on all the current Solaris targets that Rust supports?

Or is there a better/correct approach than the above?

To kinda recap how I think it would work in my head, I would modify library/std/src/sys/conf.rs to contain Solaris' mode-sensitive confstr and then expand + modify our current confstr for apple/linux on gnu target envs to solaris (and illumos). I would maybe put the __xpg4 extern value in this file as well.

The get_default_path() function in library/std/src/sys/process/unix/common.rs would just expand the confstr calling branch to Solaris, and then in last_execvpe_error in library/std/src/sys/process/unix/unix.rs, I would have a separate branch for Solaris that would check on __xpg4 to determine whether we'd use _PATH_BSHELL or /usr/xpg4/bin/sh for the first argument in our libc::execve call. In get_shell_argv() since both Solaris and AIX put "sh" as argv[0], I'd just cfg gate it for both to set shell_argv[0] to "sh".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for Solaris, reading __xpg4 is the correct way to reproduce the shell selection made by execvp.

I checked the current Solaris 11.4 ON sources. __xpg4 is an int, and it is exported by libc.so.1 in the SUNW_0.8 symbol version with size 4 and the NODIRECT flag. The same libc mapfile is used for both currently supported Solaris targets, sparcv9-sun-solaris and x86_64-pc-solaris. The illumos ABI has the same definition in its libc mapfile.

NODIRECT is important here: the normal definition in libc is 0, while values-xpg4.o (and values-xpg6.o) can interpose a definition that enables the conforming mode for the whole process. Consequently, a private std binding observes the same value as libc's execvp. I would use libc::c_int rather than spelling the Rust representation as i32, for example:

#[cfg(target_os = "solaris")]
fn is_xpg4() -> bool {
    unsafe extern "C" {
        #[link_name = "__xpg4"]
        static XPG4_MODE: libc::c_int;
    }

    // SAFETY: __xpg4 is an exported process-wide libc data symbol.
    unsafe { XPG4_MODE != 0 }
}

This can remain a private Solaris implementation detail in std; I do not think it needs to be added to the public rust-lang/libc API.

There are a few details I would change in the proposed layout:

  1. The mode-dependent shell is Solaris-specific. Current Solaris uses _PATH_BSHELL when __xpg4 == 0 and /usr/xpg4/bin/sh otherwise. The illumos execvp implementation you linked, however, always uses _PATH_BSHELL for ENOEXEC; it uses __xpg4 only when constructing the fallback PATH.

  2. The executable pathname and argv[0] need to remain separate. On Solaris the call is conceptually:

execve(shell_path, [c"sh", file, args..].as_ptr(), envp)

where shell_path is selected using __xpg4. The current code uses shell_argv[0] both as the pathname passed to execve and as argv[0], so supporting Solaris/AIX's "sh" value requires carrying the shell pathname separately from the argument vector.

  1. For Solaris, calling the private base confstr is only the first part of the fallback path. execvp then appends :/usr/sbin when either the real or effective UID is 0, and otherwise appends : so that the current directory is searched last. The UID test must reflect the credentials after Command::uid and any pre_exec callback have run. Since allocation is not allowed there, the root and non-root variants could be prepared before fork and selected afterwards.

  2. Illumos should not simply share the Solaris confstr(_CS_PATH) branch. In the linked illumos implementation, confstr(_CS_PATH) returns the XPG path, whereas execvp chooses among separate __xpg4/UID-dependent paths. Illumos therefore needs its own fallback-path handling if the goal is to preserve its native execvp behavior exactly.

So the __xpg4 binding itself is fine for Solaris, but I would keep the Solaris and illumos path/shell cases separate rather than treating them as one confstr implementation.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@asder8215
asder8215 force-pushed the commandext_exec_bug_fix branch from b206c99 to 2200bb8 Compare October 27, 2026 20:32
@asder8215
asder8215 requested a review from tgross35 October 27, 2026 22:12
…ng and refactor code that uses this function

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. O-unix Operating system: Unix-like S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. to-announce Announce this issue on triage meeting

Projects

None yet

Development

Successfully merging this pull request may close these issues.