Files
AdvDebug-Brovan/Brovan/GeneralHelper.cs
T
AdvDebug 0fd65008d5 Map guest-visible Windows image paths
Add a dedicated `GuestImagePath` in the emulator and switch Windows guest/process metadata, startup parameters, and module lookups to use guest-visible paths instead of raw host paths. Extend IO path handling with Windows mount mappings and Windows-path helpers so PE images launched on non-Windows hosts resolve to valid in-guest paths and path-based APIs behave correctly across platforms.
2026-08-03 18:30:14 +03:00

3822 lines
152 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Formats.Tar;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using Brovan.Core.Emulation;
using Brovan.Core.Helpers;
using Brovan.Core;
using Microsoft.Win32.SafeHandles;
using static Brovan.Core.Helpers.BinaryHelpers;
using static Brovan.Core.Helpers.Utils;
namespace Brovan
{
public struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public UIntPtr RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO
{
public uint cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
internal class NativeWinImports
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, UIntPtr dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern UIntPtr VirtualQuery(IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, UIntPtr dwLength);
[DllImport("kernel32.dll")]
public static extern bool GetProcessMitigationPolicy(IntPtr hProcess, uint MitigationPolicy, out uint Buffer, UIntPtr Length);
[DllImport("kernelbase.dll", SetLastError = true)]
public static extern IntPtr GetModuleHandleA(string Library);
[DllImport("kernelbase.dll", SetLastError = true)]
public static extern IntPtr LoadLibraryA(string Library);
[DllImport("kernelbase.dll", SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string Function);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute, IntPtr lpValue, IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern void DeleteProcThreadAttributeList(IntPtr lpAttributeList);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessW(string lpApplicationName, StringBuilder lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFOEX lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetConsoleScreenBufferInfo(IntPtr hConsoleOutput, out ConsoleScreenBufferInfo lpConsoleScreenBufferInfo);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool ReadConsoleOutputCharacterW(IntPtr hConsoleOutput, StringBuilder lpCharacter, uint nLength, int dwReadCoord, out uint lpNumberOfCharsRead);
[DllImport("kernel32.dll")]
public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out int lpMode);
[DllImport("kernel32.dll")]
public static extern bool SetConsoleMode(IntPtr hConsoleHandle, int dwMode);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern SafeFileHandle CreateFileW(string FileName, uint DesiredAccess, FileShare ShareMode, IntPtr SecurityAttributes, FileMode CreationDisposition, uint FlagsAndAttributes, IntPtr TemplateFile);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateHardLinkW(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateSymbolicLinkW(string lpSymlinkFileName, string lpTargetFileName, int dwFlags);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeviceIoControl(SafeFileHandle Handle, uint IoControlCode, IntPtr InBuffer, int InBufferSize, byte[] OutBuffer, int OutBufferSize, out int BytesReturned, IntPtr Overlapped);
[DllImport("user32.dll")]
public static extern int GetSystemMetrics(int nIndex);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern unsafe bool EnumDisplaySettingsW(string lpszDeviceName, int iModeNum, byte* lpDevMode);
[DllImport("user32.dll")]
public static extern int GetSystemMetricsForDpi(int nIndex, uint dpi);
[DllImport("user32.dll")]
public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
[DllImport("user32.dll")]
public static extern IntPtr MonitorFromWindow(IntPtr hWnd, int dwFlags);
[DllImport("shcore.dll")]
public static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY);
}
internal class NativeUnixImports
{
[DllImport("libc", EntryPoint = "link", SetLastError = true, CharSet = CharSet.Ansi)]
public static extern int Link(string oldpath, string newpath);
[DllImport("libc", EntryPoint = "symlink", SetLastError = true, CharSet = CharSet.Ansi)]
public static extern int Symlink(string oldpath, string newpath);
}
internal class GeneralHelper
{
public static bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
/// <summary>
/// returns true for android too.
/// </summary>
public static bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
public static string WindowsLibsPath = Path.Combine(AppContext.BaseDirectory, "WindowsLibs");
public static string System32
{
get
{
if (IsWindows)
{
return "C:\\Windows\\System32";
}
else
{
return Path.Combine(AppContext.BaseDirectory, "WindowsLibs");
}
}
}
public static string SysWOW64
{
get
{
if (IsWindows)
{
return "C:\\Windows\\SysWOW64";
}
else
{
return Path.Combine(AppContext.BaseDirectory, "WindowsLibs\\SysWOW64");
}
}
}
private delegate IntPtr GetPebPtr();
public static string GetWindowsLibPath(string Library, bool IsWow64 = false, BinaryArchitecture Arch = BinaryArchitecture.x64)
{
if (string.IsNullOrWhiteSpace(Library))
return string.Empty;
if (!Library.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
Library += ".dll";
bool Wow64View = IsWow64 || Arch == BinaryArchitecture.x86;
string ShippedPath = Wow64View
? Path.Combine(WindowsLibsPath, "SysWOW64")
: WindowsLibsPath;
string BasePath;
if (IsWindows)
{
string WindowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
BasePath = Wow64View && Environment.Is64BitOperatingSystem
? Path.Combine(WindowsDir, "SysWOW64")
: Path.Combine(WindowsDir, "System32");
}
else
{
BasePath = ShippedPath;
}
string Result = Path.Combine(BasePath, Library);
if (!File.Exists(Result))
{
// The Visual C++ runtimes are not part of Windows, so --install-runtimes puts them in WindowsLibs
// even on a Windows host, where System32 only has them once a redistributable has been installed.
if (IsWindows)
{
string Shipped = Path.Combine(ShippedPath, Library);
if (File.Exists(Shipped))
return Shipped;
}
else
{
string Resolved = ResolveShippedLibraryCase(BasePath, Library);
if (Resolved != null)
return Resolved;
}
PrintHighlight($"[-] Windows library not found: {Result}", true);
}
return Result;
}
private static readonly Dictionary<string, Dictionary<string, string>> ShippedLibraryIndex = new(StringComparer.Ordinal);
private static string ResolveShippedLibraryCase(string Directory, string FileName)
{
Dictionary<string, string> Index;
lock (ShippedLibraryIndex)
{
if (!ShippedLibraryIndex.TryGetValue(Directory, out Index))
{
Index = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
foreach (string Entry in System.IO.Directory.EnumerateFiles(Directory))
Index[Path.GetFileName(Entry)] = Entry;
}
catch (DirectoryNotFoundException)
{
}
ShippedLibraryIndex[Directory] = Index;
}
}
return Index.TryGetValue(FileName, out string Actual) ? Actual : null;
}
public static bool DumpApiSetMap()
{
try
{
const uint MEM_COMMIT = 0x1000;
const uint MEM_RESERVE = 0x2000;
const uint PAGE_READWRITE = 0x04;
const uint PAGE_EXECUTE_READ = 0x20;
const uint MEM_COMMIT_STATE = 0x1000;
const uint PAGE_NOACCESS = 0x01;
const uint PAGE_GUARD = 0x100;
byte[] StubX64 = { 0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00, 0xC3 };
byte[] StubX86 = { 0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0xC3 };
bool IsX64 = IntPtr.Size == 8;
byte[] Stub = IsX64 ? StubX64 : StubX86;
IntPtr AllocatedMemory = NativeWinImports.VirtualAlloc(IntPtr.Zero, (UIntPtr)Stub.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (AllocatedMemory == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc failed while allocating memory for the PEB-retrieval code.");
Marshal.Copy(Stub, 0, AllocatedMemory, Stub.Length);
if (!NativeWinImports.VirtualProtect(AllocatedMemory, (UIntPtr)Stub.Length, PAGE_EXECUTE_READ, out _))
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualProtect failed while changing protection for the PEB-retrieval code.");
var PEB = Marshal.GetDelegateForFunctionPointer<GetPebPtr>(AllocatedMemory);
IntPtr ApiSetMapPtr = PEB() + (IsX64 ? 0x68 : 0x38);
unsafe
{
IntPtr ApiSetMap = (IntPtr)(IsX64 ? *(ulong*)ApiSetMapPtr : *(uint*)ApiSetMapPtr);
if (NativeWinImports.VirtualQuery(ApiSetMap, out var mbi0, (UIntPtr)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == UIntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualQuery failed");
IntPtr AllocationBase = mbi0.AllocationBase;
long TotalSize = 0;
IntPtr Cursor = AllocationBase;
while (true)
{
if (NativeWinImports.VirtualQuery(Cursor, out var mbi, (UIntPtr)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == UIntPtr.Zero)
break;
if (mbi.AllocationBase != AllocationBase)
break;
int RegionSize = (int)(long)mbi.RegionSize;
if (RegionSize <= 0)
break;
TotalSize += RegionSize;
Cursor = IntPtr.Add(mbi.BaseAddress, RegionSize);
}
if (TotalSize <= 0 || TotalSize > 128L * 1024 * 1024)
throw new InvalidOperationException("Invalid allocation size");
byte[] Dump = new byte[TotalSize];
Cursor = AllocationBase;
long Offset = 0;
while (Offset < TotalSize)
{
if (NativeWinImports.VirtualQuery(Cursor, out var mbi, (UIntPtr)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == UIntPtr.Zero)
break;
if (mbi.AllocationBase != AllocationBase)
break;
int RegionSize = (int)(long)mbi.RegionSize;
if (RegionSize <= 0)
break;
bool Readable = mbi.State == MEM_COMMIT_STATE &&
(mbi.Protect & PAGE_GUARD) == 0 &&
(mbi.Protect & PAGE_NOACCESS) == 0;
if (Readable)
Marshal.Copy(mbi.BaseAddress, Dump, (int)Offset, RegionSize);
Offset += RegionSize;
Cursor = IntPtr.Add(mbi.BaseAddress, RegionSize);
}
File.WriteAllBytes(BinaryEmulator.ApiSetMapPath, Dump);
return true;
}
}
catch
{
return false;
}
}
public static bool RunSilent(string FileName, string Arguments)
{
try
{
using var p = new Process();
p.StartInfo.FileName = FileName;
p.StartInfo.Arguments = Arguments;
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
p.Start();
p.WaitForExit();
return p.ExitCode == 0;
}
catch
{
return false;
}
}
public static bool VerifyRegDump(string RegDir, bool SilentFinalMsg)
{
string[] Files =
{
Path.Combine(RegDir, "SYSTEM"),
Path.Combine(RegDir, "SECURITY"),
Path.Combine(RegDir, "SOFTWARE"),
Path.Combine(RegDir, "HARDWARE"),
Path.Combine(RegDir, "SAM"),
};
bool Ok = true;
foreach (string FilePath in Files)
{
if (!File.Exists(FilePath))
{
// HARDWARE is volatile: the kernel builds it at boot, so installation media never carries one.
if (FilePath.EndsWith("HARDWARE", StringComparison.OrdinalIgnoreCase))
{
PrintHighlight("[!] No HARDWARE hive; programs that read HKLM\\HARDWARE will see an empty key.");
continue;
}
PrintHighlight($"[-] Missing registry file: {FilePath}");
Ok = false;
continue;
}
try
{
long Size = new FileInfo(FilePath).Length;
if (Size <= 0)
{
PrintHighlight($"[-] Empty registry file: {FilePath}");
Ok = false;
}
}
catch
{
PrintHighlight($"[-] Failed to validate registry file: {FilePath}");
Ok = false;
}
}
if (!SilentFinalMsg)
{
if (Ok)
PrintHighlight("[*] Registry dump is ready.");
else
PrintHighlight("[-] Registry dump is incomplete.");
}
return Ok;
}
public static bool DumpReg(string RegDir)
{
try
{
Directory.CreateDirectory(RegDir);
RunSilent("reg", $"save HKLM\\SYSTEM \"{Path.Combine(RegDir, "SYSTEM")}\" /y");
RunSilent("reg", $"save HKLM\\SECURITY \"{Path.Combine(RegDir, "SECURITY")}\" /y");
RunSilent("reg", $"save HKLM\\SOFTWARE \"{Path.Combine(RegDir, "SOFTWARE")}\" /y");
RunSilent("reg", $"save HKLM\\HARDWARE \"{Path.Combine(RegDir, "HARDWARE")}\" /y");
RunSilent("reg", $"save HKLM\\SAM \"{Path.Combine(RegDir, "SAM")}\" /y");
return true;
}
catch
{
return false;
}
}
public static bool RunAdminWait(string Arguments, out int ExitCode)
{
ExitCode = -1;
string ExePath = Environment.ProcessPath ?? string.Empty;
if (string.IsNullOrEmpty(ExePath))
return false;
ProcessStartInfo StartInfo = new ProcessStartInfo
{
FileName = ExePath,
Arguments = Arguments,
UseShellExecute = true,
Verb = "runas"
};
try
{
using Process p = Process.Start(StartInfo);
if (p == null)
return false;
p.WaitForExit();
ExitCode = p.ExitCode;
return true;
}
catch
{
return false;
}
}
/// <summary>
/// returns whether the current proccess is running as an admin.
/// </summary>
/// <returns>returns true if running as an admin, otherwise false.</returns>
public static bool IsAdmin()
{
if (!IsWindows)
return false;
#pragma warning disable
using WindowsIdentity Identity = WindowsIdentity.GetCurrent();
WindowsPrincipal Principal = new WindowsPrincipal(Identity);
return Principal.IsInRole(WindowsBuiltInRole.Administrator);
#pragma warning restore
}
internal static string QuoteCommandLineArg(string Arg)
{
if (Arg == null)
return "\"\"";
bool NeedsQuotes = Arg.Length == 0 || Arg.Any(Ch => char.IsWhiteSpace(Ch) || Ch == '"');
if (!NeedsQuotes)
return Arg;
StringBuilder Builder = new StringBuilder();
Builder.Append('"');
int Backslashes = 0;
foreach (char Ch in Arg)
{
if (Ch == '\\')
{
Backslashes++;
continue;
}
if (Ch == '"')
{
Builder.Append('\\', Backslashes * 2 + 1);
Builder.Append('"');
Backslashes = 0;
continue;
}
if (Backslashes != 0)
{
Builder.Append('\\', Backslashes);
Backslashes = 0;
}
Builder.Append(Ch);
}
if (Backslashes != 0)
Builder.Append('\\', Backslashes * 2);
Builder.Append('"');
return Builder.ToString();
}
private static string BuildCommandLine(string ExePath, string[] Args)
{
StringBuilder Builder = new StringBuilder();
Builder.Append(QuoteCommandLineArg(ExePath));
for (int i = 0; i < Args.Length; i++)
{
Builder.Append(' ');
Builder.Append(QuoteCommandLineArg(Args[i]));
}
return Builder.ToString();
}
/// <summary>
/// Restarts the current process with CFG disabled.
/// </summary>
/// <param name="KeepAlive">Keeps the parent alive/waiting till the process finishes.</param>
/// <returns>returns true if the process was restarted, otherwise false.</returns>
public static bool RestartProcessWithCfgDisabled(bool KeepAlive)
{
if (!IsWindows)
return false;
string ExePath = Environment.ProcessPath ?? string.Empty;
if (string.IsNullOrEmpty(ExePath))
return false;
string[] Args = Environment.GetCommandLineArgs().Skip(1).ToArray();
string CommandLine = BuildCommandLine(ExePath, Args);
const uint ExtendedStartupInfoPresent = 0x00080000;
const int ProcThreadAttributeMitigationPolicy = 0x00020007;
ulong MitigationPolicy = 0x00000002UL << 40;
ulong MitigationPolicy2 = 0x00000002UL << 8;
IntPtr AttributeListSize = IntPtr.Zero;
NativeWinImports.InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref AttributeListSize);
if (AttributeListSize == IntPtr.Zero)
return false;
IntPtr AttributeList = IntPtr.Zero;
IntPtr MitigationPtr = IntPtr.Zero;
bool AttributeListInitialized = false;
try
{
AttributeList = Marshal.AllocHGlobal(AttributeListSize);
if (!NativeWinImports.InitializeProcThreadAttributeList(AttributeList, 1, 0, ref AttributeListSize))
return false;
AttributeListInitialized = true;
STARTUPINFOEX StartupInfoEx = new STARTUPINFOEX();
StartupInfoEx.StartupInfo.cb = (uint)Marshal.SizeOf<STARTUPINFOEX>();
StartupInfoEx.lpAttributeList = AttributeList;
bool AttributeOk = false;
MitigationPtr = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(MitigationPtr, unchecked((long)MitigationPolicy));
Marshal.WriteInt64(IntPtr.Add(MitigationPtr, sizeof(ulong)), unchecked((long)MitigationPolicy2));
AttributeOk = NativeWinImports.UpdateProcThreadAttribute(AttributeList, 0, (IntPtr)ProcThreadAttributeMitigationPolicy, MitigationPtr, (IntPtr)(sizeof(ulong) * 2), IntPtr.Zero, IntPtr.Zero);
if (!AttributeOk)
{
Marshal.WriteInt64(MitigationPtr, unchecked((long)MitigationPolicy));
AttributeOk = NativeWinImports.UpdateProcThreadAttribute(AttributeList, 0, (IntPtr)ProcThreadAttributeMitigationPolicy, MitigationPtr, (IntPtr)sizeof(ulong), IntPtr.Zero, IntPtr.Zero);
}
if (!AttributeOk)
return false;
string PrevEnv = Environment.GetEnvironmentVariable("BROVAN_CFG_DISABLED");
Environment.SetEnvironmentVariable("BROVAN_CFG_DISABLED", "1");
try
{
PROCESS_INFORMATION ProcessInfo;
bool Created = NativeWinImports.CreateProcessW(null, new StringBuilder(CommandLine), IntPtr.Zero, IntPtr.Zero, false, ExtendedStartupInfoPresent, IntPtr.Zero, Environment.CurrentDirectory, ref StartupInfoEx, out ProcessInfo);
if (!Created)
{
if (PrevEnv == null)
Environment.SetEnvironmentVariable("BROVAN_CFG_DISABLED", null);
else
Environment.SetEnvironmentVariable("BROVAN_CFG_DISABLED", PrevEnv);
return false;
}
if (ProcessInfo.hThread != IntPtr.Zero)
NativeWinImports.CloseHandle(ProcessInfo.hThread);
if (KeepAlive)
{
NativeWinImports.WaitForSingleObject(ProcessInfo.hProcess, 0xFFFFFFFF);
}
if (ProcessInfo.hProcess != IntPtr.Zero)
NativeWinImports.CloseHandle(ProcessInfo.hProcess);
}
catch
{
if (PrevEnv == null)
Environment.SetEnvironmentVariable("BROVAN_CFG_DISABLED", null);
else
Environment.SetEnvironmentVariable("BROVAN_CFG_DISABLED", PrevEnv);
return false;
}
}
finally
{
if (AttributeList != IntPtr.Zero)
{
if (AttributeListInitialized)
{
try
{
NativeWinImports.DeleteProcThreadAttributeList(AttributeList);
}
catch
{
}
}
Marshal.FreeHGlobal(AttributeList);
}
if (MitigationPtr != IntPtr.Zero)
Marshal.FreeHGlobal(MitigationPtr);
}
Environment.Exit(0);
return true;
}
/// <summary>
/// Appends a single byte using the escaped guest-console representation.
/// </summary>
private static void AppendEscapedConsoleByte(StringBuilder Builder, byte Value)
{
switch (Value)
{
case 0x09:
Builder.Append('\t');
break;
case 0x0A:
Builder.Append('\n');
break;
case 0x0D:
Builder.Append("\\r");
break;
case 0x1B:
Builder.Append("\\x1B");
break;
case 0x08:
Builder.Append("\\b");
break;
case 0x07:
Builder.Append("\\a");
break;
default:
if (Value < 0x20 || Value == 0x7F)
Builder.Append("\\x").Append(Value.ToString("X2"));
else
Builder.Append((char)Value);
break;
}
}
/// <summary>
/// Appends a byte range using the escaped guest-console representation.
/// </summary>
private static void AppendEscapedConsoleBytes(StringBuilder Builder, ReadOnlySpan<byte> Data, int Start, int Length)
{
int End = Start + Length;
for (int i = Start; i < End; i++)
AppendEscapedConsoleByte(Builder, Data[i]);
}
/// <summary>
/// Checks whether a CSI sequence is safe to forward in light escaped console mode.
/// </summary>
private static bool IsSafeLightConsoleCsiSequence(ReadOnlySpan<byte> Data, int Start, int Length)
{
if (Length < 3 || Data[Start] != 0x1B || Data[Start + 1] != (byte)'[')
return false;
byte Final = Data[Start + Length - 1];
bool IsSgr = Final == (byte)'m';
bool IsCursorMotion =
Final == (byte)'A' || Final == (byte)'B' || Final == (byte)'C' || Final == (byte)'D' ||
Final == (byte)'E' || Final == (byte)'F' || Final == (byte)'G' || Final == (byte)'H' ||
Final == (byte)'f' || Final == (byte)'s' || Final == (byte)'u';
if (!IsSgr && !IsCursorMotion)
return false;
for (int i = Start + 2; i < Start + Length - 1; i++)
{
byte Value = Data[i];
if (IsSgr && Value == (byte)':')
continue;
if (!((Value >= (byte)'0' && Value <= (byte)'9') || Value == (byte)';'))
return false;
}
return true;
}
/// <summary>
/// Attempts to find the end of a guest-provided virtual terminal sequence.
/// </summary>
private static bool TryGetConsoleEscapeSequenceLength(ReadOnlySpan<byte> Data, int Start, out int Length)
{
Length = 1;
if (Start + 1 >= Data.Length)
return true;
byte Type = Data[Start + 1];
if (Type == (byte)'[')
{
for (int i = Start + 2; i < Data.Length; i++)
{
byte Value = Data[i];
if (Value >= 0x40 && Value <= 0x7E)
{
Length = i - Start + 1;
return true;
}
}
Length = Data.Length - Start;
return false;
}
if (Type == (byte)']' || Type == (byte)'P' || Type == (byte)'^' || Type == (byte)'_' || Type == (byte)'X')
{
for (int i = Start + 2; i < Data.Length; i++)
{
if (Data[i] == 0x07)
{
Length = i - Start + 1;
return true;
}
if (Data[i] == 0x1B && i + 1 < Data.Length && Data[i + 1] == (byte)'\\')
{
Length = i - Start + 2;
return true;
}
}
Length = Data.Length - Start;
return false;
}
Length = 2;
return true;
}
/// <summary>
/// Writes guest-controlled console bytes while allowing safe styling and cursor-positioning escape sequences.
/// </summary>
private static void WriteLightEscapedConsoleBytes(ReadOnlySpan<byte> Data, Stream Output)
{
StringBuilder Builder = new StringBuilder(Data.Length);
for (int i = 0; i < Data.Length; i++)
{
byte Value = Data[i];
if (Value == 0x0D)
{
Builder.Append('\r');
continue;
}
if (Value != 0x1B)
{
AppendEscapedConsoleByte(Builder, Value);
continue;
}
TryGetConsoleEscapeSequenceLength(Data, i, out int SequenceLength);
if (IsSafeLightConsoleCsiSequence(Data, i, SequenceLength))
Builder.Append(Encoding.ASCII.GetString(Data.Slice(i, SequenceLength)));
else
AppendEscapedConsoleBytes(Builder, Data, i, SequenceLength);
i += SequenceLength - 1;
}
byte[] Escaped = Encoding.UTF8.GetBytes(Builder.ToString());
Output.Write(Escaped, 0, Escaped.Length);
}
/// <summary>
/// Writes guest-controlled console bytes with terminal control characters escaped.
/// </summary>
private static void WriteEscapedConsoleBytes(ReadOnlySpan<byte> Data, Stream Output)
{
StringBuilder Builder = new StringBuilder(Data.Length);
for (int i = 0; i < Data.Length; i++)
AppendEscapedConsoleByte(Builder, Data[i]);
byte[] Escaped = Encoding.UTF8.GetBytes(Builder.ToString());
Output.Write(Escaped, 0, Escaped.Length);
}
private static Stream Stdout = null;
/// <summary>
/// Writes guest-controlled console output to the host console using the selected safety policy.
/// </summary>
public static void ConsoleWrite(ReadOnlySpan<byte> Data, Stream Output, GuestConsoleOutputMode Mode)
{
if (Data.Length == 0 || Output == null)
return;
switch (Mode)
{
case GuestConsoleOutputMode.Suppressed:
return;
case GuestConsoleOutputMode.Raw:
Output.Write(Data);
Output.Flush();
return;
case GuestConsoleOutputMode.LightEscaped:
WriteLightEscapedConsoleBytes(Data, Output);
Output.Flush();
return;
case GuestConsoleOutputMode.Escaped:
default:
WriteEscapedConsoleBytes(Data, Output);
Output.Flush();
return;
}
}
/// <summary>
/// Writes guest-controlled console output to the host console using the selected safety policy.
/// </summary>
public static void ConsoleWrite(byte[] Data, Stream Output, GuestConsoleOutputMode Mode)
{
if (Data == null)
return;
ConsoleWrite(Data.AsSpan(), Output, Mode);
}
/// <summary>
/// Writes guest-controlled console output to the host standard output stream using the configured safety policy.
/// </summary>
public static void ConsoleWrite(byte[] Data, GuestConsoleOutputMode Mode)
{
if (Data == null)
return;
if (Stdout == null)
Stdout = Console.OpenStandardOutput();
ConsoleWrite(Data.AsSpan(), Stdout, Mode);
}
/// <summary>
/// Writes guest-controlled console output to the host standard output stream using the configured safety policy.
/// </summary>
public static void ConsoleWrite(ReadOnlySpan<byte> Data, GuestConsoleOutputMode Mode)
{
if (Stdout == null)
Stdout = Console.OpenStandardOutput();
ConsoleWrite(Data, Stdout, Mode);
}
/// <summary>
/// Cross-platform IO helper used to translate emulated paths into host paths.
/// </summary>
public static class IO
{
private static readonly object WindowsLibsIndexLock = new();
private static Dictionary<string, string> WindowsLibsFileIndex;
private static readonly object DriveMapLock = new();
private static readonly Dictionary<char, string> DriveMappings = new();
private static readonly HashSet<string> AllowedRoots = new(IsWindows ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
private static readonly Dictionary<string, string> LinuxMountMappings = new(StringComparer.Ordinal);
private static readonly Dictionary<string, string> WindowsMountMappings = new(StringComparer.OrdinalIgnoreCase);
public static string VirtualFileSystemRoot = Path.Combine(AppContext.BaseDirectory, "VirtualFS");
public static string LinuxVirtualFileSystemRoot = Path.Combine(VirtualFileSystemRoot, "Linux");
public static string LinuxCurrentDirectory = "/";
public static string DefaultDriveLetter = "C";
public static bool Wow64FileRedirect;
private const string System32WindowsPath = "C:\\Windows\\System32";
private const string SysWow64WindowsPath = "C:\\Windows\\SysWOW64";
private const string SysnativeWindowsPath = "C:\\Windows\\Sysnative";
private static readonly string[] Wow64RedirectExclusions =
{
"catroot", "catroot2", "driverstore", "drivers\\etc", "logfiles", "spool"
};
private static readonly char[] WindowsPathSeparators = { '\\', '/' };
/// <summary>
/// Initializes the default sandbox drive mappings and allowed IO roots.
/// </summary>
static IO()
{
// Keep the emulator sandboxed by default.
EnsureDriveMapping('C', Path.Combine(VirtualFileSystemRoot, "C"));
EnsureDriveMapping('E', Path.Combine(VirtualFileSystemRoot, "E"));
EnsureLinuxMountMapping("/", LinuxVirtualFileSystemRoot);
try
{
Directory.CreateDirectory(VirtualFileSystemRoot);
Directory.CreateDirectory(LinuxVirtualFileSystemRoot);
EnsureLinuxBaseFilesystem(LinuxVirtualFileSystemRoot);
}
catch (Exception ex)
{
Utils.LogError($"[IO] Failed to create the Virtual File System: {ex.Message}");
}
RefreshAllowedRoots();
}
/// <summary>
/// Creates a small Linux root filesystem skeleton for programs that inspect common system files.
/// Existing files are left untouched.
/// </summary>
/// <param name="Root">host directory mapped to the emulated Linux root.</param>
private static void EnsureLinuxBaseFilesystem(string Root)
{
if (string.IsNullOrWhiteSpace(Root))
return;
string[] Directories =
{
"bin",
"boot",
"dev",
"etc",
"home",
"home/brovan",
"lib",
"lib64",
"media",
"mnt",
"opt",
"proc",
"root",
"run",
"sbin",
"srv",
"sys",
"tmp",
"usr",
"usr/bin",
"usr/lib",
"usr/lib64",
"usr/local",
"usr/local/bin",
"usr/sbin",
"var",
"var/cache",
"var/lib",
"var/log",
"var/run",
"var/tmp"
};
foreach (string DirectoryPath in Directories)
CreateLinuxDirectoryIfMissing(Root, DirectoryPath);
WriteLinuxTextFileIfMissing(Root, "etc/passwd",
"root:x:0:0:root:/root:/bin/sh\n" +
"brovan:x:1000:1000:Brovan User:/home/brovan:/bin/sh\n" +
"nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n");
WriteLinuxTextFileIfMissing(Root, "etc/group",
"root:x:0:\n" +
"brovan:x:1000:brovan\n" +
"nogroup:x:65534:\n");
WriteLinuxTextFileIfMissing(Root, "etc/shadow",
"root:*:19700:0:99999:7:::\n" +
"brovan:*:19700:0:99999:7:::\n" +
"nobody:*:19700:0:99999:7:::\n");
WriteLinuxTextFileIfMissing(Root, "etc/gshadow",
"root:*::\n" +
"brovan:*::brovan\n" +
"nogroup:*::\n");
WriteLinuxTextFileIfMissing(Root, "etc/hostname", "brovan\n");
WriteLinuxTextFileIfMissing(Root, "etc/hosts",
"127.0.0.1\tlocalhost\n" +
"127.0.1.1\tbrovan\n" +
"::1\tlocalhost ip6-localhost ip6-loopback\n" +
"ff02::1\tip6-allnodes\n" +
"ff02::2\tip6-allrouters\n");
WriteLinuxTextFileIfMissing(Root, "etc/resolv.conf",
"nameserver 1.1.1.1\n" +
"nameserver 8.8.8.8\n" +
"options edns0 trust-ad\n");
WriteLinuxTextFileIfMissing(Root, "etc/nsswitch.conf",
"passwd: files\n" +
"group: files\n" +
"shadow: files\n" +
"hosts: files dns\n" +
"networks: files\n" +
"protocols: files\n" +
"services: files\n" +
"ethers: files\n" +
"rpc: files\n");
string OsRelease =
"NAME=\"Brovan Linux\"\n" +
"PRETTY_NAME=\"Brovan Linux\"\n" +
"ID=brovan\n" +
"VERSION_ID=\"1.0\"\n" +
"HOME_URL=\"https://github.com/AdvDebug/Brovan\"\n" +
"SUPPORT_URL=\"https://github.com/AdvDebug/Brovan\"\n" +
"BUG_REPORT_URL=\"https://github.com/AdvDebug/Brovan\"\n";
WriteLinuxTextFileIfMissing(Root, "etc/os-release", OsRelease);
WriteLinuxTextFileIfMissing(Root, "usr/lib/os-release", OsRelease);
WriteLinuxTextFileIfMissing(Root, "etc/issue", "Brovan Linux \\n \\l\n");
WriteLinuxTextFileIfMissing(Root, "etc/locale.conf", "LANG=C.UTF-8\n");
WriteLinuxTextFileIfMissing(Root, "etc/profile", "export PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin\n");
WriteLinuxTextFileIfMissing(Root, "etc/shells", "/bin/sh\n/bin/bash\n/usr/bin/sh\n/usr/bin/bash\n");
WriteLinuxTextFileIfMissing(Root, "etc/fstab", "# <file system> <mount point> <type> <options> <dump> <pass>\n");
WriteLinuxTextFileIfMissing(Root, "etc/mtab", "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0\nsysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0\n");
WriteLinuxTextFileIfMissing(Root, "etc/machine-id", CreateStableMachineId(Root) + "\n");
WriteLinuxTextFileIfMissing(Root, "etc/protocols",
"ip 0 IP\n" +
"icmp 1 ICMP\n" +
"tcp 6 TCP\n" +
"udp 17 UDP\n" +
"ipv6 41 IPv6\n" +
"raw 255 RAW\n");
WriteLinuxTextFileIfMissing(Root, "etc/services",
"tcpmux 1/tcp\n" +
"echo 7/tcp\n" +
"echo 7/udp\n" +
"domain 53/tcp\n" +
"domain 53/udp\n" +
"http 80/tcp www\n" +
"https 443/tcp\n");
}
/// <summary>
/// Creates a Linux VFS directory if it is missing.
/// </summary>
/// <param name="Root">host directory mapped to the emulated Linux root.</param>
/// <param name="RelativePath">linux-style relative directory path.</param>
private static void CreateLinuxDirectoryIfMissing(string Root, string RelativePath)
{
string HostPath = CombineLinuxRelativePath(Root, RelativePath);
if (string.IsNullOrWhiteSpace(HostPath))
return;
Directory.CreateDirectory(HostPath);
}
/// <summary>
/// Writes a Linux VFS text file when no file is already present.
/// </summary>
/// <param name="Root">host directory mapped to the emulated Linux root.</param>
/// <param name="RelativePath">linux-style relative file path.</param>
/// <param name="Content">file content to write.</param>
private static void WriteLinuxTextFileIfMissing(string Root, string RelativePath, string Content)
{
string HostPath = CombineLinuxRelativePath(Root, RelativePath);
if (string.IsNullOrWhiteSpace(HostPath) || File.Exists(HostPath) || Directory.Exists(HostPath))
return;
string Parent = Path.GetDirectoryName(HostPath);
if (!string.IsNullOrWhiteSpace(Parent))
Directory.CreateDirectory(Parent);
File.WriteAllText(HostPath, Content, new UTF8Encoding(false));
}
private sealed class UbuntuRootfsPendingLink
{
public string EntryPath;
public string LinkName;
public TarEntryType EntryType;
public UnixFileMode Mode;
public DateTimeOffset ModificationTime;
}
private static string GetUbuntuBaseRootfsUrl(BinaryArchitecture Architecture)
{
return Architecture switch
{
BinaryArchitecture.x64 => "https://cdimage.ubuntu.com/ubuntu-base/releases/26.04/release/ubuntu-base-26.04-base-amd64.tar.gz",
_ => null,
};
}
private static bool IsUbuntuBaseRootfsInstalled()
{
string[] RequiredFiles =
{
Path.Combine(LinuxVirtualFileSystemRoot, "lib64", "ld-linux-x86-64.so.2"),
Path.Combine(LinuxVirtualFileSystemRoot, "bin", "bash"),
Path.Combine(LinuxVirtualFileSystemRoot, "usr", "bin", "bash"),
};
for (int i = 0; i < RequiredFiles.Length; i++)
{
if (File.Exists(RequiredFiles[i]))
return true;
}
return false;
}
public static bool EnsureUbuntuBaseRootfs(BinaryArchitecture Architecture)
{
if (IsUbuntuBaseRootfsInstalled())
return true;
string RootfsUrl = GetUbuntuBaseRootfsUrl(Architecture);
if (string.IsNullOrWhiteSpace(RootfsUrl))
{
Utils.LogError($"[IO] Ubuntu Base rootfs is not available for architecture '{Architecture}'.");
return false;
}
try
{
Directory.CreateDirectory(LinuxVirtualFileSystemRoot);
PrintHighlight("[+] Downloading Ubuntu Base rootfs...", true);
using HttpClient Client = new HttpClient();
Client.Timeout = TimeSpan.FromMinutes(30);
using HttpResponseMessage Response = Client.GetAsync(RootfsUrl, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult();
Response.EnsureSuccessStatusCode();
using Stream ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
using GZipStream GzipStream = new GZipStream(ResponseStream, CompressionMode.Decompress, leaveOpen: false);
using TarReader Reader = new TarReader(GzipStream);
List<UbuntuRootfsPendingLink> PendingLinks = new();
Dictionary<string, UbuntuRootfsPendingLink> PendingLinksByArchivePath = new(StringComparer.Ordinal);
TarEntry Entry;
while ((Entry = Reader.GetNextEntry()) != null)
{
string NormalizedEntryPath = NormalizeUbuntuRootfsArchivePath(Entry.Name);
if (string.IsNullOrWhiteSpace(NormalizedEntryPath))
continue;
string HostPath = ResolveUbuntuRootfsHostPath(NormalizedEntryPath);
if (string.IsNullOrWhiteSpace(HostPath))
continue;
if (Entry.EntryType == TarEntryType.Directory)
{
DeletePathIfExists(HostPath);
Directory.CreateDirectory(HostPath);
ApplyTarMetadata(HostPath, Entry, true);
continue;
}
if (Entry.EntryType == TarEntryType.SymbolicLink || Entry.EntryType == TarEntryType.HardLink)
{
UbuntuRootfsPendingLink PendingLink = new UbuntuRootfsPendingLink
{
EntryPath = HostPath,
LinkName = Entry.LinkName,
EntryType = Entry.EntryType,
Mode = Entry.Mode,
ModificationTime = Entry.ModificationTime,
};
PendingLinks.Add(PendingLink);
PendingLinksByArchivePath[NormalizedEntryPath] = PendingLink;
continue;
}
if (Entry.EntryType != TarEntryType.RegularFile)
continue;
if (Entry.DataStream == null)
continue;
string Parent = Path.GetDirectoryName(HostPath);
if (!string.IsNullOrWhiteSpace(Parent))
Directory.CreateDirectory(Parent);
DeletePathIfExists(HostPath);
using (FileStream Output = new FileStream(HostPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
Entry.DataStream.CopyTo(Output);
}
ApplyTarMetadata(HostPath, Entry, false);
}
List<UbuntuRootfsPendingLink> PendingHardLinks = PendingLinks.Where(Link => Link.EntryType == TarEntryType.HardLink).ToList();
while (PendingHardLinks.Count > 0)
{
bool Progress = false;
List<UbuntuRootfsPendingLink> Remaining = new();
foreach (UbuntuRootfsPendingLink Link in PendingHardLinks)
{
if (TryMaterializeRootfsHardLink(Link, PendingLinksByArchivePath))
{
Progress = true;
continue;
}
Remaining.Add(Link);
}
if (!Progress)
{
PendingHardLinks = Remaining;
break;
}
PendingHardLinks = Remaining;
}
if (PendingHardLinks.Count > 0)
Utils.LogError($"[IO] {PendingHardLinks.Count} rootfs hardlink(s) could not be resolved after retries.");
List<UbuntuRootfsPendingLink> PendingSymlinks = PendingLinks.Where(Link => Link.EntryType == TarEntryType.SymbolicLink).ToList();
while (PendingSymlinks.Count > 0)
{
bool Progress = false;
List<UbuntuRootfsPendingLink> Remaining = new();
foreach (UbuntuRootfsPendingLink Link in PendingSymlinks)
{
if (TryMaterializeRootfsSymlink(Link, PendingLinksByArchivePath))
{
Progress = true;
continue;
}
Remaining.Add(Link);
}
if (!Progress)
{
PendingSymlinks = Remaining;
break;
}
PendingSymlinks = Remaining;
}
if (PendingSymlinks.Count > 0)
Utils.LogError($"[IO] {PendingSymlinks.Count} rootfs symlink(s) could not be resolved after retries.");
if (!IsUbuntuBaseRootfsInstalled())
Utils.LogError("[IO] Ubuntu Base rootfs download completed, but the expected interpreter file was still not found.");
return IsUbuntuBaseRootfsInstalled();
}
catch (Exception ex)
{
Utils.LogError($"[IO] Failed to download or extract Ubuntu Base rootfs: {ex.Message}");
return false;
}
}
private static bool CreateHardLinkPortable(string LinkPath, string ExistingPath)
{
try
{
if (IsWindows)
return NativeWinImports.CreateHardLinkW(LinkPath, ExistingPath, IntPtr.Zero);
return NativeUnixImports.Link(ExistingPath, LinkPath) == 0;
}
catch (Exception ex)
{
Utils.LogError($"[IO] Failed to create hardlink '{LinkPath}' -> '{ExistingPath}': {ex.Message}");
return false;
}
}
private static bool CreateSymbolicLinkPortable(string LinkPath, string TargetPath, bool IsDirectory)
{
try
{
if (IsWindows)
{
const int SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1;
const int SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2;
int Flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
if (IsDirectory)
Flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
return NativeWinImports.CreateSymbolicLinkW(LinkPath, TargetPath, Flags);
}
return NativeUnixImports.Symlink(TargetPath, LinkPath) == 0;
}
catch (Exception ex)
{
Utils.LogError($"[IO] Failed to create symlink '{LinkPath}' -> '{TargetPath}': {ex.Message}");
return false;
}
}
private static void ApplyTarMetadata(string TargetPath, TarEntry Entry, bool IsDirectory)
{
if (Entry == null)
return;
ApplyTarMetadata(TargetPath, Entry.Mode, Entry.ModificationTime, IsDirectory);
}
private static void ApplyTarMetadata(string TargetPath, UnixFileMode Mode, DateTimeOffset ModificationTime, bool IsDirectory)
{
if (string.IsNullOrWhiteSpace(TargetPath))
return;
try
{
if (ModificationTime != default)
File.SetLastWriteTimeUtc(TargetPath, ModificationTime.UtcDateTime);
}
catch
{
}
try
{
FileSystemInfo Info = IsDirectory ? new DirectoryInfo(TargetPath) : new FileInfo(TargetPath);
Info.UnixFileMode = Mode;
}
catch
{
}
}
private static bool TryMaterializeRootfsHardLink(UbuntuRootfsPendingLink Link, IReadOnlyDictionary<string, UbuntuRootfsPendingLink> PendingLinksByArchivePath)
{
if (Link == null || string.IsNullOrWhiteSpace(Link.EntryPath) || string.IsNullOrWhiteSpace(Link.LinkName))
return false;
if (!TryResolveUbuntuRootfsArchivePath(Link.LinkName, PendingLinksByArchivePath, out string ResolvedArchivePath))
{
Utils.LogError($"[IO] Failed to resolve hardlink target '{Link.LinkName}' for '{Link.EntryPath}'.");
return false;
}
string HardLinkTarget = ResolveUbuntuRootfsHostPath(ResolvedArchivePath);
if (string.IsNullOrWhiteSpace(HardLinkTarget) || !File.Exists(HardLinkTarget))
{
Utils.LogError($"[IO] Failed to resolve hardlink target '{Link.LinkName}' for '{Link.EntryPath}'.");
return false;
}
string HardLinkParent = Path.GetDirectoryName(Link.EntryPath);
if (!string.IsNullOrWhiteSpace(HardLinkParent))
Directory.CreateDirectory(HardLinkParent);
DeletePathIfExists(Link.EntryPath);
if (!CreateHardLinkPortable(Link.EntryPath, HardLinkTarget))
{
Utils.LogError($"[IO] Failed to create hardlink '{Link.EntryPath}' -> '{Link.LinkName}'.");
return false;
}
ApplyTarMetadata(Link.EntryPath, Link.Mode, Link.ModificationTime, false);
return true;
}
private static bool TryMaterializeRootfsSymlink(UbuntuRootfsPendingLink Link, IReadOnlyDictionary<string, UbuntuRootfsPendingLink> PendingLinksByArchivePath)
{
string ResolvedTarget = ResolveUbuntuRootfsLinkTargetHostPath(Link.EntryPath, Link.LinkName, PendingLinksByArchivePath);
if (string.IsNullOrWhiteSpace(ResolvedTarget) || (!File.Exists(ResolvedTarget) && !Directory.Exists(ResolvedTarget)))
{
Utils.LogError($"[IO] Failed to resolve symlink target '{Link.LinkName}' for '{Link.EntryPath}'.");
return false;
}
string Parent = Path.GetDirectoryName(Link.EntryPath);
if (!string.IsNullOrWhiteSpace(Parent))
Directory.CreateDirectory(Parent);
string EntryFullPath = Path.GetFullPath(Link.EntryPath);
string TargetFullPath = Path.GetFullPath(ResolvedTarget);
StringComparison Comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (string.Equals(TargetFullPath, EntryFullPath, Comparison))
{
bool IsDirectory = Directory.Exists(ResolvedTarget) && !File.Exists(ResolvedTarget);
ApplyTarMetadata(Link.EntryPath, Link.Mode, Link.ModificationTime, IsDirectory);
return true;
}
bool EntryIsDirectory = Directory.Exists(Link.EntryPath) && !File.Exists(Link.EntryPath);
bool TargetIsDirectory = Directory.Exists(ResolvedTarget) && !File.Exists(ResolvedTarget);
if (EntryIsDirectory && TargetIsDirectory)
{
if (ArePathsNested(EntryFullPath, TargetFullPath))
{
Utils.LogError($"[IO] Refusing to flatten recursive directory symlink '{Link.EntryPath}' -> '{Link.LinkName}'.");
return false;
}
if (!TryCopyDirectoryRecursive(Link.EntryPath, ResolvedTarget, Link.Mode, Link.ModificationTime))
{
Utils.LogError($"[IO] Failed to merge existing directory '{Link.EntryPath}' into symlink target '{ResolvedTarget}'.");
return false;
}
}
DeletePathIfExists(Link.EntryPath);
if (TargetIsDirectory)
{
if (!TryCopyDirectoryRecursive(ResolvedTarget, Link.EntryPath, Link.Mode, Link.ModificationTime))
{
Utils.LogError($"[IO] Failed to flatten symlink directory '{Link.EntryPath}' -> '{Link.LinkName}'.");
return false;
}
return true;
}
try
{
File.Copy(ResolvedTarget, Link.EntryPath, true);
ApplyTarMetadata(Link.EntryPath, Link.Mode, Link.ModificationTime, false);
return true;
}
catch (Exception ex)
{
Utils.LogError($"[IO] Failed to flatten symlink '{Link.EntryPath}' -> '{Link.LinkName}': {ex.Message}");
return false;
}
}
private static bool ArePathsNested(string FirstPath, string SecondPath)
{
if (string.IsNullOrWhiteSpace(FirstPath) || string.IsNullOrWhiteSpace(SecondPath))
return false;
string FirstFullPath = Path.GetFullPath(FirstPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string SecondFullPath = Path.GetFullPath(SecondPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
StringComparison Comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (string.Equals(FirstFullPath, SecondFullPath, Comparison))
return true;
return SecondFullPath.StartsWith(FirstFullPath + Path.DirectorySeparatorChar, Comparison) ||
SecondFullPath.StartsWith(FirstFullPath + Path.AltDirectorySeparatorChar, Comparison) ||
FirstFullPath.StartsWith(SecondFullPath + Path.DirectorySeparatorChar, Comparison) ||
FirstFullPath.StartsWith(SecondFullPath + Path.AltDirectorySeparatorChar, Comparison);
}
private static bool TryCopyDirectoryRecursive(string SourceDirectory, string DestinationDirectory, UnixFileMode Mode, DateTimeOffset ModificationTime)
{
if (string.IsNullOrWhiteSpace(SourceDirectory) || string.IsNullOrWhiteSpace(DestinationDirectory))
return false;
try
{
string SourceFullPath = Path.GetFullPath(SourceDirectory);
string DestinationFullPath = Path.GetFullPath(DestinationDirectory);
StringComparison Comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (string.Equals(SourceFullPath, DestinationFullPath, Comparison))
{
Directory.CreateDirectory(DestinationFullPath);
ApplyTarMetadata(DestinationFullPath, Mode, ModificationTime, true);
return true;
}
Directory.CreateDirectory(DestinationFullPath);
foreach (string DirectoryPath in Directory.EnumerateDirectories(SourceFullPath, "*", SearchOption.AllDirectories))
{
string RelativePath = Path.GetRelativePath(SourceFullPath, DirectoryPath);
string DestinationPath = Path.Combine(DestinationFullPath, RelativePath);
Directory.CreateDirectory(DestinationPath);
try
{
DirectoryInfo SourceInfo = new DirectoryInfo(DirectoryPath);
DirectoryInfo DestinationInfo = new DirectoryInfo(DestinationPath);
if (SourceInfo.Exists)
{
if (SourceInfo.LastWriteTimeUtc != default)
Directory.SetLastWriteTimeUtc(DestinationPath, SourceInfo.LastWriteTimeUtc);
DestinationInfo.UnixFileMode = SourceInfo.UnixFileMode;
}
}
catch
{
}
}
foreach (string FilePath in Directory.EnumerateFiles(SourceFullPath, "*", SearchOption.AllDirectories))
{
string RelativePath = Path.GetRelativePath(SourceFullPath, FilePath);
string DestinationPath = Path.Combine(DestinationFullPath, RelativePath);
string DestinationParent = Path.GetDirectoryName(DestinationPath);
if (!string.IsNullOrWhiteSpace(DestinationParent))
Directory.CreateDirectory(DestinationParent);
File.Copy(FilePath, DestinationPath, true);
try
{
FileInfo SourceInfo = new FileInfo(FilePath);
FileInfo DestinationInfo = new FileInfo(DestinationPath);
if (SourceInfo.Exists)
{
if (SourceInfo.LastWriteTimeUtc != default)
File.SetLastWriteTimeUtc(DestinationPath, SourceInfo.LastWriteTimeUtc);
DestinationInfo.UnixFileMode = SourceInfo.UnixFileMode;
}
}
catch
{
}
}
ApplyTarMetadata(DestinationFullPath, Mode, ModificationTime, true);
return true;
}
catch (Exception ex)
{
Utils.LogError($"[IO] Failed to copy directory '{SourceDirectory}' to '{DestinationDirectory}': {ex.Message}");
return false;
}
}
private static string ResolveUbuntuRootfsArchiveLinkTarget(string LinkEntryPath, string LinkName)
{
if (string.IsNullOrWhiteSpace(LinkEntryPath) || string.IsNullOrWhiteSpace(LinkName))
return null;
string NormalizedLinkName = LinkName.Replace('\\', '/').Trim();
if (NormalizedLinkName.Length == 0)
return null;
string RelativeEntryPath = NormalizeUbuntuRootfsArchivePath(LinkEntryPath);
if (RelativeEntryPath == null)
return null;
string CombinedPath = NormalizedLinkName.StartsWith("/", StringComparison.Ordinal)
? NormalizedLinkName
: Path.Combine(Path.GetDirectoryName(RelativeEntryPath) ?? string.Empty, NormalizedLinkName).Replace('\\', '/');
return NormalizeUbuntuRootfsArchivePath(CombinedPath);
}
private static bool TryResolveUbuntuRootfsArchivePath(string PathValue, IReadOnlyDictionary<string, UbuntuRootfsPendingLink> PendingLinksByArchivePath, out string ResolvedPath)
{
ResolvedPath = NormalizeUbuntuRootfsArchivePath(PathValue);
if (ResolvedPath == null)
return false;
if (ResolvedPath.Length == 0)
return true;
HashSet<string> VisitedPaths = new(StringComparer.Ordinal);
while (true)
{
if (!VisitedPaths.Add(ResolvedPath))
return false;
string[] Parts = ResolvedPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
bool AppliedLink = false;
for (int PrefixLength = Parts.Length; PrefixLength >= 1; PrefixLength--)
{
string Prefix = string.Join('/', Parts.Take(PrefixLength));
if (!PendingLinksByArchivePath.TryGetValue(Prefix, out UbuntuRootfsPendingLink PendingLink) || PendingLink.EntryType != TarEntryType.SymbolicLink)
continue;
string ResolvedPrefix = ResolveUbuntuRootfsArchiveLinkTarget(Prefix, PendingLink.LinkName);
if (string.IsNullOrWhiteSpace(ResolvedPrefix))
return false;
if (PrefixLength == Parts.Length)
ResolvedPath = ResolvedPrefix;
else
{
string Suffix = string.Join('/', Parts.Skip(PrefixLength));
ResolvedPath = string.IsNullOrWhiteSpace(Suffix) ? ResolvedPrefix : $"{ResolvedPrefix}/{Suffix}";
ResolvedPath = NormalizeUbuntuRootfsArchivePath(ResolvedPath);
if (ResolvedPath == null)
return false;
}
AppliedLink = true;
break;
}
if (!AppliedLink)
return true;
}
}
private static string ResolveUbuntuRootfsLinkTargetHostPath(string LinkEntryPath, string LinkName, IReadOnlyDictionary<string, UbuntuRootfsPendingLink> PendingLinksByArchivePath)
{
string ResolvedArchiveTarget = ResolveUbuntuRootfsArchiveLinkTarget(LinkEntryPath, LinkName);
if (ResolvedArchiveTarget == null)
return null;
if (!TryResolveUbuntuRootfsArchivePath(ResolvedArchiveTarget, PendingLinksByArchivePath, out string FullyResolvedArchivePath))
return null;
return ResolveUbuntuRootfsHostPath(FullyResolvedArchivePath);
}
private static string NormalizeUbuntuRootfsArchivePath(string PathValue)
{
if (string.IsNullOrWhiteSpace(PathValue))
return null;
string Normalized = PathValue.Replace('\\', '/').Trim();
if (Normalized.Length == 0)
return null;
while (Normalized.StartsWith("./", StringComparison.Ordinal))
Normalized = Normalized.Substring(2);
while (Normalized.StartsWith("/", StringComparison.Ordinal))
Normalized = Normalized.Substring(1);
if (Normalized.Length == 0)
return string.Empty;
List<string> Parts = new();
foreach (string Part in Normalized.Split('/', StringSplitOptions.RemoveEmptyEntries))
{
if (Part == ".")
continue;
if (Part == "..")
{
if (Parts.Count == 0)
return null;
Parts.RemoveAt(Parts.Count - 1);
continue;
}
Parts.Add(Part);
}
if (Parts.Count == 0)
return string.Empty;
return string.Join('/', Parts);
}
private static string ResolveUbuntuRootfsHostPath(string RelativePath)
{
string Normalized = NormalizeUbuntuRootfsArchivePath(RelativePath);
if (Normalized == null)
return null;
string Root = Path.GetFullPath(LinuxVirtualFileSystemRoot);
string Combined = string.IsNullOrEmpty(Normalized)
? Root
: Path.GetFullPath(Path.Combine(Root, Normalized.Replace('/', Path.DirectorySeparatorChar)));
StringComparison Comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
string RootPrefix = Root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (!(string.Equals(Combined, RootPrefix, Comparison) || Combined.StartsWith(RootPrefix + Path.DirectorySeparatorChar, Comparison)))
return null;
return Combined;
}
private static void DeletePathIfExists(string PathValue)
{
if (string.IsNullOrWhiteSpace(PathValue))
return;
try
{
if (File.Exists(PathValue))
{
File.Delete(PathValue);
return;
}
if (Directory.Exists(PathValue))
{
try
{
FileAttributes Attributes = File.GetAttributes(PathValue);
if ((Attributes & FileAttributes.ReparsePoint) != 0)
{
Directory.Delete(PathValue);
return;
}
}
catch
{
}
Directory.Delete(PathValue, true);
}
}
catch
{
}
}
/// <summary>
/// Creates a deterministic machine-id for the Linux VFS root.
/// </summary>
/// <param name="Root">host directory mapped to the emulated Linux root.</param>
/// <returns>returns a 32-character lowercase hexadecimal machine-id.</returns>
private static string CreateStableMachineId(string Root)
{
byte[] Hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(Path.GetFullPath(Root)));
StringBuilder Builder = new();
for (int i = 0; i < 16; i++)
Builder.Append(Hash[i].ToString("x2"));
return Builder.ToString();
}
/// <summary>
/// Sets a host directory mapping for an emulated drive letter.
/// </summary>
/// <param name="DriveLetter">drive letter to map (for example 'C').</param>
/// <param name="HostRoot">host directory root for the drive.</param>
public static void SetDriveMapping(char DriveLetter, string HostRoot)
{
EnsureDriveMapping(DriveLetter, HostRoot);
RefreshAllowedRoots();
}
/// <summary>
/// Sets a host directory mapping for an emulated linux mount point.
/// </summary>
/// <param name="MountPoint">linux mount point to map (for example "/" or "/tmp").</param>
/// <param name="HostRoot">host directory root for the mount point.</param>
public static void SetLinuxMountMapping(string MountPoint, string HostRoot)
{
EnsureLinuxMountMapping(MountPoint, HostRoot);
RefreshAllowedRoots();
}
/// <summary>
/// Sets a host directory mapping for an emulated windows directory.
/// </summary>
/// <param name="GuestDirectory">absolute emulated windows directory to map (for example "C:\Users\User\Game").</param>
/// <param name="HostRoot">host directory root for the emulated directory.</param>
/// <returns>returns true when the mapping was registered.</returns>
public static bool SetWindowsMountMapping(string GuestDirectory, string HostRoot)
{
if (!EnsureWindowsMountMapping(GuestDirectory, HostRoot))
return false;
RefreshAllowedRoots();
return true;
}
/// <summary>
/// Mounts the host directory that holds an emulated windows program so the guest reaches it through a
/// windows path instead of a host path.
/// </summary>
/// <param name="HostImagePath">host path of the program image.</param>
/// <param name="GuestParentDirectory">emulated windows directory the program directory is mounted under.</param>
/// <returns>returns the emulated windows path of the program image.</returns>
/// <remarks>
/// A host that is already a windows machine hands out paths the guest can use as-is, so nothing is mounted there.
/// </remarks>
public static string MountWindowsProgramDirectory(string HostImagePath, string GuestParentDirectory)
{
if (string.IsNullOrWhiteSpace(HostImagePath) || IsWindowsRootedPath(HostImagePath))
return HostImagePath;
string HostDirectory;
try
{
HostDirectory = Path.GetDirectoryName(Path.GetFullPath(HostImagePath));
}
catch
{
return HostImagePath;
}
if (string.IsNullOrEmpty(HostDirectory))
return HostImagePath;
string Leaf = SanitizeWindowsPathComponent(Path.GetFileName(HostDirectory));
string GuestDirectory = CanonicalizeWindowsAbsolutePath(GuestParentDirectory);
if (string.IsNullOrEmpty(GuestDirectory))
return HostImagePath;
GuestDirectory = CanonicalizeWindowsAbsolutePath(GuestDirectory.TrimEnd('\\') + "\\" + Leaf);
if (string.IsNullOrEmpty(GuestDirectory) || !SetWindowsMountMapping(GuestDirectory, HostDirectory))
return HostImagePath;
return GuestDirectory + "\\" + Path.GetFileName(HostImagePath);
}
/// <summary>
/// Translates a host path into the emulated windows path it is reachable through.
/// </summary>
/// <param name="HostPath">host path to translate.</param>
/// <returns>returns the emulated windows path, or null when the host path is not mounted.</returns>
public static string ToGuestWindowsPath(string HostPath)
{
if (string.IsNullOrWhiteSpace(HostPath))
return null;
if (IsWindowsRootedPath(HostPath))
return HostPath;
string Full;
try
{
Full = ResolveSandboxLinks(Path.GetFullPath(HostPath), IncludeFinal: true, EnforceAllowedRoots: false);
}
catch
{
return null;
}
if (string.IsNullOrEmpty(Full))
return null;
StringComparison Comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
lock (DriveMapLock)
{
foreach (KeyValuePair<string, string> Pair in WindowsMountMappings)
{
string Root = Pair.Value.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (string.Equals(Full, Root, Comparison))
return Pair.Key;
if (!Full.StartsWith(Root + Path.DirectorySeparatorChar, Comparison))
continue;
string Relative = Full.Substring(Root.Length + 1).Replace(Path.DirectorySeparatorChar, '\\');
return Pair.Key + "\\" + Relative;
}
}
return null;
}
/// <summary>
/// Returns the file name of an emulated windows path.
/// </summary>
/// <param name="WinPath">emulated windows path.</param>
/// <returns>returns the trailing path component.</returns>
/// <remarks>
/// <see cref="Path.GetFileName(string)"/> only splits on the host separator, so it returns the whole
/// backslash path unchanged on a linux host.
/// </remarks>
public static string GetWindowsFileName(string WinPath)
{
if (string.IsNullOrEmpty(WinPath))
return WinPath;
int Separator = WinPath.LastIndexOfAny(WindowsPathSeparators);
return Separator >= 0 ? WinPath.Substring(Separator + 1) : WinPath;
}
/// <summary>
/// Returns the directory of an emulated windows path.
/// </summary>
/// <param name="WinPath">emulated windows path.</param>
/// <returns>returns the directory portion, or null when the path has none.</returns>
/// <remarks>
/// <see cref="Path.GetDirectoryName(string)"/> only splits on the host separator, so it returns an empty
/// string for a backslash path on a linux host.
/// </remarks>
public static string GetWindowsDirectoryName(string WinPath)
{
if (string.IsNullOrEmpty(WinPath))
return null;
int Separator = WinPath.LastIndexOfAny(WindowsPathSeparators);
if (Separator < 0)
return null;
if (Separator == 2 && WinPath.Length >= 3 && WinPath[1] == ':')
return WinPath.Substring(0, 3);
return Separator == 0 ? "\\" : WinPath.Substring(0, Separator);
}
/// <summary>
/// Resolves an emulated path into an absolute host path that can be used for real IO.
/// </summary>
/// <param name="EmulatedPath">emulated path to resolve.</param>
/// <param name="Format">binary format of the emulated process (used to apply platform rules).</param>
/// <param name="CreateDirectories">whether to create parent directories for the resolved path.</param>
/// <param name="PreserveFinalLink">whether to preserve the final filesystem link so the caller can inspect it.</param>
/// <returns>returns the resolved host path, or null if the path is not allowed or cannot be resolved.</returns>
/// <remarks>
/// This method shouldn't be called when resolving for writes, for resolving writes use <see cref="ResolveVirtualHostPath"/> instead.
/// </remarks>
public static string ResolveHostPath(string EmulatedPath, BinaryFormat Format, bool CreateDirectories = false, bool PreserveFinalLink = false)
{
if (string.IsNullOrWhiteSpace(EmulatedPath))
return null;
string Raw = EmulatedPath.Trim().TrimEnd('\0');
if (Raw.Length == 0)
return null;
if (Format == BinaryFormat.ELF)
return ResolveLinuxHostPath(Raw, CreateDirectories, PreserveFinalLink);
// Non-Windows binaries keep normal host semantics.
if (Format != BinaryFormat.PE)
return GetNativeFullPath(Raw, CreateDirectories);
string RawWinPath = Raw.Replace('/', '\\').Trim();
if (RawWinPath.StartsWith("\\KnownDlls\\", StringComparison.OrdinalIgnoreCase))
{
string KnownDllLeaf = Path.GetFileName(RawWinPath);
if (!string.IsNullOrEmpty(KnownDllLeaf))
RawWinPath = @"C:\Windows\System32\\" + KnownDllLeaf;
}
else if (RawWinPath.StartsWith("\\KnownDlls32\\", StringComparison.OrdinalIgnoreCase))
{
string KnownDllLeaf = Path.GetFileName(RawWinPath);
if (!string.IsNullOrEmpty(KnownDllLeaf))
RawWinPath = @"C:\Windows\SysWOW64\\" + KnownDllLeaf;
}
if (!LooksLikeAbsoluteWindowsPath(RawWinPath) && !RawWinPath.StartsWith("\\", StringComparison.Ordinal))
{
string LeafOnly = Path.GetFileName(RawWinPath);
if (!string.IsNullOrEmpty(LeafOnly) && string.Equals(LeafOnly, RawWinPath, StringComparison.OrdinalIgnoreCase))
{
if (Wow64FileRedirect)
{
string Wow64LeafPath = TryResolveFromWindowsLibsRelative(Path.Combine(WindowsLibsPath, "SysWOW64"), LeafOnly);
if (!string.IsNullOrEmpty(Wow64LeafPath))
return Wow64LeafPath;
}
string LeafLibPath = TryResolveFromWindowsLibsByLeaf(LeafOnly);
if (!string.IsNullOrEmpty(LeafLibPath))
return LeafLibPath;
}
}
string WinPath = NormalizeWindowsEmulatedPath(RawWinPath);
if (string.IsNullOrEmpty(WinPath))
return null;
WinPath = ApplyWow64FileRedirect(WinPath);
string VirtualPath = ResolveVirtualHostPathInternal(WinPath, CreateDirectories, PreserveFinalLink);
if (!string.IsNullOrEmpty(VirtualPath))
{
if (CreateDirectories || File.Exists(VirtualPath) || Directory.Exists(VirtualPath))
return VirtualPath;
}
// this resolve function be called for writes, so i think this is secure when reading or checking files/directories.
if (IsWindows)
{
string Native = GetNativeFullPath(WinPath, CreateDirectories);
if (!CreateDirectories && !string.IsNullOrEmpty(Native) && !File.Exists(Native) && !Directory.Exists(Native))
{
string Shipped = TryResolveFromWindowsLibs(WinPath);
if (!string.IsNullOrEmpty(Shipped))
return Shipped;
}
return Native;
}
string WindowsLibMapped = TryResolveFromWindowsLibs(WinPath);
if (!string.IsNullOrEmpty(WindowsLibMapped))
return WindowsLibMapped;
return VirtualPath;
}
/// <summary>
/// Resolves an emulated path directly into the virtual filesystem.
/// </summary>
/// <param name="EmulatedPath">emulated path to resolve.</param>
/// <param name="Format">binary format of the emulated process.</param>
/// <param name="CreateDirectories">whether to create parent directories for the resolved path.</param>
/// <returns>returns the resolved virtual path, or null if the path cannot be mapped into the sandbox.</returns>
public static string ResolveVirtualHostPath(string EmulatedPath, BinaryFormat Format, bool CreateDirectories = false)
{
if (string.IsNullOrWhiteSpace(EmulatedPath))
return null;
string Raw = EmulatedPath.Trim().TrimEnd('\0');
if (Raw.Length == 0)
return null;
if (Format == BinaryFormat.ELF)
return ResolveLinuxVirtualHostPath(Raw, CreateDirectories);
if (Format != BinaryFormat.PE)
return GetNativeFullPath(Raw, CreateDirectories);
string WinPath = NormalizeWindowsEmulatedPath(Raw);
if (string.IsNullOrEmpty(WinPath))
return null;
WinPath = ApplyWow64FileRedirect(WinPath);
return ResolveVirtualHostPathInternal(WinPath, CreateDirectories);
}
private static string NormalizeWindowsEmulatedPath(string Raw)
{
if (string.IsNullOrWhiteSpace(Raw))
return null;
string WinPath = Raw.Replace('/', '\\').Trim();
if (WinPath.StartsWith("\\\\?\\", StringComparison.OrdinalIgnoreCase) ||
WinPath.StartsWith("\\\\.\\", StringComparison.OrdinalIgnoreCase))
{
WinPath = WinPath.Substring(4);
}
else if (WinPath.StartsWith("\\??\\", StringComparison.OrdinalIgnoreCase))
{
WinPath = WinPath.Substring(4);
}
else if (WinPath.StartsWith("\\DosDevices\\", StringComparison.OrdinalIgnoreCase))
{
WinPath = WinPath.Substring("\\DosDevices\\".Length);
}
string Trimmed = WinPath.TrimStart('\\');
static bool EqualsIgnoreCase(string A, string B) =>
string.Equals(A, B, StringComparison.OrdinalIgnoreCase);
static void SplitFirst(string Value, out string First, out string Rest)
{
int Index = Value.IndexOf('\\');
if (Index < 0)
{
First = Value;
Rest = string.Empty;
return;
}
First = Value.Substring(0, Index);
Rest = Value.Substring(Index + 1);
}
SplitFirst(Trimmed, out string First, out string Rest);
if (EqualsIgnoreCase(First, "System32"))
{
Rest = Rest.TrimStart('\\');
WinPath = string.IsNullOrEmpty(Rest) ? @"C:\Windows\System32" : $@"C:\Windows\System32\{Rest}";
}
else if (EqualsIgnoreCase(First, "SysWow64"))
{
Rest = Rest.TrimStart('\\');
WinPath = string.IsNullOrEmpty(Rest) ? @"C:\Windows\SysWOW64" : $@"C:\Windows\SysWOW64\{Rest}";
}
else if (EqualsIgnoreCase(First, "Windows"))
{
SplitFirst(Rest, out string Second, out string Rest2);
if (EqualsIgnoreCase(Second, "System32"))
{
Rest2 = Rest2.TrimStart('\\');
WinPath = string.IsNullOrEmpty(Rest2) ? @"C:\Windows\System32" : $@"C:\Windows\System32\{Rest2}";
}
else if (EqualsIgnoreCase(Second, "SysWow64"))
{
Rest2 = Rest2.TrimStart('\\');
WinPath = string.IsNullOrEmpty(Rest2) ? @"C:\Windows\SysWOW64" : $@"C:\Windows\SysWOW64\{Rest2}";
}
}
if (WinPath.Length >= 2 && char.IsLetter(WinPath[0]) && WinPath[1] == ':')
{
if (WinPath.Length == 2)
WinPath += "\\";
else if (WinPath[2] != '\\')
WinPath = WinPath.Substring(0, 2) + "\\" + WinPath.Substring(2).TrimStart('\\');
}
else if (!LooksLikeAbsoluteWindowsPath(WinPath))
{
WinPath = DefaultDriveLetter + ":\\" + WinPath.TrimStart('\\');
}
else if (WinPath.StartsWith("\\", StringComparison.Ordinal))
{
WinPath = DefaultDriveLetter + ":" + WinPath;
}
return CanonicalizeWindowsAbsolutePath(WinPath);
}
/// <summary>
/// Applies WOW64 file-system redirection to a canonical absolute Windows path.
/// </summary>
private static string ApplyWow64FileRedirect(string WinPath)
{
if (!Wow64FileRedirect || string.IsNullOrEmpty(WinPath))
return WinPath;
if (WinPath.Equals(SysnativeWindowsPath, StringComparison.OrdinalIgnoreCase))
return System32WindowsPath;
if (WinPath.StartsWith(SysnativeWindowsPath + "\\", StringComparison.OrdinalIgnoreCase))
return System32WindowsPath + "\\" + WinPath.Substring(SysnativeWindowsPath.Length + 1);
if (WinPath.Equals(System32WindowsPath, StringComparison.OrdinalIgnoreCase))
return SysWow64WindowsPath;
if (!WinPath.StartsWith(System32WindowsPath + "\\", StringComparison.OrdinalIgnoreCase))
return WinPath;
string Rest = WinPath.Substring(System32WindowsPath.Length + 1);
foreach (string Excluded in Wow64RedirectExclusions)
{
if (Rest.Equals(Excluded, StringComparison.OrdinalIgnoreCase) ||
Rest.StartsWith(Excluded + "\\", StringComparison.OrdinalIgnoreCase))
return WinPath;
}
return SysWow64WindowsPath + "\\" + Rest;
}
private static string ResolveVirtualHostPathInternal(string WinPath, bool CreateDirectories, bool PreserveFinalLink = false)
{
if (string.IsNullOrWhiteSpace(WinPath))
return null;
if (WinPath.Length < 2 || WinPath[1] != ':')
return null;
if (TryResolveWindowsMount(WinPath, out string MountedHostPath))
return GetSandboxedFullPath(MountedHostPath, CreateDirectories, PreserveFinalLink);
char Drive = char.ToUpperInvariant(WinPath[0]);
string DriveRelative = WinPath.Substring(2).TrimStart('\\');
string Root = GetDriveRoot(Drive);
if (string.IsNullOrEmpty(Root))
return null;
string HostPath = CombineWindowsRelativePath(Root, DriveRelative);
return GetSandboxedFullPath(HostPath, CreateDirectories, PreserveFinalLink);
}
private static bool TryResolveWindowsMount(string WinPath, out string HostPath)
{
HostPath = null;
int BestLength = 0;
string BestRoot = null;
lock (DriveMapLock)
{
foreach (KeyValuePair<string, string> Pair in WindowsMountMappings)
{
string Mount = Pair.Key;
if (WinPath.Length < Mount.Length || Mount.Length <= BestLength)
continue;
if (WinPath.Length != Mount.Length && WinPath[Mount.Length] != '\\')
continue;
if (!WinPath.AsSpan(0, Mount.Length).Equals(Mount.AsSpan(), StringComparison.OrdinalIgnoreCase))
continue;
BestLength = Mount.Length;
BestRoot = Pair.Value;
}
}
if (BestRoot == null)
return false;
string Relative = WinPath.Length == BestLength ? string.Empty : WinPath.Substring(BestLength + 1);
HostPath = Relative.Length == 0 ? BestRoot : CombineWindowsRelativePath(BestRoot, Relative);
return true;
}
private static bool IsWindowsRootedPath(string PathValue)
{
if (string.IsNullOrEmpty(PathValue))
return false;
if (PathValue.Length >= 3 && char.IsLetter(PathValue[0]) && PathValue[1] == ':' && (PathValue[2] == '\\' || PathValue[2] == '/'))
return true;
return PathValue.StartsWith("\\\\", StringComparison.Ordinal);
}
private static string SanitizeWindowsPathComponent(string Component)
{
if (string.IsNullOrWhiteSpace(Component))
return "Program";
Span<char> Buffer = stackalloc char[Component.Length];
for (int i = 0; i < Component.Length; i++)
{
char Value = Component[i];
bool Invalid = Value < ' ' || Value == '<' || Value == '>' || Value == ':' || Value == '"' ||
Value == '/' || Value == '\\' || Value == '|' || Value == '?' || Value == '*';
Buffer[i] = Invalid ? '_' : Value;
}
string Sanitized = new string(Buffer).Trim().TrimEnd('.');
return Sanitized.Length == 0 ? "Program" : Sanitized;
}
/// <summary>
/// Canonicalizes an absolute DOS-style Windows path by collapsing "." and ".." segments.
/// The result always stays anchored to the original drive root.
/// </summary>
/// <param name="WinPath">absolute windows path to canonicalize.</param>
/// <returns>returns the canonical absolute windows path, or null if the path is invalid.</returns>
private static string CanonicalizeWindowsAbsolutePath(string WinPath)
{
if (string.IsNullOrWhiteSpace(WinPath))
return null;
string Normalized = WinPath.Replace('/', '\\');
if (Normalized.Length < 2 || !char.IsLetter(Normalized[0]) || Normalized[1] != ':')
return null;
char Drive = char.ToUpperInvariant(Normalized[0]);
string Relative = Normalized.Substring(2).TrimStart('\\');
List<string> Parts = new();
if (!string.IsNullOrEmpty(Relative))
{
foreach (string Part in Relative.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries))
{
if (Part == ".")
continue;
if (Part == "..")
{
if (Parts.Count > 0)
Parts.RemoveAt(Parts.Count - 1);
continue;
}
Parts.Add(Part);
}
}
if (Parts.Count == 0)
return Drive + ":\\";
return Drive + ":\\" + string.Join("\\", Parts);
}
/// <summary>
/// Reads a file from the emulated filesystem.
/// </summary>
/// <param name="Path">path to read.</param>
/// <param name="Format">binary format of the emulated process.</param>
/// <returns>returns the file bytes, or null if the file doesn't exist or cannot be accessed.</returns>
public static byte[] ReadFile(string Path, BinaryFormat Format)
{
string HostPath = ResolveHostPath(Path, Format);
if (string.IsNullOrEmpty(HostPath))
return null;
if (!File.Exists(HostPath))
return null;
return File.ReadAllBytes(HostPath);
}
/// <summary>
/// returns whether a file exists in the emulated filesystem.
/// </summary>
/// <param name="Path">path to check.</param>
/// <param name="Format">binary format of the emulated process.</param>
/// <returns>returns true if the file exists, otherwise false.</returns>
public static bool FileExists(string Path, BinaryFormat Format)
{
string HostPath = ResolveHostPath(Path, Format);
if (string.IsNullOrEmpty(HostPath))
return false;
return File.Exists(HostPath);
}
/// <summary>
/// returns whether a directory exists in the emulated filesystem.
/// </summary>
/// <param name="Path">path to check.</param>
/// <param name="Format">binary format of the emulated process.</param>
/// <returns>returns true if the directory exists, otherwise false.</returns>
public static bool DirectoryExists(string Path, BinaryFormat Format)
{
string HostPath = ResolveHostPath(Path, Format);
if (string.IsNullOrEmpty(HostPath))
return false;
return Directory.Exists(HostPath);
}
/// <summary>
/// Creates a directory inside the emulated virtual filesystem.
/// </summary>
/// <param name="Path">directory path to create.</param>
/// <param name="Format">binary format of the emulated process.</param>
/// <returns>returns true if the directory exists or was created successfully, otherwise false.</returns>
public static bool CreateDirectory(string Path, BinaryFormat Format)
{
string HostPath = ResolveVirtualHostPath(Path, Format, CreateDirectories: true);
if (string.IsNullOrEmpty(HostPath))
return false;
try
{
Directory.CreateDirectory(HostPath);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Gets the file size in bytes for a path in the emulated filesystem.
/// </summary>
/// <param name="Path">file path to query.</param>
/// <param name="Format">binary format of the emulated process.</param>
/// <returns>returns the file length in bytes, or 0 if the file doesn't exist or cannot be accessed.</returns>
public static long GetFileLength(string Path, BinaryFormat Format)
{
string HostPath = ResolveHostPath(Path, Format);
if (string.IsNullOrEmpty(HostPath) || !File.Exists(HostPath))
return 0;
try
{
return new FileInfo(HostPath).Length;
}
catch
{
return 0;
}
}
/// <summary>
/// Writes a file into the emulated filesystem.
/// </summary>
/// <param name="Path">path to write.</param>
/// <param name="Data">file bytes to write.</param>
/// <param name="Format">binary format of the emulated process.</param>
/// <returns>returns true if the file was written successfully, otherwise false.</returns>
public static bool WriteFile(string Path, byte[] Data, BinaryFormat Format)
{
if (Data == null)
return false;
string HostPath = ResolveVirtualHostPath(Path, Format, CreateDirectories: true);
if (string.IsNullOrEmpty(HostPath))
return false;
try
{
File.WriteAllBytes(HostPath, Data);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Ensures an emulated drive letter has a host root mapping (creates the directory if possible).
/// </summary>
/// <param name="DriveLetter">drive letter to map.</param>
/// <param name="HostRoot">host directory root for the drive.</param>
private static void EnsureDriveMapping(char DriveLetter, string HostRoot)
{
DriveLetter = char.ToUpperInvariant(DriveLetter);
if (!char.IsLetter(DriveLetter))
return;
if (string.IsNullOrWhiteSpace(HostRoot))
return;
lock (DriveMapLock)
{
DriveMappings[DriveLetter] = HostRoot;
}
try
{
Directory.CreateDirectory(HostRoot);
}
catch
{
Utils.LogError("[IO] Failed to create drive mapping directory.");
}
}
private static bool EnsureWindowsMountMapping(string GuestDirectory, string HostRoot)
{
string NormalizedGuestDirectory = CanonicalizeWindowsAbsolutePath(GuestDirectory);
// A drive root belongs to the drive map, and letting one in here would make every mount key need a
// trailing-separator special case during lookup.
if (string.IsNullOrEmpty(NormalizedGuestDirectory) || NormalizedGuestDirectory.Length <= 3 || string.IsNullOrWhiteSpace(HostRoot))
return false;
string NormalizedHostRoot;
try
{
NormalizedHostRoot = ResolveSandboxLinks(Path.GetFullPath(HostRoot), IncludeFinal: true, EnforceAllowedRoots: false);
}
catch
{
return false;
}
if (string.IsNullOrEmpty(NormalizedHostRoot))
return false;
lock (DriveMapLock)
{
WindowsMountMappings[NormalizedGuestDirectory] = NormalizedHostRoot;
}
try
{
Directory.CreateDirectory(NormalizedHostRoot);
}
catch
{
Utils.LogError("[IO] Failed to create windows mount mapping directory.");
}
return true;
}
private static void EnsureLinuxMountMapping(string MountPoint, string HostRoot)
{
string NormalizedMountPoint = NormalizeLinuxMountPoint(MountPoint);
if (string.IsNullOrEmpty(NormalizedMountPoint) || string.IsNullOrWhiteSpace(HostRoot))
return;
lock (DriveMapLock)
{
LinuxMountMappings[NormalizedMountPoint] = HostRoot;
}
try
{
Directory.CreateDirectory(HostRoot);
}
catch
{
Utils.LogError("[IO] Failed to create linux mount mapping directory.");
}
}
private static string NormalizeLinuxMountPoint(string MountPoint)
{
string Normalized = NormalizeLinuxEmulatedPath(MountPoint);
if (string.IsNullOrEmpty(Normalized))
return null;
if (Normalized.Length > 1)
Normalized = Normalized.TrimEnd('/');
return Normalized;
}
private static string ResolveLinuxHostPath(string EmulatedPath, bool CreateDirectories, bool PreserveFinalLink)
{
string LinuxPath = NormalizeLinuxEmulatedPath(EmulatedPath);
if (string.IsNullOrEmpty(LinuxPath))
return null;
string VirtualHostPath = ResolveLinuxVirtualHostPathInternal(LinuxPath, CreateDirectories, PreserveFinalLink);
if (CreateDirectories || LinuxPath == "/")
return VirtualHostPath;
if (LinuxHostPathExists(VirtualHostPath))
return VirtualHostPath;
foreach (string AliasPath in EnumerateLinuxPathAliases(LinuxPath))
{
if (string.Equals(AliasPath, LinuxPath, StringComparison.Ordinal))
continue;
string AliasVirtualHostPath = ResolveLinuxVirtualHostPathInternal(AliasPath, false, PreserveFinalLink);
if (LinuxHostPathExists(AliasVirtualHostPath))
return AliasVirtualHostPath;
}
if (IsLinux)
{
string NativeHostPath = GetNativeFullPath(LinuxPath, false);
if (LinuxHostPathExists(NativeHostPath))
return NativeHostPath;
foreach (string AliasPath in EnumerateLinuxPathAliases(LinuxPath))
{
if (string.Equals(AliasPath, LinuxPath, StringComparison.Ordinal))
continue;
string AliasNativeHostPath = GetNativeFullPath(AliasPath, false);
if (LinuxHostPathExists(AliasNativeHostPath))
return AliasNativeHostPath;
}
}
return VirtualHostPath;
}
private static string ResolveLinuxVirtualHostPath(string EmulatedPath, bool CreateDirectories)
{
string LinuxPath = NormalizeLinuxEmulatedPath(EmulatedPath);
if (string.IsNullOrEmpty(LinuxPath))
return null;
return ResolveLinuxVirtualHostPathInternal(LinuxPath, CreateDirectories, PreserveFinalLink: false);
}
private static bool LinuxHostPathExists(string HostPath)
{
if (string.IsNullOrWhiteSpace(HostPath))
return false;
return File.Exists(HostPath) || Directory.Exists(HostPath);
}
private static IEnumerable<string> EnumerateLinuxPathAliases(string LinuxPath)
{
if (string.IsNullOrWhiteSpace(LinuxPath) || !LinuxPath.StartsWith("/", StringComparison.Ordinal))
yield break;
yield return LinuxPath;
if (LinuxPath.StartsWith("/lib64/", StringComparison.Ordinal))
yield return "/usr" + LinuxPath;
else if (LinuxPath.StartsWith("/usr/lib64/", StringComparison.Ordinal))
yield return LinuxPath.Substring(4);
if (LinuxPath.StartsWith("/lib/", StringComparison.Ordinal))
yield return "/usr" + LinuxPath;
else if (LinuxPath.StartsWith("/usr/lib/", StringComparison.Ordinal))
yield return LinuxPath.Substring(4);
}
private static string NormalizeLinuxEmulatedPath(string Raw)
{
if (string.IsNullOrWhiteSpace(Raw))
return null;
string LinuxPath = Raw.Replace('\\', '/').Trim().TrimEnd(' ');
if (LinuxPath.Length == 0)
return null;
if (!LinuxPath.StartsWith("/", StringComparison.Ordinal))
{
string CurrentDirectory = string.IsNullOrWhiteSpace(LinuxCurrentDirectory) ? "/" : LinuxCurrentDirectory;
if (!CurrentDirectory.StartsWith("/", StringComparison.Ordinal))
CurrentDirectory = "/" + CurrentDirectory;
LinuxPath = CurrentDirectory.TrimEnd('/') + "/" + LinuxPath;
}
List<string> Parts = new();
foreach (string Part in LinuxPath.Split('/', StringSplitOptions.RemoveEmptyEntries))
{
if (Part == ".")
continue;
if (Part == "..")
{
if (Parts.Count > 0)
Parts.RemoveAt(Parts.Count - 1);
continue;
}
Parts.Add(Part);
}
if (Parts.Count == 0)
return "/";
return "/" + string.Join('/', Parts);
}
private static string ResolveLinuxVirtualHostPathInternal(string LinuxPath, bool CreateDirectories, bool PreserveFinalLink = false)
{
if (string.IsNullOrWhiteSpace(LinuxPath) || !LinuxPath.StartsWith("/", StringComparison.Ordinal))
return null;
string MountPoint = "/";
string HostRoot = LinuxVirtualFileSystemRoot;
lock (DriveMapLock)
{
foreach (KeyValuePair<string, string> Pair in LinuxMountMappings)
{
string CandidateMount = Pair.Key;
if (string.IsNullOrWhiteSpace(CandidateMount) || string.IsNullOrWhiteSpace(Pair.Value))
continue;
bool IsMatch = LinuxPath == CandidateMount ||
LinuxPath.StartsWith(CandidateMount + "/", StringComparison.Ordinal) ||
CandidateMount == "/";
if (!IsMatch)
continue;
if (CandidateMount.Length > MountPoint.Length || (CandidateMount.Length == MountPoint.Length && !string.Equals(HostRoot, Pair.Value, StringComparison.Ordinal)))
{
MountPoint = CandidateMount;
HostRoot = Pair.Value;
}
}
}
string RelativePath = LinuxPath == MountPoint ? string.Empty : LinuxPath.Substring(MountPoint.Length).TrimStart('/');
string HostPath = string.IsNullOrEmpty(RelativePath) ? HostRoot : CombineLinuxRelativePath(HostRoot, RelativePath);
return GetSandboxedFullPath(HostPath, CreateDirectories, PreserveFinalLink);
}
private static string CombineLinuxRelativePath(string Root, string LinuxRelative)
{
if (string.IsNullOrWhiteSpace(Root))
return null;
if (string.IsNullOrWhiteSpace(LinuxRelative))
return Root;
string[] Parts = LinuxRelative.Split('/', StringSplitOptions.RemoveEmptyEntries);
string Result = Root;
for (int i = 0; i < Parts.Length; i++)
Result = Path.Combine(Result, Parts[i]);
return Result;
}
/// <summary>
/// Gets the host root directory for an emulated drive letter (creates a default mapping on demand).
/// </summary>
/// <param name="DriveLetter">drive letter to resolve.</param>
/// <returns>returns the host root directory for the drive.</returns>
private static string GetDriveRoot(char DriveLetter)
{
DriveLetter = char.ToUpperInvariant(DriveLetter);
lock (DriveMapLock)
{
if (DriveMappings.TryGetValue(DriveLetter, out string Root) && !string.IsNullOrWhiteSpace(Root))
return Root;
}
// Create a default mapping on demand.
string DefaultRoot = Path.Combine(VirtualFileSystemRoot, DriveLetter.ToString());
EnsureDriveMapping(DriveLetter, DefaultRoot);
return DefaultRoot;
}
/// <summary>
/// Rebuilds the allowed root list used to keep emulated IO sandboxed.
/// </summary>
private static void RefreshAllowedRoots()
{
lock (DriveMapLock)
{
AllowedRoots.Clear();
foreach (KeyValuePair<char, string> Pair in DriveMappings)
{
if (string.IsNullOrWhiteSpace(Pair.Value))
continue;
try
{
AllowedRoots.Add(Path.GetFullPath(Pair.Value));
}
catch
{
// ignore
}
}
foreach (KeyValuePair<string, string> Pair in LinuxMountMappings)
{
if (string.IsNullOrWhiteSpace(Pair.Value))
continue;
try
{
AllowedRoots.Add(Path.GetFullPath(Pair.Value));
}
catch
{
// ignore
}
}
foreach (KeyValuePair<string, string> Pair in WindowsMountMappings)
{
if (string.IsNullOrWhiteSpace(Pair.Value))
continue;
try
{
AllowedRoots.Add(Path.GetFullPath(Pair.Value));
}
catch
{
// ignore
}
}
try
{
AllowedRoots.Add(Path.GetFullPath(WindowsLibsPath));
}
catch
{
// ignore
}
}
}
/// <summary>
/// Returns a full path only if it stays within the allowed sandbox roots.
/// </summary>
/// <param name="CandidatePath">candidate path to normalize and validate.</param>
/// <param name="CreateDirectories">whether to create parent directories.</param>
/// <param name="PreserveFinalLink">whether to keep the final path component unresolved.</param>
/// <returns>returns the sandboxed full path, or null if it escapes the sandbox.</returns>
private static string GetSandboxedFullPath(string CandidatePath, bool CreateDirectories, bool PreserveFinalLink = false)
{
if (string.IsNullOrWhiteSpace(CandidatePath))
return null;
string Full;
try
{
Full = Path.GetFullPath(CandidatePath);
}
catch
{
return null;
}
if (!IsUnderAllowedRoots(Full))
return null;
bool IncludeFinal = !PreserveFinalLink && (File.Exists(Full) || Directory.Exists(Full));
string Resolved = ResolveSandboxLinks(Full, IncludeFinal);
if (string.IsNullOrEmpty(Resolved) || !IsUnderAllowedRoots(Resolved))
return null;
if (CreateDirectories)
{
try
{
string Dir = Path.GetDirectoryName(Resolved);
if (!string.IsNullOrEmpty(Dir))
Directory.CreateDirectory(Dir);
}
catch
{
return null;
}
IncludeFinal = !PreserveFinalLink && (File.Exists(Resolved) || Directory.Exists(Resolved));
Resolved = ResolveSandboxLinks(Resolved, IncludeFinal);
if (string.IsNullOrEmpty(Resolved) || !IsUnderAllowedRoots(Resolved))
return null;
}
return Resolved;
}
private static string ResolveSandboxLinks(string FullPath, bool IncludeFinal, bool EnforceAllowedRoots = true)
{
if (string.IsNullOrWhiteSpace(FullPath))
return null;
string Normalized;
try
{
Normalized = Path.GetFullPath(FullPath);
}
catch
{
return null;
}
string Root = Path.GetPathRoot(Normalized);
if (string.IsNullOrEmpty(Root))
return null;
string Relative = Normalized.Substring(Root.Length);
string[] Parts = Relative.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
if (Parts.Length == 0)
return Normalized;
string Current = Root;
for (int i = 0; i < Parts.Length; i++)
{
Current = Path.Combine(Current, Parts[i]);
bool IsFinal = i == Parts.Length - 1;
if (IsFinal && !IncludeFinal)
continue;
if (!File.Exists(Current) && !Directory.Exists(Current))
continue;
FileAttributes Attributes;
try
{
Attributes = File.GetAttributes(Current);
}
catch
{
return null;
}
if ((Attributes & FileAttributes.ReparsePoint) == 0)
continue;
string Resolved = ResolveLinkTarget(Current, Attributes);
if (string.IsNullOrEmpty(Resolved))
return null;
Current = Resolved;
if (EnforceAllowedRoots && !IsUnderAllowedRoots(Current))
return null;
}
try
{
return Path.GetFullPath(Current);
}
catch
{
return null;
}
}
private static string ResolveLinkTarget(string PathValue, FileAttributes Attributes)
{
try
{
FileSystemInfo Target = (Attributes & FileAttributes.Directory) != 0
? Directory.ResolveLinkTarget(PathValue, true)
: File.ResolveLinkTarget(PathValue, true);
if (Target == null || string.IsNullOrWhiteSpace(Target.FullName))
return null;
return Path.GetFullPath(Target.FullName);
}
catch
{
return null;
}
}
/// <summary>
/// Returns a full path without sandbox validation (normal .NET path resolution).
/// </summary>
/// <param name="CandidatePath">candidate path to normalize.</param>
/// <param name="CreateDirectories">whether to create parent directories.</param>
/// <returns>returns the normalized full path, or null if it cannot be resolved.</returns>
private static string GetNativeFullPath(string CandidatePath, bool CreateDirectories)
{
if (string.IsNullOrWhiteSpace(CandidatePath))
return null;
string Full;
try
{
Full = Path.GetFullPath(CandidatePath);
}
catch
{
return null;
}
if (CreateDirectories)
{
try
{
string Dir = Path.GetDirectoryName(Full);
if (!string.IsNullOrEmpty(Dir))
Directory.CreateDirectory(Dir);
}
catch
{
// ignore
}
}
return Full;
}
/// <summary>
/// returns whether a full path is inside any allowed sandbox root directory.
/// </summary>
/// <param name="FullPath">full path to validate.</param>
/// <returns>returns true if the path is allowed, otherwise false.</returns>
private static bool IsUnderAllowedRoots(string FullPath)
{
if (string.IsNullOrWhiteSpace(FullPath))
return false;
string Normalized;
try
{
Normalized = Path.GetFullPath(FullPath);
}
catch
{
return false;
}
lock (DriveMapLock)
{
foreach (string Root in AllowedRoots)
{
if (string.IsNullOrWhiteSpace(Root))
continue;
string RootNorm = Root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
StringComparison Comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (Normalized.StartsWith(RootNorm + Path.DirectorySeparatorChar, Comparison) ||
string.Equals(Normalized, RootNorm, Comparison))
{
return true;
}
}
}
return false;
}
/// <summary>
/// Combines a host root directory with a Windows-style relative path.
/// </summary>
/// <param name="Root">host root directory.</param>
/// <param name="WindowsRelative">windows-style relative path using backslashes.</param>
/// <returns>returns the combined host path.</returns>
private static string CombineWindowsRelativePath(string Root, string WindowsRelative)
{
if (string.IsNullOrWhiteSpace(Root))
return null;
if (string.IsNullOrWhiteSpace(WindowsRelative))
return Root;
string[] Parts = WindowsRelative.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
string Result = Root;
for (int i = 0; i < Parts.Length; i++)
Result = Path.Combine(Result, Parts[i]);
return Result;
}
/// <summary>
/// returns whether the provided string looks like an absolute Windows path or UNC path.
/// </summary>
/// <param name="WinPath">path to check.</param>
/// <returns>returns true if it looks absolute, otherwise false.</returns>
private static bool LooksLikeAbsoluteWindowsPath(string WinPath)
{
if (string.IsNullOrEmpty(WinPath))
return false;
if (WinPath.Length >= 3 && char.IsLetter(WinPath[0]) && WinPath[1] == ':' && WinPath[2] == '\\')
return true;
if (WinPath.StartsWith("\\\\", StringComparison.Ordinal))
return true;
if (WinPath.StartsWith("\\", StringComparison.Ordinal))
return true;
return false;
}
/// <summary>
/// Attempts to translate a Windows System32/SysWOW64/KnownDlls path into the shipped WindowsLibs directory.
/// </summary>
/// <param name="WinPath">windows-style path to resolve.</param>
/// <returns>returns the resolved host path inside WindowsLibs, or null if no mapping applies.</returns>
private static string TryResolveFromWindowsLibs(string WinPath)
{
if (string.IsNullOrWhiteSpace(WinPath))
return null;
// Common shipped locations.
const string System32Prefix = "C:\\Windows\\System32\\";
const string SysWow64Prefix = "C:\\Windows\\SysWOW64\\";
string Normalized = WinPath.Replace('/', '\\');
if (Normalized.StartsWith(System32Prefix, StringComparison.OrdinalIgnoreCase))
{
string Rel = Normalized.Substring(System32Prefix.Length);
return TryResolveFromWindowsLibsRelative(WindowsLibsPath, Rel);
}
if (Normalized.StartsWith(SysWow64Prefix, StringComparison.OrdinalIgnoreCase))
{
string Rel = Normalized.Substring(SysWow64Prefix.Length);
return TryResolveFromWindowsLibsRelative(Path.Combine(WindowsLibsPath, "SysWOW64"), Rel);
}
// Some callers pass "\\KnownDlls\\xxx.dll" or similar, which ultimately maps to System32.
if (Normalized.StartsWith("\\KnownDlls\\", StringComparison.OrdinalIgnoreCase) || Normalized.StartsWith("\\KnownDlls32\\", StringComparison.OrdinalIgnoreCase))
{
string Leaf = Path.GetFileName(Normalized);
return TryResolveFromWindowsLibsByLeaf(Leaf);
}
return null;
}
/// <summary>
/// Attempts to resolve a relative path inside WindowsLibs (falls back to a case-insensitive leaf search).
/// </summary>
/// <param name="BaseDir">base directory inside WindowsLibs.</param>
/// <param name="WindowsRelative">windows-style relative path.</param>
/// <returns>returns the resolved host path, or null if it cannot be found.</returns>
private static string TryResolveFromWindowsLibsRelative(string BaseDir, string WindowsRelative)
{
if (string.IsNullOrWhiteSpace(BaseDir) || string.IsNullOrWhiteSpace(WindowsRelative))
return null;
string Candidate = CombineWindowsRelativePath(BaseDir, WindowsRelative);
if (string.IsNullOrWhiteSpace(Candidate))
return null;
string Full = GetSandboxedFullPath(Candidate, CreateDirectories: false);
if (!string.IsNullOrEmpty(Full) && (File.Exists(Full) || Directory.Exists(Full)))
return Full;
// Fall back to a case-insensitive leaf search.
string Leaf = Path.GetFileName(WindowsRelative);
return TryResolveFromWindowsLibsByLeaf(Leaf);
}
/// <summary>
/// Looks up a file by name inside WindowsLibs (case-insensitive).
/// </summary>
/// <param name="Leaf">file name to search for.</param>
/// <returns>returns the resolved host path, or null if not found.</returns>
private static string TryResolveFromWindowsLibsByLeaf(string Leaf)
{
if (string.IsNullOrWhiteSpace(Leaf))
return null;
EnsureWindowsLibsIndex();
lock (WindowsLibsIndexLock)
{
if (WindowsLibsFileIndex != null && WindowsLibsFileIndex.TryGetValue(Leaf, out string Found))
return Found;
}
return null;
}
/// <summary>
/// Builds the WindowsLibs leaf index on demand for fast case-insensitive resolution.
/// </summary>
private static void EnsureWindowsLibsIndex()
{
lock (WindowsLibsIndexLock)
{
if (WindowsLibsFileIndex != null)
return;
WindowsLibsFileIndex = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
if (!Directory.Exists(WindowsLibsPath))
return;
foreach (string FilePath in Directory.EnumerateFiles(WindowsLibsPath, "*", SearchOption.AllDirectories))
{
string Leaf = Path.GetFileName(FilePath);
if (string.IsNullOrEmpty(Leaf))
continue;
// Keep first hit to avoid churn when duplicates exist.
if (!WindowsLibsFileIndex.ContainsKey(Leaf))
{
string Full = GetSandboxedFullPath(FilePath, CreateDirectories: false);
if (!string.IsNullOrEmpty(Full))
WindowsLibsFileIndex[Leaf] = Full;
}
}
}
catch (Exception ex)
{
Utils.LogError($"[IO] Failed to build WindowsLibs Index: {ex.Message}");
}
}
}
}
}
/// <summary>
/// Generators for other platforms.
/// </summary>
public class CrossGenerator
{
/// <summary>
/// Generate an ApiSetMap that the emulated binary can use.
/// </summary>
/// <returns>returns a byte array containing the generated ApiSetMap.</returns>
/// <exception cref="ArgumentNullException"></exception>
public static byte[] GenerateMap()
{
Dictionary<string, string> ApiSetMap = HelperFunctions.ApiSetMap;
Dictionary<ApiSetOverrideKey, string> ApiSetOverrideMap = HelperFunctions.ApiSetOverrideMap;
if (ApiSetMap == null)
throw new ArgumentNullException(nameof(ApiSetMap));
ApiSetOverrideMap ??= new Dictionary<ApiSetOverrideKey, string>();
// Aggregate everything per contract so offsets/counts are easy to compute later.
Dictionary<string, ApiSetContractBuild> ContractTable = new(StringComparer.OrdinalIgnoreCase);
foreach (KeyValuePair<string, string> Pair in ApiSetMap)
{
string ContractName = NormalizeContractName(Pair.Key);
string HostName = NormalizeHostName(Pair.Value);
if (string.IsNullOrWhiteSpace(ContractName) || string.IsNullOrWhiteSpace(HostName))
continue;
ApiSetContractBuild Contract = GetOrCreateContract(ContractTable, ContractName);
Contract.DefaultHostName = HostName;
}
foreach (KeyValuePair<ApiSetOverrideKey, string> Pair in ApiSetOverrideMap)
{
string ContractName = NormalizeContractName(Pair.Key.ContractName);
string ImportingModuleName = NormalizeHostName(Pair.Key.ImportingModuleName);
string HostName = NormalizeHostName(Pair.Value);
if (string.IsNullOrWhiteSpace(ContractName) || string.IsNullOrWhiteSpace(ImportingModuleName) || string.IsNullOrWhiteSpace(HostName))
continue;
ApiSetContractBuild Contract = GetOrCreateContract(ContractTable, ContractName);
Contract.OverrideHosts[ImportingModuleName] = HostName;
}
List<ApiSetContractBuild> Contracts = ContractTable.Values
.Where(Contract => !string.IsNullOrWhiteSpace(Contract.DefaultHostName) || Contract.OverrideHosts.Count != 0)
.OrderBy(Contract => Contract.ContractName, StringComparer.OrdinalIgnoreCase)
.ToList();
int ContractCount = Contracts.Count;
// Values are stored in one packed array, so count them up front.
int TotalValueCount = 0;
for (int Index = 0; Index < ContractCount; Index++)
{
int ValueCount = Contracts[Index].OverrideHosts.Count + (string.IsNullOrWhiteSpace(Contracts[Index].DefaultHostName) ? 0 : 1);
TotalValueCount += ValueCount;
}
// ApiSet schema v6 (header + entry table + hash table + value table + UTF-16 string pool)
const int NamespaceSize = 28;
const int NamespaceEntrySize = 24;
const int HashEntrySize = 8;
const int ValueEntrySize = 20;
int EntryOffset = NamespaceSize;
int HashOffset = EntryOffset + (ContractCount * NamespaceEntrySize);
int ValuesOffset = HashOffset + (ContractCount * HashEntrySize);
int StringPoolOffset = ValuesOffset + (TotalValueCount * ValueEntrySize);
using MemoryStream Stream = new();
using BinaryWriter Writer = new(Stream, Encoding.Unicode, leaveOpen: true);
Stream.SetLength(StringPoolOffset);
Dictionary<string, ApiSetString> StringTable = new(StringComparer.OrdinalIgnoreCase);
ApiSetString AddString(string Value)
{
if (StringTable.TryGetValue(Value, out ApiSetString Cached))
return Cached;
uint Offset = checked((uint)Stream.Length);
byte[] Bytes = Encoding.Unicode.GetBytes(Value);
Writer.Seek((int)Stream.Length, SeekOrigin.Begin);
Writer.Write(Bytes);
ApiSetString Result = new(Offset, checked((uint)Bytes.Length));
StringTable[Value] = Result;
return Result;
}
// populate the pool first so all offsets are known before writing structs
for (int Index = 0; Index < ContractCount; Index++)
{
AddString(Contracts[Index].ContractName);
foreach (KeyValuePair<string, string> OverridePair in Contracts[Index].OverrideHosts.OrderBy(Pair => Pair.Key, StringComparer.OrdinalIgnoreCase))
{
AddString(OverridePair.Key);
AddString(OverridePair.Value);
}
if (!string.IsNullOrWhiteSpace(Contracts[Index].DefaultHostName))
AddString(Contracts[Index].DefaultHostName);
}
const uint HashFactor = 0x1F;
// hash table is sorted by hash, each entry points back into the namespace entry array by index.
List<ApiSetHashItem> HashItems = new(ContractCount);
for (int Index = 0; Index < ContractCount; Index++)
{
uint HashedLengthBytes = ComputeHashedLengthBytes(Contracts[Index].ContractName);
uint Hash = ComputeApiSetHash(Contracts[Index].ContractName, HashedLengthBytes, HashFactor);
HashItems.Add(new ApiSetHashItem(Hash, checked((uint)Index)));
}
HashItems.Sort((A, B) => A.Hash.CompareTo(B.Hash));
uint TotalSize = checked((uint)Stream.Length);
Writer.Seek(0, SeekOrigin.Begin);
Writer.Write((uint)6);
Writer.Write(TotalSize);
Writer.Write((uint)0);
Writer.Write((uint)ContractCount);
Writer.Write((uint)EntryOffset);
Writer.Write((uint)HashOffset);
Writer.Write(HashFactor);
int CurrentValueIndex = 0;
for (int ContractIndex = 0; ContractIndex < ContractCount; ContractIndex++)
{
ApiSetContractBuild Contract = Contracts[ContractIndex];
ApiSetString NameString = StringTable[Contract.ContractName];
uint HashedLengthBytes = ComputeHashedLengthBytes(Contract.ContractName);
int ValueCount = Contract.OverrideHosts.Count + (string.IsNullOrWhiteSpace(Contract.DefaultHostName) ? 0 : 1);
uint ValueOffset = checked((uint)(ValuesOffset + (CurrentValueIndex * ValueEntrySize)));
int EntryBase = EntryOffset + (ContractIndex * NamespaceEntrySize);
Writer.Seek(EntryBase, SeekOrigin.Begin);
Writer.Write((uint)0);
Writer.Write(NameString.Offset);
Writer.Write(NameString.ByteLength);
Writer.Write(HashedLengthBytes);
Writer.Write(ValueOffset);
Writer.Write(checked((uint)ValueCount));
foreach (KeyValuePair<string, string> OverridePair in Contract.OverrideHosts.OrderBy(Pair => Pair.Key, StringComparer.OrdinalIgnoreCase))
{
ApiSetString ImportingModuleString = StringTable[OverridePair.Key];
ApiSetString HostString = StringTable[OverridePair.Value];
int ValueBase = ValuesOffset + (CurrentValueIndex * ValueEntrySize);
Writer.Seek(ValueBase, SeekOrigin.Begin);
Writer.Write((uint)0);
Writer.Write(ImportingModuleString.Offset);
Writer.Write(ImportingModuleString.ByteLength);
Writer.Write(HostString.Offset);
Writer.Write(HostString.ByteLength);
CurrentValueIndex++;
}
if (!string.IsNullOrWhiteSpace(Contract.DefaultHostName))
{
ApiSetString HostString = StringTable[Contract.DefaultHostName];
int ValueBase = ValuesOffset + (CurrentValueIndex * ValueEntrySize);
Writer.Seek(ValueBase, SeekOrigin.Begin);
Writer.Write((uint)0);
Writer.Write((uint)0);
Writer.Write((uint)0);
Writer.Write(HostString.Offset);
Writer.Write(HostString.ByteLength);
CurrentValueIndex++;
}
}
for (int Index = 0; Index < ContractCount; Index++)
{
int HashBase = HashOffset + (Index * HashEntrySize);
Writer.Seek(HashBase, SeekOrigin.Begin);
Writer.Write(HashItems[Index].Hash);
Writer.Write(HashItems[Index].Index);
}
return Stream.ToArray();
}
private static ApiSetContractBuild GetOrCreateContract(Dictionary<string, ApiSetContractBuild> ContractTable, string ContractName)
{
if (!ContractTable.TryGetValue(ContractName, out ApiSetContractBuild Contract))
{
Contract = new ApiSetContractBuild(ContractName);
ContractTable[ContractName] = Contract;
}
return Contract;
}
private static string NormalizeContractName(string Name)
{
Name = (Name ?? string.Empty).Trim();
if (Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
Name = Name[..^4];
return Name.ToLowerInvariant();
}
private static string NormalizeHostName(string Name)
{
Name = (Name ?? string.Empty).Trim();
if (!Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
Name += ".dll";
return Name.ToLowerInvariant();
}
private static uint ComputeHashedLengthBytes(string ContractNameWithoutDll)
{
if (string.IsNullOrEmpty(ContractNameWithoutDll))
return 0;
int LastDash = ContractNameWithoutDll.LastIndexOf('-');
if (LastDash <= 0)
return checked((uint)(ContractNameWithoutDll.Length * 2));
string Tail = ContractNameWithoutDll[(LastDash + 1)..];
bool TailIsDigits = Tail.Length > 0 && Tail.All(char.IsDigit);
if (TailIsDigits)
return checked((uint)(LastDash * 2));
return checked((uint)(ContractNameWithoutDll.Length * 2));
}
private static uint ComputeApiSetHash(string ContractNameWithoutDll, uint HashedLengthBytes, uint HashFactor)
{
string DllName = (ContractNameWithoutDll ?? string.Empty) + ".dll";
int HashedLengthChars = checked((int)(HashedLengthBytes / 2));
int Limit = Math.Min(DllName.Length, HashedLengthChars);
uint Hash = 0;
for (int Index = 0; Index < Limit; Index++)
{
char CharValue = DllName[Index];
if (CharValue >= 'A' && CharValue <= 'Z')
CharValue = (char)(CharValue + 0x20);
Hash = unchecked((Hash * HashFactor) + CharValue);
}
return Hash;
}
private sealed class ApiSetContractBuild
{
public readonly string ContractName;
public string DefaultHostName;
public readonly Dictionary<string, string> OverrideHosts;
public ApiSetContractBuild(string ContractName)
{
this.ContractName = ContractName;
DefaultHostName = string.Empty;
OverrideHosts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
private readonly struct ApiSetHashItem
{
public readonly uint Hash;
public readonly uint Index;
public ApiSetHashItem(uint Hash, uint Index)
{
this.Hash = Hash;
this.Index = Index;
}
}
private readonly struct ApiSetString
{
public readonly uint Offset;
public readonly uint ByteLength;
public ApiSetString(uint Offset, uint ByteLength)
{
this.Offset = Offset;
this.ByteLength = ByteLength;
}
}
}
[Flags]
public enum LinuxSignal
{
None = 0,
SIGHUP = 1 << 0,
SIGINT = 1 << 1,
SIGQUIT = 1 << 2,
SIGILL = 1 << 3,
SIGTRAP = 1 << 4,
SIGABRT = 1 << 5,
SIGBUS = 1 << 6,
SIGFPE = 1 << 7,
SIGKILL = 1 << 8,
SIGUSR1 = 1 << 9,
SIGSEGV = 1 << 10,
SIGUSR2 = 1 << 11,
SIGPIPE = 1 << 12,
SIGALRM = 1 << 13,
SIGTERM = 1 << 14,
SIGSTKFLT = 1 << 15,
SIGCHLD = 1 << 16,
SIGCONT = 1 << 17,
SIGSTOP = 1 << 18,
SIGTSTP = 1 << 19,
SIGTTIN = 1 << 20,
SIGTTOU = 1 << 21,
SIGURG = 1 << 22,
SIGXCPU = 1 << 23,
SIGXFSZ = 1 << 24,
SIGVTALRM = 1 << 25,
SIGPROF = 1 << 26,
SIGWINCH = 1 << 27,
SIGIO = 1 << 28,
SIGPWR = 1 << 29,
SIGSYS = 1 << 30,
FatalException = SIGILL | SIGBUS | SIGFPE | SIGSEGV
}
public static unsafe class LinuxNativeCrashHandler
{
private const int SA_SIGINFO = 0x00000004;
private const int SA_RESETHAND = unchecked((int)0x80000000);
private const int SIGILL = 4;
private const int SIGTRAP = 5;
private const int SIGABRT = 6;
private const int SIGBUS = 7;
private const int SIGFPE = 8;
private const int SIGSEGV = 11;
private const int SIGSYS = 31;
private const int SEGV_MAPERR = 1;
private const int SEGV_ACCERR = 2;
private const int REG_RIP = 16;
private const int REG_ERR = 19;
private const int REG_TRAPNO = 20;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void CrashCallback(int sig, ulong faultAddress, ulong rip, IntPtr modulePathUtf8, ulong moduleBase, IntPtr symbolNameUtf8, ulong symbolAddress, int siCode, long trapNo, long err);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SigActionHandler(int sig, SigInfo* info, void* ucontext);
private static CrashCallback? _UserCallback;
private static SigActionHandler? _SignalHandler;
public static void Install(CrashCallback callback, LinuxSignal signals)
{
if (!OperatingSystem.IsLinux())
return;
_UserCallback = callback;
_SignalHandler = OnSignal;
var sa = default(SigAction);
sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
sa.sa_sigaction = Marshal.GetFunctionPointerForDelegate(_SignalHandler);
foreach (var entry in SignalMap)
{
if ((signals & entry.Flag) != 0)
Register(entry.Number, &sa);
}
}
public static string GetSegvReason(int siCode)
{
return siCode switch
{
SEGV_MAPERR => "UNMAPPED",
SEGV_ACCERR => "PROTECTION",
_ => $"CODE_{siCode}"
};
}
public static string GetPageFaultAccess(long trapNo, long err)
{
if (trapNo != 14)
return "UNKNOWN";
bool isWrite = (err & (1 << 1)) != 0;
bool isExec = (err & (1 << 4)) != 0;
if (isExec) return "EXECUTE";
return isWrite ? "WRITE" : "READ";
}
private static void Register(int sig, SigAction* sa)
{
if (sigaction(sig, sa, null) != 0)
throw new InvalidOperationException($"sigaction({sig}) failed errno={Marshal.GetLastPInvokeError()}");
}
private static void OnSignal(int sig, SigInfo* info, void* uctx)
{
var cb = _UserCallback;
if (cb == null)
{
_exit(128 + sig);
return;
}
ulong fault = (ulong)(nuint)info->si_addr;
ulong rip = 0;
long err = 0;
long trapNo = 0;
TryGetContext(uctx, out rip, out err, out trapNo);
IntPtr modulePath = IntPtr.Zero;
ulong moduleBase = 0;
IntPtr symbolName = IntPtr.Zero;
ulong symbolAddr = 0;
if (rip != 0 && dladdr((IntPtr)(nuint)rip, out var di) != 0)
{
modulePath = di.dli_fname;
moduleBase = (ulong)(nuint)di.dli_fbase;
symbolName = di.dli_sname;
symbolAddr = (ulong)(nuint)di.dli_saddr;
}
cb(sig, fault, rip, modulePath, moduleBase, symbolName, symbolAddr, info->si_code, trapNo, err);
_exit(128 + sig);
}
private static void TryGetContext(void* uctx, out ulong rip, out long err, out long trapNo)
{
rip = 0;
err = 0;
trapNo = 0;
try
{
var ctx = (UContext64*)uctx;
rip = (ulong)ctx->uc_mcontext.gregs[REG_RIP];
err = ctx->uc_mcontext.gregs[REG_ERR];
trapNo = ctx->uc_mcontext.gregs[REG_TRAPNO];
}
catch
{
}
}
private readonly struct SignalEntry
{
public readonly LinuxSignal Flag;
public readonly int Number;
public SignalEntry(LinuxSignal flag, int number)
{
Flag = flag;
Number = number;
}
}
private static readonly SignalEntry[] SignalMap =
{
new(LinuxSignal.SIGILL, SIGILL),
new(LinuxSignal.SIGTRAP, SIGTRAP),
new(LinuxSignal.SIGBUS, SIGBUS),
new(LinuxSignal.SIGFPE, SIGFPE),
new(LinuxSignal.SIGSEGV, SIGSEGV),
new(LinuxSignal.SIGSYS, SIGSYS),
};
[StructLayout(LayoutKind.Sequential)]
private struct SigInfo
{
public int si_signo;
public int si_errno;
public int si_code;
public void* si_addr;
}
[StructLayout(LayoutKind.Sequential)]
private struct SigSet
{
public fixed ulong __val[16];
}
[StructLayout(LayoutKind.Sequential)]
private struct SigAction
{
public IntPtr sa_sigaction;
public SigSet sa_mask;
public int sa_flags;
public IntPtr sa_restorer;
}
[StructLayout(LayoutKind.Sequential)]
private struct DlInfo
{
public IntPtr dli_fname;
public IntPtr dli_fbase;
public IntPtr dli_sname;
public IntPtr dli_saddr;
}
[StructLayout(LayoutKind.Sequential)]
private struct StackT
{
public void* ss_sp;
public int ss_flags;
public nuint ss_size;
}
[StructLayout(LayoutKind.Sequential)]
private struct MContext64
{
public fixed long gregs[23];
}
[StructLayout(LayoutKind.Sequential)]
private struct UContext64
{
public ulong uc_flags;
public void* uc_link;
public StackT uc_stack;
public MContext64 uc_mcontext;
}
[DllImport("libc", SetLastError = true)]
private static extern int sigaction(int signum, SigAction* act, SigAction* oldact);
[DllImport("libc", SetLastError = true)]
private static extern int dladdr(IntPtr addr, out DlInfo info);
[DllImport("libc", SetLastError = true)]
private static extern void _exit(int status);
}
}