Skip to content

Overlay PATH lookup picks a file the current user can't execute (e.g. root-owned 0700), so the start fails with "Permission denied" where the shell runs the next match (net7+) #143

Open

Description

@matt-edmondson

What's wrong

Since #137, when EnvironmentVariables overrides PATH, RunCommand resolves a bare executable name itself (ResolveOnSearchPath) and pins startInfo.FileName to the result (RunCommand/RunCommand.cs:413-417).

The per-candidate check in TryExecutable (RunCommand/RunCommand.cs:528-533, net7+) accepts a file if any of the user, group or other execute bits is set:

const UnixFileMode anyExecute = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
if (!OperatingSystem.IsWindows() && (File.GetUnixFileMode(candidate) & anyExecute) == 0)

It never checks whether the current user can execute the file. A file with mode 0700 owned by another user passes the check and ends the search, though execve will refuse it.

execvp, the shell, and .NET's own PATH search all skip such a file and keep looking. .NET's own search is the one used when there is no overlay, and it checks the bit that applies to the effective uid and gid.

This is a sibling of #139, but a different case:

Failure scenario

The caller runs as a non-root user, with overlay PATH=/opt/a:/opt/b:

  • /opt/a/tool is owned by root with mode 0700. This is common with shared toolchains and per-user installs.
  • /opt/b/tool has mode 0755.

Reproduced on net10 running as nobody:

native (caller PATH=a:b, no overlay): exit=0 out:from-b
overlay (PATH=a:b):  Win32Exception: An error occurred trying to start process '/…/a/tool' … Permission denied
shell (sh -c tool):  from-b

The same PATH, passed through the overlay, turns a working command into a startup failure.

Suggested fix / acceptance criteria

  • On non-Windows platforms, decide executability the way the runtime does:
    • Check the user bit when the file's uid matches geteuid().
    • Otherwise check the group bit when the file's gid matches the effective gid or one of the supplementary groups.
    • Otherwise check the other bit.
    • Root needs any execute bit.
  • Alternatively, P/Invoke access(path, X_OK), or faccessat with AT_EACCESS.
  • If not executable, skip the candidate and continue the search, as net5.0/net6.0/netstandard builds pick a non-executable file earlier on the overlay PATH, so the command fails with "Permission denied" instead of running the executable copy later on #139 asks for the no-bit case.
  • Add a test, Unix-only and skipped when running as root, that fails today:
    • An earlier PATH entry holds a file the test user can't execute: owned by another user with mode 0700, or owned by the test user with mode 0070 or 0007.
    • A later entry holds an executable copy.
    • The test asserts that the later one runs.

It's probably simplest to fix this together with #139, so all target frameworks share one executability check.

Activity

  1. matt-edmondson commented on Nov 4, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions