Skip to content

Ask access(X_OK) whether an overlay PATH match may run, on every target [patch] - #141

Open
matt-edmondson wants to merge 2 commits into
mainfrom
fix/139-access-x-ok
Open

matt-edmondson wants to merge 2 commits into
mainfrom
fix/139-access-x-ok

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #139

What changed

#137 made the overlay-PATH search skip a file with no execute bit. That check used File.GetUnixFileMode under #if NET7_0_OR_GREATER, so the net5.0, net6.0 and netstandard builds accepted any file that existed. On Linux and macOS, a non-executable file earlier on the overlay PATH therefore won, and the start failed with "Permission denied" instead of running the executable copy later on the path.

  • TryExecutable now calls libc access(path, X_OK) through a new internal NativeMethods.CanExecute on every target outside Windows. All builds share this one check, so the net10.0 tests exercise the code the older builds run.
  • access answers for the calling process, which is what a shell's PATH search does. It also skips a file that only its group or others may execute. The old "any execute bit" test accepted that file, and starting it then failed.
  • If libc can't be called (DllNotFoundException / EntryPointNotFoundException), the file is kept as a candidate. That matches the behaviour before the check existed.

I left the README alone. Its sentence "On other platforms the file must have an execute bit" is accurate on every target once this lands. It is also the line #140 edits, so changing it here would make the two PRs conflict.

Tests

  • ExecuteAsyncShouldSkipAFileOnTheOverlayPathThatOnlyOthersMayExecute: the first file on the overlay path is mode 0655, so the caller owns it but may not execute it. The test expects the executable copy to run. It reports inconclusive when run as root, because root may run any file that has an execute bit.
  • CanExecuteShouldReportWhetherTheCallerMayExecuteTheFile: a 755 file can run and a 644 file can't.
  • I ran the suite as a non-root user (uid 65534) with the old check put back. The new test failed with Win32Exception ... Permission denied. With the fix, all 6 overlay/CanExecute tests passed.
  • The issue's repro, run with a scratch net10.0 console against the netstandard2.0 build: on main it fails with Win32Exception ... '/tmp/rc-noexec/rctool' ... Permission denied, and on this branch it prints executable copy.
  • Full suite as root, net10.0: 91 passed, 3 skipped (two Windows elevation tests plus the new root-inconclusive one), 0 failed. All 5 library targets build with no warnings.

Generated by Claude Code

…et [patch]

#137 skipped a file on the overlay PATH that had no execute bit, but the
check used File.GetUnixFileMode and sat under NET7_0_OR_GREATER. The net5.0,
net6.0 and netstandard builds accepted any file that existed, so on Linux and
macOS a non-executable file earlier on the overlay PATH won and the start
failed with "Permission denied" instead of running the executable copy later
on. The tests only target net10.0, so they never saw it.

TryExecutable now calls libc access(path, X_OK) through a new NativeMethods
class on every target outside Windows, so all builds share one check and the
net10.0 tests exercise the code the older builds run. access answers for the
caller, as a shell's PATH search does, so it also passes over a file only its
group or others may execute, which "any execute bit" accepted. If libc
cannot be called the file is kept, as before the check existed.

New tests: a file whose owner (the caller) lacks the execute bit while group
and others have it is skipped in favour of the executable copy (inconclusive
as root, which may run it), and CanExecute reports a 755 file as runnable and
a 644 file as not.

Fixes #139

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5DyciGEV4wZvik3pQ

[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
[DllImport("libc", EntryPoint = "access", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern int Access(string path, int mode);

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.

Keeping the P/Invoke. The suggested fix would bring #139 back:

access(X_OK) is the managed-free equivalent of what a shell's PATH search asks. The interop is one call in NativeMethods, with DefaultDllImportSearchPaths set. It falls back to keeping the candidate if libc can't be loaded, and it only runs off Windows. I'm leaving this thread open for the maintainer's call.


Generated by Claude Code

Comment thread RunCommand/NativeMethods.cs Fixed
CanExecute's DllNotFoundException / EntryPointNotFoundException fallback had
no test, so SonarCloud's new-code coverage was 60% (gate is 80%). The access
call is now a parameter of an internal overload, as TryKill's kill is, and a
test makes it throw both exceptions and checks the file is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5DyciGEV4wZvik3pQ
@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

2 participants