namespace StellaOps.Scanner.Analyzers.Native; /// /// Represents a step in the resolution process for explain traces. /// /// The path that was searched. /// Why this path was searched (e.g., "rpath", "runpath", "default"). /// Whether the library was found at this path. /// The full resolved path if found. public sealed record ResolveStep( string SearchPath, string SearchReason, bool Found, string? ResolvedPath); /// /// Result of resolving a native library dependency. /// /// The original library name that was requested. /// Whether the library was successfully resolved. /// The final resolved path (if resolved). /// The resolution steps taken (explain trace). public sealed record ResolveResult( string RequestedName, bool Resolved, string? ResolvedPath, IReadOnlyList Steps); /// /// Virtual filesystem interface for resolver operations. /// public interface IVirtualFileSystem { bool FileExists(string path); bool DirectoryExists(string path); IEnumerable EnumerateFiles(string directory, string pattern); } /// /// Simple virtual filesystem implementation backed by a set of known paths. /// public sealed class VirtualFileSystem : IVirtualFileSystem { private readonly HashSet _files; private readonly HashSet _directories; public VirtualFileSystem(IEnumerable files) { ArgumentNullException.ThrowIfNull(files); _files = new HashSet(StringComparer.OrdinalIgnoreCase); _directories = new HashSet(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 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]; } } /// /// Resolves ELF shared library dependencies using the Linux dynamic linker algorithm. /// public static class ElfResolver { private static readonly string[] DefaultSearchPaths = [ "/lib64", "/usr/lib64", "/lib", "/usr/lib", "/usr/local/lib64", "/usr/local/lib", ]; /// /// Resolves an ELF shared library dependency. /// /// The soname to resolve (e.g., "libc.so.6"). /// DT_RPATH values from the binary. /// DT_RUNPATH values from the binary. /// LD_LIBRARY_PATH entries (optional). /// The $ORIGIN value (directory of the executable). /// Virtual filesystem for checking existence. /// Resolution result with explain trace. public static ResolveResult Resolve( string soname, IReadOnlyList rpaths, IReadOnlyList runpaths, IReadOnlyList? ldLibraryPath, string? origin, IVirtualFileSystem fs) { var steps = new List(); // 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 paths, string reason, string? origin, IVirtualFileSystem fs, List 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); } } /// /// Resolves PE DLL dependencies using the Windows DLL search order. /// public static class PeResolver { private static readonly string[] SystemDirectories = [ "C:/Windows/System32", "C:/Windows/SysWOW64", "C:/Windows", ]; /// /// Resolves a PE DLL dependency using SafeDll search order. /// /// The DLL name to resolve. /// The application's directory. /// The current working directory (optional, used with LOAD_WITH_ALTERED_SEARCH_PATH). /// PATH environment variable entries. /// Virtual filesystem for checking existence. /// Resolution result with explain trace. public static ResolveResult Resolve( string dllName, string? applicationDirectory, string? currentDirectory, IReadOnlyList? pathEnvironment, IVirtualFileSystem fs) { var steps = new List(); // 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 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; } } /// /// Resolves Mach-O dylib dependencies using the macOS dynamic linker algorithm. /// public static class MachOResolver { private static readonly string[] DefaultFrameworkPaths = [ "/System/Library/Frameworks", "/Library/Frameworks", ]; private static readonly string[] DefaultLibraryPaths = [ "/usr/lib", "/usr/local/lib", ]; /// /// Resolves a Mach-O dylib dependency. /// /// The dylib path (may contain @rpath, @loader_path, @executable_path). /// LC_RPATH values from the binary. /// The @loader_path value (directory of the loading binary). /// The @executable_path value (directory of the main executable). /// Virtual filesystem for checking existence. /// Resolution result with explain trace. public static ResolveResult Resolve( string dylibPath, IReadOnlyList rpaths, string? loaderPath, string? executablePath, IVirtualFileSystem fs) { var steps = new List(); // 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('\\', '/'); } }