Files
git.stella-ops.org/src/Scanner/StellaOps.Scanner.Analyzers.Native/NativeResolver.cs
StellaOps Bot 7d5250238c save progress
2025-12-18 09:53:46 +02:00

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('\\', '/');
}
}