up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
421
src/Scanner/StellaOps.Scanner.Analyzers.Native/NativeResolver.cs
Normal file
421
src/Scanner/StellaOps.Scanner.Analyzers.Native/NativeResolver.cs
Normal file
@@ -0,0 +1,421 @@
|
||||
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)
|
||||
{
|
||||
_files = new HashSet<string>(files, StringComparer.OrdinalIgnoreCase);
|
||||
_directories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var file in _files)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(file);
|
||||
while (!string.IsNullOrEmpty(dir))
|
||||
{
|
||||
_directories.Add(dir);
|
||||
dir = Path.GetDirectoryName(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 = Path.GetDirectoryName(f);
|
||||
return string.Equals(fileDir, normalizedDir, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path) =>
|
||||
path.Replace('\\', '/').TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <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('\\', '/');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user