Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions library/std/src/sys/pal/unix/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@ pub fn page_size() -> usize {
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
}

/// Returns the value for [`confstr(key, ...)`][posix_confstr]. Currently only
/// used on Darwin, but should work on any unix (in case we need to get
/// `_CS_PATH` or `_CS_V[67]_ENV` in the future).
/// Returns the value for [`confstr(key, ...)`][posix_confstr].
///
/// [posix_confstr]:
/// https://pubs.opengroup.org/onlinepubs/9799919799/functions/confstr.html
#[cfg(target_vendor = "apple")]
#[cfg(any(target_vendor = "apple", all(target_os = "linux", target_env = "gnu")))]
pub fn confstr(
key: crate::ffi::c_int,
size_hint: Option<usize>,
) -> crate::io::Result<crate::ffi::OsString> {
use crate::ffi::OsString;
) -> crate::io::Result<crate::ffi::CString> {
use alloc_crate::ffi::CString;

use crate::io;
use crate::os::unix::ffi::OsStringExt;

let mut buf: Vec<u8> = Vec::with_capacity(0);
let mut bytes_needed_including_nul = size_hint
Expand Down Expand Up @@ -61,11 +59,17 @@ pub fn confstr(
unsafe {
buf.set_len(bytes_needed_including_nul);
// Remove the NUL-terminator.
let last_byte = buf.pop();
let last_byte = buf.last();
// ... and smoke-check that it *was* a NUL-terminator.
assert_eq!(last_byte, Some(0), "`confstr` provided a string which wasn't nul-terminated");
assert_eq!(
last_byte.copied(),
Some(0),
"`confstr` provided a string which wasn't nul-terminated"
);
};
Ok(OsString::from_vec(buf))
// SAFETY: From above, we have confirmed that the buffer
// contains a nul terminating byte.
Ok(unsafe { CString::from_vec_with_nul_unchecked(buf) })
}

#[cfg(all(target_os = "linux", target_env = "gnu"))]
Expand Down
2 changes: 1 addition & 1 deletion library/std/src/sys/pal/unix/conf/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn test_confstr() {
let value_nohint = super::confstr(key, None).unwrap_or_else(|e| {
panic!("confstr({key}, None) failed: {e:?}");
});
let end = (value_nohint.len() + 1) * 2;
let end = (value_nohint.count_bytes() + 1) * 2;
for hint in 0..end {
assert_eq!(
super::confstr(key, Some(hint)).as_deref().ok(),
Expand Down
14 changes: 7 additions & 7 deletions library/std/src/sys/paths/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,13 +430,13 @@ pub fn current_exe() -> io::Result<PathBuf> {

#[cfg(target_vendor = "apple")]
fn darwin_temp_dir() -> PathBuf {
crate::sys::pal::conf::confstr(libc::_CS_DARWIN_USER_TEMP_DIR, Some(64))
.map(PathBuf::from)
.unwrap_or_else(|_| {
// It failed for whatever reason (there are several possible reasons),
// so return the global one.
PathBuf::from("/tmp")
})
let darwin_temp_dir = crate::sys::pal::conf::confstr(libc::_CS_DARWIN_USER_TEMP_DIR, Some(64));
match darwin_temp_dir {
Ok(temp_dir) => OsString::from_vec(temp_dir.into_bytes()).into(),
// It failed for whatever reason (there are several possible reasons),
// so return the global one.
Err(_) => PathBuf::from("/tmp"),
}
}

pub fn temp_dir() -> PathBuf {
Expand Down
34 changes: 34 additions & 0 deletions library/std/src/sys/process/unix/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,33 @@ 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.

#[cfg(all(target_os = "linux", target_env = "gnu"))]
use crate::sys::conf::confstr;
cfg_select! {
all(target_os = "linux", target_env = "gnu") => {
confstr(libc::_CS_PATH, None).unwrap_or(CString::default())
}
any(
all(target_os = "linux", not(target_env = "gnu")),
target_vendor = "apple",
target_os = "android",
target_os = "openbsd",
target_os = "freebsd",
target_os = "dragonfly",
) => {
// SAFETY: libc::_PATH_DEFPATH is a nul-terminated char pointer
unsafe { CString::from(CStr::from_ptr(libc::_PATH_DEFPATH)) }
}
target_os = "aix" => {
CString::new(":/bin:/usr/bin").expect("the provided string should not have a nul byte")
}
_ => CString::default(),
}
}

////////////////////////////////////////////////////////////////////////////////
// Command
////////////////////////////////////////////////////////////////////////////////
Expand All @@ -91,6 +118,7 @@ pub struct Command {
program: CString,
args: CStringArray,
env: CommandEnv,
default_path: CString,

program_kind: ProgramKind,
cwd: Option<CString>,
Expand Down Expand Up @@ -168,10 +196,12 @@ impl Command {
let program = os2c(program, &mut saw_nul);
let mut args = CStringArray::with_capacity(1);
args.push(program.clone());

Command {
program,
args,
env: Default::default(),
default_path: get_default_path(),
program_kind,
cwd: None,
chroot: None,
Expand Down Expand Up @@ -271,6 +301,10 @@ impl Command {
self.env.does_clear()
}

pub fn get_default_path(&self) -> &CStr {
&self.default_path
}

pub fn get_resolved_envs(&self) -> CommandResolvedEnvs {
CommandResolvedEnvs::new(self.env.capture())
}
Expand Down
5 changes: 5 additions & 0 deletions library/std/src/sys/process/unix/common/cstring_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ impl CStringArray {
drop(unsafe { CString::from_raw(old.cast_mut()) });
}

/// Returns the length of the array (null pointer excluded).
pub fn len(&self) -> usize {
self.ptrs.len() - 1
}

/// Push an additional string to the array.
pub fn push(&mut self, item: CString) {
let argc = self.ptrs.len() - 1;
Expand Down
Loading
Loading