Skip to content

Commit cb83298

Browse files
committed
environment: check execute permission in TryLocateExecutable
In f1a1ae5 (environment: manually scan $PATH on POSIX systems, 2022-05-31) the `which`-based lookup was replaced with a manual PATH scan that only checks FileExists, without verifying execute permissions. Unlike `which`, this means a non-executable file earlier in PATH can shadow a valid executable, causing process creation to fail when GCM later tries to run the located path. Add an IsExecutable check that verifies at least one execute bit is set on POSIX systems, matching the behaviour of `which`. On Windows, any existing file is considered executable. Guard the POSIX-specific File.GetUnixFileMode call with #if !NETFRAMEWORK for net472 compatibility. Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
1 parent 2b8f907 commit cb83298

File tree

4 files changed

+79
-1
lines changed

4 files changed

+79
-1
lines changed

src/shared/Core.Tests/EnvironmentTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public void PosixEnvironment_TryLocateExecutable_Exists_ReturnTrueAndPath()
9494
[expectedPath] = Array.Empty<byte>(),
9595
}
9696
};
97+
fs.SetExecutable(expectedPath);
9798
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
9899
var env = new PosixEnvironment(fs, envars);
99100

@@ -116,6 +117,32 @@ public void PosixEnvironment_TryLocateExecutable_ExistsMultiple_ReturnTrueAndFir
116117
["/bin/foo"] = Array.Empty<byte>(),
117118
}
118119
};
120+
fs.SetExecutable(expectedPath);
121+
fs.SetExecutable("/usr/local/bin/foo");
122+
fs.SetExecutable("/bin/foo");
123+
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
124+
var env = new PosixEnvironment(fs, envars);
125+
126+
bool actualResult = env.TryLocateExecutable(PosixExecName, out string actualPath);
127+
128+
Assert.True(actualResult);
129+
Assert.Equal(expectedPath, actualPath);
130+
}
131+
132+
[PosixFact]
133+
public void PosixEnvironment_TryLocateExecutable_NotExecutable_SkipsToNextMatch()
134+
{
135+
string nonExecPath = "/home/john.doe/bin/foo";
136+
string expectedPath = "/usr/local/bin/foo";
137+
var fs = new TestFileSystem
138+
{
139+
Files = new Dictionary<string, byte[]>
140+
{
141+
[nonExecPath] = Array.Empty<byte>(),
142+
[expectedPath] = Array.Empty<byte>(),
143+
}
144+
};
145+
fs.SetExecutable(expectedPath);
119146
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
120147
var env = new PosixEnvironment(fs, envars);
121148

@@ -142,6 +169,8 @@ public void MacOSEnvironment_TryLocateExecutable_Paths_Are_Ignored()
142169
[expectedPath] = Array.Empty<byte>(),
143170
}
144171
};
172+
fs.SetExecutable(pathsToIgnore.FirstOrDefault());
173+
fs.SetExecutable(expectedPath);
145174
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
146175
var env = new PosixEnvironment(fs, envars);
147176

src/shared/Core/EnvironmentBase.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ internal virtual bool TryLocateExecutable(string program, ICollection<string> pa
138138
{
139139
string candidatePath = Path.Combine(basePath, program);
140140
if (FileSystem.FileExists(candidatePath) && (pathsToIgnore is null ||
141-
!pathsToIgnore.Contains(candidatePath, StringComparer.OrdinalIgnoreCase)))
141+
!pathsToIgnore.Contains(candidatePath, StringComparer.OrdinalIgnoreCase))
142+
&& FileSystem.FileIsExecutable(candidatePath))
142143
{
143144
path = candidatePath;
144145
return true;

src/shared/Core/FileSystem.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ public interface IFileSystem
3434
/// <returns>True if a file exists, false otherwise.</returns>
3535
bool FileExists(string path);
3636

37+
/// <summary>
38+
/// Check if a file has execute permissions.
39+
/// On Windows this always returns true. On POSIX it checks for any execute bit.
40+
/// </summary>
41+
/// <param name="path">Full path to file to test.</param>
42+
/// <returns>True if the file is executable, false otherwise.</returns>
43+
bool FileIsExecutable(string path);
44+
3745
/// <summary>
3846
/// Check if a directory exists at the specified path.
3947
/// </summary>
@@ -122,6 +130,23 @@ public abstract class FileSystem : IFileSystem
122130

123131
public bool FileExists(string path) => File.Exists(path);
124132

133+
#if NETFRAMEWORK
134+
public bool FileIsExecutable(string path) => true;
135+
#else
136+
public bool FileIsExecutable(string path)
137+
{
138+
if (!PlatformUtils.IsPosix())
139+
return true;
140+
141+
#pragma warning disable CA1416 // Platform guard via PlatformUtils.IsPosix()
142+
var mode = File.GetUnixFileMode(path);
143+
return (mode & (UnixFileMode.UserExecute |
144+
UnixFileMode.GroupExecute |
145+
UnixFileMode.OtherExecute)) != 0;
146+
#pragma warning restore CA1416
147+
}
148+
#endif
149+
125150
public bool DirectoryExists(string path) => Directory.Exists(path);
126151

127152
public string GetCurrentDirectory() => Directory.GetCurrentDirectory();

src/shared/TestInfrastructure/Objects/TestFileSystem.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class TestFileSystem : IFileSystem
1111
public string UserHomePath { get; set; }
1212
public string UserDataDirectoryPath { get; set; }
1313
public IDictionary<string, byte[]> Files { get; set; } = new Dictionary<string, byte[]>();
14+
public ISet<string> ExecutableFiles { get; } = new HashSet<string>();
1415
public ISet<string> Directories { get; set; } = new HashSet<string>();
1516
public string CurrentDirectory { get; set; } = Path.GetTempPath();
1617
public bool IsCaseSensitive { get; set; } = false;
@@ -36,6 +37,14 @@ bool IFileSystem.FileExists(string path)
3637
return Files.ContainsKey(path);
3738
}
3839

40+
bool IFileSystem.FileIsExecutable(string path)
41+
{
42+
if (!Files.ContainsKey(path))
43+
throw new FileNotFoundException("File not found", path);
44+
45+
return ExecutableFiles.Contains(path);
46+
}
47+
3948
bool IFileSystem.DirectoryExists(string path)
4049
{
4150
return Directories.Contains(TrimSlash(path));
@@ -130,6 +139,20 @@ string[] IFileSystem.ReadAllLines(string path)
130139

131140
#endregion
132141

142+
/// <summary>
143+
/// Mark a test file as executable. File must exist in <see cref="Files"/> already.
144+
/// </summary>
145+
public void SetExecutable(string path, bool isExecutable = true)
146+
{
147+
if (!Files.ContainsKey(path))
148+
throw new FileNotFoundException("File not found", path);
149+
150+
if (isExecutable)
151+
ExecutableFiles.Add(path);
152+
else
153+
ExecutableFiles.Remove(path);
154+
}
155+
133156
/// <summary>
134157
/// Trim trailing slashes from a path.
135158
/// </summary>

0 commit comments

Comments
 (0)