// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace SharpEmu.HLE;
///
/// Process-wide registry of ELF PT_TLS templates and per-thread dynamic thread
/// vectors. AMD64 uses TLS Variant II: startup/static module blocks precede the
/// thread pointer, while modules introduced after a thread was initialized are
/// represented by separately allocated DTV entries.
///
public static class GuestTlsTemplate
{
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
// modules can require more than one host page of Variant II static TLS;
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
public const ulong StartupStaticTlsReservation = 0x10000UL;
private static readonly object _gate = new();
private static readonly SortedDictionary _modules = new();
private static readonly Dictionary _threadDtvs = new();
private static ulong _staticTlsSize;
private static ulong _maximumAlignment = 1;
private static ulong _generation;
static GuestTlsTemplate()
{
RunTlsLayoutSelfChecks();
}
private sealed class ModuleTemplate
{
public required ulong ModuleId { get; init; }
public required byte[] InitImage { get; init; }
public required ulong MemorySize { get; init; }
public required ulong Alignment { get; init; }
public required ulong AlignmentBias { get; init; }
public required ulong StaticOffset { get; init; }
}
private sealed class ThreadDtv
{
public ulong Generation { get; set; }
public Dictionary Entries { get; } = new();
public nint DtvAllocationBase { get; set; }
public ulong DtvAddress { get; set; }
}
private sealed class DtvEntry
{
public required ulong Address { get; init; }
public nint AllocationBase { get; init; }
}
/// Main executable's initialized PT_TLS bytes.
public static byte[] InitImage
{
get
{
lock (_gate)
{
return _modules.TryGetValue(1, out var module)
? (byte[])module.InitImage.Clone()
: [];
}
}
}
/// Aligned size of the main executable's static TLS block.
public static ulong BlockSize
{
get
{
lock (_gate)
{
return _modules.TryGetValue(1, out var module)
? module.StaticOffset
: 0;
}
}
}
/// Main executable's PT_TLS alignment.
public static ulong Alignment
{
get
{
lock (_gate)
{
return _modules.TryGetValue(1, out var module)
? module.Alignment
: 1;
}
}
}
/// Total Variant II static TLS span required below the TCB.
public static ulong StaticTlsSize
{
get { lock (_gate) { return _staticTlsSize; } }
}
/// Largest PT_TLS alignment required by the registered modules.
public static ulong MaximumAlignment
{
get { lock (_gate) { return _maximumAlignment; } }
}
/// Current DTV generation, incremented for every registered module.
public static ulong Generation
{
get { lock (_gate) { return _generation; } }
}
///
/// Backwards-compatible main-module registration entry point.
///
public static void Set(ReadOnlySpan initImage, ulong memorySize, ulong alignment)
{
Reset();
RegisterModule(1, initImage, memorySize, alignment, alignmentBias: 0);
}
///
/// Registers one module's PT_TLS template and allocates its Variant II
/// static offset. Modules must be registered in loader module-ID order.
///
public static ulong RegisterModule(
ulong moduleId,
ReadOnlySpan initImage,
ulong memorySize,
ulong alignment,
ulong alignmentBias = 0)
{
if (moduleId == 0)
{
throw new ArgumentOutOfRangeException(nameof(moduleId));
}
var normalizedAlignment = alignment == 0 ? 1 : alignment;
if ((normalizedAlignment & (normalizedAlignment - 1)) != 0)
{
throw new InvalidDataException($"PT_TLS alignment 0x{alignment:X} is not a power of two.");
}
if (memorySize > int.MaxValue || (ulong)initImage.Length > memorySize)
{
throw new InvalidDataException("PT_TLS template size is invalid or exceeds the supported process limit.");
}
lock (_gate)
{
if (_modules.TryGetValue(moduleId, out var existing))
{
if (existing.MemorySize != memorySize ||
existing.Alignment != normalizedAlignment ||
existing.AlignmentBias != alignmentBias ||
!existing.InitImage.AsSpan().SequenceEqual(initImage))
{
throw new InvalidOperationException($"TLS module {moduleId} was registered with a different template.");
}
return existing.StaticOffset;
}
// FreeBSD/AMD64 Variant II: choose the smallest offset satisfying
// offset - previous >= size and (-offset) % align == p_vaddr % align.
var staticOffset = CalculateStaticOffset(
_staticTlsSize,
memorySize,
normalizedAlignment,
alignmentBias);
if (staticOffset > StartupStaticTlsReservation)
{
throw new InvalidOperationException(
$"Static TLS requires 0x{staticOffset:X} bytes, but startup maps only " +
$"0x{StartupStaticTlsReservation:X} bytes below the thread pointer.");
}
_modules.Add(moduleId, new ModuleTemplate
{
ModuleId = moduleId,
InitImage = initImage.ToArray(),
MemorySize = memorySize,
Alignment = normalizedAlignment,
AlignmentBias = alignmentBias,
StaticOffset = staticOffset,
});
_staticTlsSize = staticOffset;
_maximumAlignment = Math.Max(_maximumAlignment, normalizedAlignment);
_generation++;
return staticOffset;
}
}
/// Returns the static TP-relative block offset for a module.
public static bool TryGetStaticOffset(ulong moduleId, out ulong staticOffset)
{
lock (_gate)
{
if (_modules.TryGetValue(moduleId, out var module))
{
staticOffset = module.StaticOffset;
return true;
}
}
staticOffset = 0;
return false;
}
/// Clears all module templates and frees dynamic DTV blocks.
public static void Reset()
{
lock (_gate)
{
foreach (var dtv in _threadDtvs.Values)
{
FreeDynamicEntries(dtv);
}
_threadDtvs.Clear();
_modules.Clear();
_staticTlsSize = 0;
_maximumAlignment = 1;
_generation++;
}
}
///
/// Initializes every currently registered startup TLS block for a thread
/// and records their addresses in that thread's DTV.
///
public static void SeedThreadBlock(CpuContext context, ulong threadPointer)
{
ArgumentNullException.ThrowIfNull(context);
if (threadPointer == 0)
{
return;
}
lock (_gate)
{
if (_threadDtvs.Remove(threadPointer, out var oldDtv))
{
FreeDynamicEntries(oldDtv);
}
var dtv = new ThreadDtv();
_threadDtvs.Add(threadPointer, dtv);
foreach (var module in _modules.Values)
{
dtv.Entries[module.ModuleId] = CreateStaticOrDynamicEntry(context, threadPointer, module);
}
RebuildGuestDtv(context, threadPointer, dtv);
}
}
///
/// Implements the DTV lookup used by __tls_get_addr. Unknown module
/// IDs and offsets outside the module's TLS image are rejected with zero.
///
public static ulong ResolveAddress(CpuContext context, ulong moduleId, ulong offset)
{
ArgumentNullException.ThrowIfNull(context);
if (context.FsBase == 0 || moduleId == 0)
{
return 0;
}
lock (_gate)
{
if (!_modules.TryGetValue(moduleId, out var module) || offset >= module.MemorySize)
{
return 0;
}
if (!_threadDtvs.TryGetValue(context.FsBase, out var dtv))
{
dtv = new ThreadDtv();
_threadDtvs.Add(context.FsBase, dtv);
foreach (var startupModule in _modules.Values)
{
dtv.Entries[startupModule.ModuleId] = CreateStaticOrDynamicEntry(context, context.FsBase, startupModule);
}
RebuildGuestDtv(context, context.FsBase, dtv);
}
var entryAdded = false;
if (!dtv.Entries.TryGetValue(moduleId, out var entry))
{
// This module appeared after the thread's startup TLS layout was
// seeded. Model rtld's lazy DTV allocation instead of assuming
// unused space exists below the already-live thread pointer.
entry = CreateDynamicEntry(module);
dtv.Entries.Add(moduleId, entry);
entryAdded = true;
}
if (entryAdded || dtv.Generation != _generation)
{
RebuildGuestDtv(context, context.FsBase, dtv);
}
return checked(entry.Address + offset);
}
}
private static DtvEntry CreateStaticOrDynamicEntry(
CpuContext context,
ulong threadPointer,
ModuleTemplate module)
{
if (threadPointer >= module.StaticOffset)
{
var address = threadPointer - module.StaticOffset;
var zeroImage = module.MemorySize == 0 ? [] : new byte[(int)module.MemorySize];
if ((zeroImage.Length == 0 || context.Memory.TryWrite(address, zeroImage)) &&
(module.InitImage.Length == 0 || context.Memory.TryWrite(address, module.InitImage)))
{
return new DtvEntry { Address = address };
}
}
return CreateDynamicEntry(module);
}
private static DtvEntry CreateDynamicEntry(ModuleTemplate module)
{
var size = Math.Max(1UL, module.MemorySize);
var allocationSize = checked(size + module.Alignment - 1);
if (allocationSize > int.MaxValue)
{
throw new OutOfMemoryException("TLS module allocation exceeds the supported host allocation size.");
}
var allocationBase = Marshal.AllocHGlobal((int)allocationSize);
var alignedAddress = AlignUp(unchecked((ulong)allocationBase), module.Alignment);
Marshal.Copy(new byte[(int)size], 0, unchecked((nint)alignedAddress), (int)size);
if (module.InitImage.Length != 0)
{
Marshal.Copy(module.InitImage, 0, unchecked((nint)alignedAddress), module.InitImage.Length);
}
return new DtvEntry
{
Address = alignedAddress,
AllocationBase = allocationBase,
};
}
private static void FreeDynamicEntries(ThreadDtv dtv)
{
foreach (var entry in dtv.Entries.Values)
{
if (entry.AllocationBase != 0)
{
Marshal.FreeHGlobal(entry.AllocationBase);
}
}
if (dtv.DtvAllocationBase != 0)
{
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
dtv.DtvAllocationBase = 0;
dtv.DtvAddress = 0;
}
}
private static void RebuildGuestDtv(CpuContext context, ulong threadPointer, ThreadDtv dtv)
{
var maximumModuleId = _modules.Count == 0 ? 0UL : _modules.Keys.Max();
var byteSize = checked(2UL * sizeof(ulong) + maximumModuleId * sizeof(ulong));
if (byteSize > int.MaxValue)
{
throw new OutOfMemoryException("Guest DTV exceeds the supported host allocation size.");
}
var newAllocation = Marshal.AllocHGlobal((int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
var newAddress = unchecked((ulong)newAllocation);
Marshal.Copy(new byte[(int)Math.Max((ulong)sizeof(ulong) * 2, byteSize)], 0, newAllocation, (int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
Marshal.WriteInt64(newAllocation, 0, unchecked((long)_generation));
Marshal.WriteInt64(newAllocation, sizeof(ulong), unchecked((long)maximumModuleId));
foreach (var (moduleId, entry) in dtv.Entries)
{
var slotOffset = checked((int)(2UL * sizeof(ulong) + (moduleId - 1) * sizeof(ulong)));
Marshal.WriteInt64(newAllocation, slotOffset, unchecked((long)entry.Address));
}
if (!context.TryWriteUInt64(threadPointer + sizeof(ulong), newAddress))
{
Marshal.FreeHGlobal(newAllocation);
throw new InvalidOperationException("Failed to install the guest DTV pointer in the thread control block.");
}
if (dtv.DtvAllocationBase != 0)
{
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
}
dtv.DtvAllocationBase = newAllocation;
dtv.DtvAddress = newAddress;
dtv.Generation = _generation;
}
private static ulong AlignUp(ulong value, ulong alignment)
{
var mask = alignment - 1;
return checked((value + mask) & ~mask);
}
private static ulong CalculateStaticOffset(
ulong previousOffset,
ulong size,
ulong alignment,
ulong alignmentBias)
{
var result = checked(previousOffset + size + alignment - 1);
return result - ((result + alignmentBias) & (alignment - 1));
}
[Conditional("DEBUG")]
private static void RunTlsLayoutSelfChecks()
{
var firstOffset = RegisterModule(1, [0x11, 0x22], 0x20, 0x10, 0);
var secondOffset = RegisterModule(2, [0x7A], 0x18, 0x20, 8);
Debug.Assert(firstOffset == 0x20, "First Variant II TLS offset is incorrect.");
Debug.Assert(secondOffset >= firstOffset + 0x18, "TLS modules overlap in the static layout.");
Debug.Assert((unchecked(0UL - secondOffset) & 0x1F) == 8, "PT_TLS virtual-address congruence was not preserved.");
var lateEntry = CreateDynamicEntry(_modules[2]);
try
{
Debug.Assert(lateEntry.AllocationBase != 0, "A late TLS module did not receive a dynamic DTV block.");
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address)) == 0x7A, "Late-module tdata was not initialized.");
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address + 1)) == 0, "Late-module tbss was not zero initialized.");
}
finally
{
Marshal.FreeHGlobal(lateEntry.AllocationBase);
Reset();
}
}
}