477 lines
16 KiB
C#
477 lines
16 KiB
C#
namespace StellaOps.Scanner.Analyzers.Native;
|
|
|
|
/// <summary>
|
|
/// Represents a step in the resolution process for explain traces.
|
|
/// </summary>
|
|
/// <param name="SearchPath">The path that was searched.</param>
|
|
/// <param name="SearchReason">Why this path was searched (e.g., "rpath", "runpath", "default").</param>
|
|
/// <param name="Found">Whether the library was found at this path.</param>
|
|
/// <param name="ResolvedPath">The full resolved path if found.</param>
|
|
public sealed record ResolveStep(
|
|
string SearchPath,
|
|
string SearchReason,
|
|
bool Found,
|
|
string? ResolvedPath);
|
|
|
|
/// <summary>
|
|
/// Result of resolving a native library dependency.
|
|
/// </summary>
|
|
/// <param name="RequestedName">The original library name that was requested.</param>
|
|
/// <param name="Resolved">Whether the library was successfully resolved.</param>
|
|
/// <param name="ResolvedPath">The final resolved path (if resolved).</param>
|
|
/// <param name="Steps">The resolution steps taken (explain trace).</param>
|
|
public sealed record ResolveResult(
|
|
string RequestedName,
|
|
bool Resolved,
|
|
string? ResolvedPath,
|
|
IReadOnlyList<ResolveStep> Steps);
|
|
|
|
/// <summary>
|
|
/// Virtual filesystem interface for resolver operations.
|
|
/// </summary>
|
|
public interface IVirtualFileSystem
|
|
{
|
|
bool FileExists(string path);
|
|
bool DirectoryExists(string path);
|
|
IEnumerable<string> EnumerateFiles(string directory, string pattern);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple virtual filesystem implementation backed by a set of known paths.
|
|
/// </summary>
|
|
public sealed class VirtualFileSystem : IVirtualFileSystem
|
|
{
|
|
private readonly HashSet<string> _files;
|
|
private readonly HashSet<string> _directories;
|
|
|
|
public VirtualFileSystem(IEnumerable<string> files)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(files);
|
|
|
|
_files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
_directories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var file in files)
|
|
{
|
|
var normalizedFile = NormalizePath(file);
|
|
if (string.IsNullOrWhiteSpace(normalizedFile))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_files.Add(normalizedFile);
|
|
|
|
var dir = GetDirectoryName(normalizedFile);
|
|
while (!string.IsNullOrEmpty(dir))
|
|
{
|
|
var normalizedDir = NormalizePath(dir);
|
|
if (!string.IsNullOrEmpty(normalizedDir))
|
|
{
|
|
_directories.Add(normalizedDir);
|
|
}
|
|
|
|
dir = GetParentDirectory(dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool FileExists(string path) => _files.Contains(NormalizePath(path));
|
|
public bool DirectoryExists(string path) => _directories.Contains(NormalizePath(path));
|
|
|
|
public IEnumerable<string> EnumerateFiles(string directory, string pattern)
|
|
{
|
|
var normalizedDir = NormalizePath(directory);
|
|
return _files.Where(f =>
|
|
{
|
|
var fileDir = GetDirectoryName(f);
|
|
return string.Equals(fileDir, normalizedDir, StringComparison.OrdinalIgnoreCase);
|
|
});
|
|
}
|
|
|
|
private static string NormalizePath(string path) =>
|
|
TrimEndDirectorySeparators(path.Replace('\\', '/'));
|
|
|
|
private static string TrimEndDirectorySeparators(string path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var normalized = path;
|
|
while (normalized.Length > 1 && normalized.EndsWith("/", StringComparison.Ordinal))
|
|
{
|
|
normalized = normalized[..^1];
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
private static string GetDirectoryName(string path)
|
|
{
|
|
var normalized = NormalizePath(path);
|
|
var lastSlash = normalized.LastIndexOf('/');
|
|
if (lastSlash <= 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return normalized[..lastSlash];
|
|
}
|
|
|
|
private static string GetParentDirectory(string directory)
|
|
{
|
|
var normalized = NormalizePath(directory);
|
|
var lastSlash = normalized.LastIndexOf('/');
|
|
if (lastSlash <= 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return normalized[..lastSlash];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves ELF shared library dependencies using the Linux dynamic linker algorithm.
|
|
/// </summary>
|
|
public static class ElfResolver
|
|
{
|
|
private static readonly string[] DefaultSearchPaths =
|
|
[
|
|
"/lib64",
|
|
"/usr/lib64",
|
|
"/lib",
|
|
"/usr/lib",
|
|
"/usr/local/lib64",
|
|
"/usr/local/lib",
|
|
];
|
|
|
|
/// <summary>
|
|
/// Resolves an ELF shared library dependency.
|
|
/// </summary>
|
|
/// <param name="soname">The soname to resolve (e.g., "libc.so.6").</param>
|
|
/// <param name="rpaths">DT_RPATH values from the binary.</param>
|
|
/// <param name="runpaths">DT_RUNPATH values from the binary.</param>
|
|
/// <param name="ldLibraryPath">LD_LIBRARY_PATH entries (optional).</param>
|
|
/// <param name="origin">The $ORIGIN value (directory of the executable).</param>
|
|
/// <param name="fs">Virtual filesystem for checking existence.</param>
|
|
/// <returns>Resolution result with explain trace.</returns>
|
|
public static ResolveResult Resolve(
|
|
string soname,
|
|
IReadOnlyList<string> rpaths,
|
|
IReadOnlyList<string> runpaths,
|
|
IReadOnlyList<string>? ldLibraryPath,
|
|
string? origin,
|
|
IVirtualFileSystem fs)
|
|
{
|
|
var steps = new List<ResolveStep>();
|
|
|
|
// Resolution order (simplified Linux dynamic linker):
|
|
// 1. DT_RPATH (unless DT_RUNPATH is present)
|
|
// 2. LD_LIBRARY_PATH
|
|
// 3. DT_RUNPATH
|
|
// 4. /etc/ld.so.cache (skipped - not available in virtual fs)
|
|
// 5. Default paths (/lib, /usr/lib, etc.)
|
|
|
|
var hasRunpath = runpaths.Count > 0;
|
|
|
|
// 1. DT_RPATH (only if no RUNPATH)
|
|
if (!hasRunpath && rpaths.Count > 0)
|
|
{
|
|
var result = SearchPaths(soname, rpaths, "rpath", origin, fs, steps);
|
|
if (result != null)
|
|
{
|
|
return new ResolveResult(soname, true, result, steps);
|
|
}
|
|
}
|
|
|
|
// 2. LD_LIBRARY_PATH
|
|
if (ldLibraryPath is { Count: > 0 })
|
|
{
|
|
var result = SearchPaths(soname, ldLibraryPath, "ld_library_path", origin, fs, steps);
|
|
if (result != null)
|
|
{
|
|
return new ResolveResult(soname, true, result, steps);
|
|
}
|
|
}
|
|
|
|
// 3. DT_RUNPATH
|
|
if (hasRunpath)
|
|
{
|
|
var result = SearchPaths(soname, runpaths, "runpath", origin, fs, steps);
|
|
if (result != null)
|
|
{
|
|
return new ResolveResult(soname, true, result, steps);
|
|
}
|
|
}
|
|
|
|
// 4. Default paths
|
|
var defaultResult = SearchPaths(soname, DefaultSearchPaths, "default", origin, fs, steps);
|
|
if (defaultResult != null)
|
|
{
|
|
return new ResolveResult(soname, true, defaultResult, steps);
|
|
}
|
|
|
|
return new ResolveResult(soname, false, null, steps);
|
|
}
|
|
|
|
private static string? SearchPaths(
|
|
string soname,
|
|
IEnumerable<string> paths,
|
|
string reason,
|
|
string? origin,
|
|
IVirtualFileSystem fs,
|
|
List<ResolveStep> steps)
|
|
{
|
|
foreach (var path in paths)
|
|
{
|
|
var expandedPath = ExpandOrigin(path, origin);
|
|
var fullPath = Path.Combine(expandedPath, soname).Replace('\\', '/');
|
|
|
|
var found = fs.FileExists(fullPath);
|
|
steps.Add(new ResolveStep(expandedPath, reason, found, found ? fullPath : null));
|
|
|
|
if (found)
|
|
{
|
|
return fullPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string ExpandOrigin(string path, string? origin)
|
|
{
|
|
if (string.IsNullOrEmpty(origin))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
return path
|
|
.Replace("$ORIGIN", origin)
|
|
.Replace("${ORIGIN}", origin);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves PE DLL dependencies using the Windows DLL search order.
|
|
/// </summary>
|
|
public static class PeResolver
|
|
{
|
|
private static readonly string[] SystemDirectories =
|
|
[
|
|
"C:/Windows/System32",
|
|
"C:/Windows/SysWOW64",
|
|
"C:/Windows",
|
|
];
|
|
|
|
/// <summary>
|
|
/// Resolves a PE DLL dependency using SafeDll search order.
|
|
/// </summary>
|
|
/// <param name="dllName">The DLL name to resolve.</param>
|
|
/// <param name="applicationDirectory">The application's directory.</param>
|
|
/// <param name="currentDirectory">The current working directory (optional, used with LOAD_WITH_ALTERED_SEARCH_PATH).</param>
|
|
/// <param name="pathEnvironment">PATH environment variable entries.</param>
|
|
/// <param name="fs">Virtual filesystem for checking existence.</param>
|
|
/// <returns>Resolution result with explain trace.</returns>
|
|
public static ResolveResult Resolve(
|
|
string dllName,
|
|
string? applicationDirectory,
|
|
string? currentDirectory,
|
|
IReadOnlyList<string>? pathEnvironment,
|
|
IVirtualFileSystem fs)
|
|
{
|
|
var steps = new List<ResolveStep>();
|
|
|
|
// SafeDllSearchMode search order (default in Windows):
|
|
// 1. Application directory
|
|
// 2. System directory (System32)
|
|
// 3. 16-bit system directory (System)
|
|
// 4. Windows directory
|
|
// 5. Current directory
|
|
// 6. PATH directories
|
|
|
|
// 1. Application directory
|
|
if (!string.IsNullOrEmpty(applicationDirectory))
|
|
{
|
|
var result = TryPath(dllName, applicationDirectory, "application_directory", fs, steps);
|
|
if (result != null) return new ResolveResult(dllName, true, result, steps);
|
|
}
|
|
|
|
// 2-4. System directories
|
|
foreach (var sysDir in SystemDirectories)
|
|
{
|
|
var result = TryPath(dllName, sysDir, "system_directory", fs, steps);
|
|
if (result != null) return new ResolveResult(dllName, true, result, steps);
|
|
}
|
|
|
|
// 5. Current directory
|
|
if (!string.IsNullOrEmpty(currentDirectory))
|
|
{
|
|
var result = TryPath(dllName, currentDirectory, "current_directory", fs, steps);
|
|
if (result != null) return new ResolveResult(dllName, true, result, steps);
|
|
}
|
|
|
|
// 6. PATH directories
|
|
if (pathEnvironment is { Count: > 0 })
|
|
{
|
|
foreach (var pathDir in pathEnvironment)
|
|
{
|
|
var result = TryPath(dllName, pathDir, "path_environment", fs, steps);
|
|
if (result != null) return new ResolveResult(dllName, true, result, steps);
|
|
}
|
|
}
|
|
|
|
return new ResolveResult(dllName, false, null, steps);
|
|
}
|
|
|
|
private static string? TryPath(
|
|
string dllName,
|
|
string directory,
|
|
string reason,
|
|
IVirtualFileSystem fs,
|
|
List<ResolveStep> steps)
|
|
{
|
|
var fullPath = Path.Combine(directory, dllName).Replace('\\', '/');
|
|
var found = fs.FileExists(fullPath);
|
|
steps.Add(new ResolveStep(directory, reason, found, found ? fullPath : null));
|
|
return found ? fullPath : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves Mach-O dylib dependencies using the macOS dynamic linker algorithm.
|
|
/// </summary>
|
|
public static class MachOResolver
|
|
{
|
|
private static readonly string[] DefaultFrameworkPaths =
|
|
[
|
|
"/System/Library/Frameworks",
|
|
"/Library/Frameworks",
|
|
];
|
|
|
|
private static readonly string[] DefaultLibraryPaths =
|
|
[
|
|
"/usr/lib",
|
|
"/usr/local/lib",
|
|
];
|
|
|
|
/// <summary>
|
|
/// Resolves a Mach-O dylib dependency.
|
|
/// </summary>
|
|
/// <param name="dylibPath">The dylib path (may contain @rpath, @loader_path, @executable_path).</param>
|
|
/// <param name="rpaths">LC_RPATH values from the binary.</param>
|
|
/// <param name="loaderPath">The @loader_path value (directory of the loading binary).</param>
|
|
/// <param name="executablePath">The @executable_path value (directory of the main executable).</param>
|
|
/// <param name="fs">Virtual filesystem for checking existence.</param>
|
|
/// <returns>Resolution result with explain trace.</returns>
|
|
public static ResolveResult Resolve(
|
|
string dylibPath,
|
|
IReadOnlyList<string> rpaths,
|
|
string? loaderPath,
|
|
string? executablePath,
|
|
IVirtualFileSystem fs)
|
|
{
|
|
var steps = new List<ResolveStep>();
|
|
|
|
// Handle @rpath
|
|
if (dylibPath.StartsWith("@rpath/", StringComparison.Ordinal))
|
|
{
|
|
var relativePath = dylibPath[7..]; // Remove "@rpath/"
|
|
|
|
foreach (var rpath in rpaths)
|
|
{
|
|
var expandedRpath = ExpandPlaceholders(rpath, loaderPath, executablePath);
|
|
var fullPath = Path.Combine(expandedRpath, relativePath).Replace('\\', '/');
|
|
var found = fs.FileExists(fullPath);
|
|
|
|
steps.Add(new ResolveStep(expandedRpath, "rpath", found, found ? fullPath : null));
|
|
|
|
if (found)
|
|
{
|
|
return new ResolveResult(dylibPath, true, fullPath, steps);
|
|
}
|
|
}
|
|
|
|
// Try default paths for @rpath
|
|
foreach (var defaultPath in DefaultLibraryPaths)
|
|
{
|
|
var fullPath = Path.Combine(defaultPath, relativePath).Replace('\\', '/');
|
|
var found = fs.FileExists(fullPath);
|
|
|
|
steps.Add(new ResolveStep(defaultPath, "default_library_path", found, found ? fullPath : null));
|
|
|
|
if (found)
|
|
{
|
|
return new ResolveResult(dylibPath, true, fullPath, steps);
|
|
}
|
|
}
|
|
|
|
return new ResolveResult(dylibPath, false, null, steps);
|
|
}
|
|
|
|
// Handle @loader_path
|
|
if (dylibPath.StartsWith("@loader_path/", StringComparison.Ordinal))
|
|
{
|
|
var relativePath = dylibPath[13..];
|
|
var expanded = ExpandPlaceholders(dylibPath, loaderPath, executablePath);
|
|
var found = fs.FileExists(expanded);
|
|
|
|
steps.Add(new ResolveStep(loaderPath ?? ".", "loader_path", found, found ? expanded : null));
|
|
return new ResolveResult(dylibPath, found, found ? expanded : null, steps);
|
|
}
|
|
|
|
// Handle @executable_path
|
|
if (dylibPath.StartsWith("@executable_path/", StringComparison.Ordinal))
|
|
{
|
|
var expanded = ExpandPlaceholders(dylibPath, loaderPath, executablePath);
|
|
var found = fs.FileExists(expanded);
|
|
|
|
steps.Add(new ResolveStep(executablePath ?? ".", "executable_path", found, found ? expanded : null));
|
|
return new ResolveResult(dylibPath, found, found ? expanded : null, steps);
|
|
}
|
|
|
|
// Absolute path or relative path
|
|
if (dylibPath.StartsWith("/", StringComparison.Ordinal))
|
|
{
|
|
var found = fs.FileExists(dylibPath);
|
|
steps.Add(new ResolveStep(Path.GetDirectoryName(dylibPath) ?? "/", "absolute_path", found, found ? dylibPath : null));
|
|
return new ResolveResult(dylibPath, found, found ? dylibPath : null, steps);
|
|
}
|
|
|
|
// Relative path - search in default locations
|
|
foreach (var defaultPath in DefaultLibraryPaths)
|
|
{
|
|
var fullPath = Path.Combine(defaultPath, dylibPath).Replace('\\', '/');
|
|
var found = fs.FileExists(fullPath);
|
|
|
|
steps.Add(new ResolveStep(defaultPath, "default_library_path", found, found ? fullPath : null));
|
|
|
|
if (found)
|
|
{
|
|
return new ResolveResult(dylibPath, true, fullPath, steps);
|
|
}
|
|
}
|
|
|
|
return new ResolveResult(dylibPath, false, null, steps);
|
|
}
|
|
|
|
private static string ExpandPlaceholders(string path, string? loaderPath, string? executablePath)
|
|
{
|
|
var result = path;
|
|
|
|
if (!string.IsNullOrEmpty(loaderPath))
|
|
{
|
|
result = result.Replace("@loader_path", loaderPath);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(executablePath))
|
|
{
|
|
result = result.Replace("@executable_path", executablePath);
|
|
}
|
|
|
|
return result.Replace('\\', '/');
|
|
}
|
|
}
|