diff --git a/docs/adaptive-game-compatibility.md b/docs/adaptive-game-compatibility.md index 895fcb4..6ae70e1 100644 --- a/docs/adaptive-game-compatibility.md +++ b/docs/adaptive-game-compatibility.md @@ -18,6 +18,10 @@ Core HUD compatibility is required first. Tune and direct native gauge readers a Both platforms require typed code witnesses for all 18 direct-gauge fields. Steam uses the independently verified common field-reader code in an isolated Store reference namespace: complete initializer and getter code, exact property names, member offsets, type calls and the complete type function must all match. Only a verified single jump alias is accepted. This does not copy Store addresses into a Steam reader. +The field reader code carries the game's thread-local storage slot offset as an immediate operand (`mov edx, imm32` after the `gs:[0x58]` access). That offset is recompiled per build: 0x420 through 6.461.691.0 and 3.461.691.0, 0x42C from 6.483.970.0 and 3.483.970.0, where it rejected the direct gauge on every PC. The type function template and the 18 initializer templates declare it as a `threadLocalSlot` operand: it is masked like an address operand, it has no target, and every matched reader must carry the same 4-byte-aligned value below 64 KB. Getters do not contain it, and their member displacements are still compared byte for byte. + +A reference that two templates share may reach its target through a single 5-byte jump thunk on one side and directly on the other (Store 3.483.970.0 routes one Tune guard's call to a helper that the gauge templates call directly through such a thunk). Both shared-reference checks accept exactly that case and keep the helper itself; the thunk bytes join the proof. Any other disagreement between shared references is still rejected. + Steam's secondary HUD table is linked to the already verified main HUD table through the same complete-object RTTI type and hierarchy. Its subobject offset, full inheritance topology and slot code must still match, and the candidate must be unique. Table spacing and numeric RTTI labels are not treated as stable identities. This relationship was checked in both retained versions of both platforms. ## Drift angle and scoring guidance diff --git a/src/Wisp.App/NativeCompatibility/NativeAdaptiveFieldVerifier.cs b/src/Wisp.App/NativeCompatibility/NativeAdaptiveFieldVerifier.cs index 55ffe9e..67376e3 100644 --- a/src/Wisp.App/NativeCompatibility/NativeAdaptiveFieldVerifier.cs +++ b/src/Wisp.App/NativeCompatibility/NativeAdaptiveFieldVerifier.cs @@ -59,6 +59,7 @@ private static void ValidateCore(NativeAdaptiveImage image, JsonElement storePro var selected = new List(); var relationships = new Dictionary(); var typeReferences = new HashSet(); + var threadLocalSlots = new HashSet(); var typeSlot = checked(childVtableRva + 24); if (typeSlot % 8 != 0 || image.Kind(typeSlot, 8) != "readonly") throw new InvalidDataException("type-slot"); @@ -76,7 +77,7 @@ private static void ValidateCore(NativeAdaptiveImage image, JsonElement storePro } var identity = new TypeIdentity(typeEntry, canonicalType, hasAlias); if (!Match(image, typeBody, canonicalType)) throw new InvalidDataException("type-body-template-changed"); - BindSignature(image, typeBody, canonicalType, identity, typeReferences, relationships, selected); + BindSignature(image, typeBody, canonicalType, identity, typeReferences, relationships, selected, threadLocalSlots); image.Remember(typeSlot, image.Bytes(typeSlot, 8)); if (hasAlias) image.Remember(typeEntry, entry); image.Remember(canonicalType, image.Bytes(canonicalType, typeBody.Length)); @@ -120,8 +121,8 @@ private static void ValidateCore(NativeAdaptiveImage image, JsonElement storePro if (candidates.Count != 1) throw new InvalidDataException(candidates.Count == 0 ? "no-complete-typed-match" : "ambiguous-complete-typed-match"); var choice = candidates[0]; - BindSignature(image, initializer, choice.Initializer, identity, typeReferences, relationships, selected); - BindSignature(image, getter, choice.Getter, identity, typeReferences, relationships, selected); + BindSignature(image, initializer, choice.Initializer, identity, typeReferences, relationships, selected, threadLocalSlots); + BindSignature(image, getter, choice.Getter, identity, typeReferences, relationships, selected, threadLocalSlots); image.Remember(choice.Initializer, image.Bytes(choice.Initializer, initializer.Length)); image.Remember(choice.Getter, image.Bytes(choice.Getter, getter.Length)); image.Remember(choice.Name, image.Bytes(choice.Name, expectedName.Length)); @@ -131,13 +132,18 @@ private static void ValidateCore(NativeAdaptiveImage image, JsonElement storePro if (previous >= range.Signature.ReferenceRva && previous - range.Signature.ReferenceRva < range.Signature.Length && current != checked(range.Rva + previous - range.Signature.ReferenceRva)) throw new InvalidDataException("template-interior-reference-conflict"); + // The game's thread-local slot offset moves between builds; it must still be one value for the + // type function and every field initializer. + if (threadLocalSlots.Count > 1) throw new InvalidDataException("thread-slot-inconsistent"); + if (threadLocalSlots.Any(slot => slot % 4 != 0 || slot >= NativeAdaptiveResolver.ThreadLocalSlotLimit)) + throw new InvalidDataException("thread-slot-bounds"); image.CheckBudget(); } private static Relocation Operand(Signature signature, int offset) => signature.Relocations.Single(r => r.Offset == offset); private static void BindSignature(NativeAdaptiveImage image, Signature signature, uint rva, TypeIdentity identity, - HashSet typeReferences, Dictionary relationships, List selected) + HashSet typeReferences, Dictionary relationships, List selected, HashSet threadLocalSlots) { void Bind(uint previous, uint current) { @@ -147,11 +153,21 @@ void Bind(uint previous, uint current) current = identity.Canonical; } if (relationships.TryGetValue(previous, out var existing) && existing != current) + { + // The same single-jump-alias rule as the resolver: one side may reach a shared helper through a thunk. + if (NativeAdaptiveResolver.JumpAliasTarget(image, existing) == current) { relationships[previous] = current; return; } + if (NativeAdaptiveResolver.JumpAliasTarget(image, current) == existing) return; throw new InvalidDataException("template-shared-reference-conflict"); + } relationships[previous] = current; } Bind(signature.ReferenceRva, rva); - foreach (var relocation in signature.Relocations) Bind(relocation.ReferenceTarget, Target(image, signature, rva, relocation)); + foreach (var relocation in signature.Relocations) + { + if (relocation.Kind == NativeAdaptiveResolver.ThreadLocalSlotKind) + threadLocalSlots.Add(BinaryPrimitives.ReadUInt32LittleEndian(image.Bytes(checked(rva + (uint)relocation.Offset), 4))); + else Bind(relocation.ReferenceTarget, Target(image, signature, rva, relocation)); + } selected.Add(new Selected(signature, rva)); } @@ -198,6 +214,11 @@ private static bool Match(NativeAdaptiveImage image, Signature signature, uint r var normalized = image.Bytes(rva, signature.Length).ToArray(); foreach (var relocation in signature.Relocations) { + if (relocation.Kind == NativeAdaptiveResolver.ThreadLocalSlotKind) + { + normalized.AsSpan(relocation.Offset, relocation.Width).Clear(); + continue; + } uint target; try { target = Target(image, signature, rva, relocation); } catch (Exception e) when (e is InvalidDataException or OverflowException) { return false; } @@ -241,8 +262,11 @@ internal Signature(JsonElement value) foreach (var r in Relocations) { if (r.Width is not (1 or 4 or 8) || r.Offset < 0 || r.Offset > Length - r.Width || - r.Kind is not ("relative" or "absolute" or "image") || r.Kind == "relative" && (r.Width == 8 || r.Next < r.Offset + r.Width || r.Next > Length) || - r.Kind == "absolute" && r.Width != 8 || r.Kind == "image" && r.Width != 4 || r.Section is not ("code" or "readonly" or "data" or "image")) + r.Kind is not ("relative" or "absolute" or "image" or NativeAdaptiveResolver.ThreadLocalSlotKind) || + r.Kind == "relative" && (r.Width == 8 || r.Next < r.Offset + r.Width || r.Next > Length) || + r.Kind == "absolute" && r.Width != 8 || r.Kind == "image" && r.Width != 4 || + r.Kind == NativeAdaptiveResolver.ThreadLocalSlotKind && (r.Width != 4 || r.Section != "none") || + r.Kind != NativeAdaptiveResolver.ThreadLocalSlotKind && r.Section is not ("code" or "readonly" or "data" or "image")) throw new InvalidDataException("template-relocation-bounds"); for (var i = r.Offset; i < r.Offset + r.Width; i++) { @@ -280,12 +304,14 @@ private static FieldSpec[] Specifications(NativeGaugeLayout g) // Complete old Store type body; independently matched both current Store // and captured Steam bodies after declared relocation normalization. // All field and type templates share this isolated OLD Store namespace. + // Offset 34 is the game's thread-local slot immediate (0x420 through 6.461/3.461, + // 0x42C from 6.483/3.483); it is masked and checked for consistency instead. private static JsonDocument ReadTypeBodyTemplate() { return JsonDocument.Parse(TypeBodyTemplate); } private const string TypeBodyTemplate = """ -{"referenceRva":58668368,"length":505,"normalizedSha256":"96C92ABE8C4B1E06321A753EAC44A2BD52E4112C7BB873572313D42D91918641","anchorHex":"48895C24104889742418574881EC80","anchorOffset":0,"section":"code","relocations":[{"offset":20,"width":4,"next":24,"kind":"relative","targetRva":180139504,"expectedTargetSection":"data","internalOffset":null},{"offset":49,"width":4,"next":53,"kind":"relative","targetRva":177820000,"expectedTargetSection":"data","internalOffset":null},{"offset":55,"width":4,"next":59,"kind":"relative","targetRva":58668586,"expectedTargetSection":"code","internalOffset":218},{"offset":62,"width":4,"next":66,"kind":"relative","targetRva":177819824,"expectedTargetSection":"data","internalOffset":null},{"offset":91,"width":4,"next":95,"kind":"relative","targetRva":104645616,"expectedTargetSection":"readonly","internalOffset":null},{"offset":130,"width":4,"next":134,"kind":"relative","targetRva":98646732,"expectedTargetSection":"code","internalOffset":null},{"offset":146,"width":4,"next":150,"kind":"relative","targetRva":3864832,"expectedTargetSection":"code","internalOffset":null},{"offset":179,"width":4,"next":183,"kind":"relative","targetRva":177819824,"expectedTargetSection":"data","internalOffset":null},{"offset":184,"width":4,"next":188,"kind":"relative","targetRva":61564832,"expectedTargetSection":"code","internalOffset":null},{"offset":191,"width":4,"next":195,"kind":"relative","targetRva":103282304,"expectedTargetSection":"code","internalOffset":null},{"offset":196,"width":4,"next":200,"kind":"relative","targetRva":94177100,"expectedTargetSection":"code","internalOffset":null},{"offset":204,"width":4,"next":208,"kind":"relative","targetRva":177820000,"expectedTargetSection":"data","internalOffset":null},{"offset":209,"width":4,"next":213,"kind":"relative","targetRva":94177304,"expectedTargetSection":"code","internalOffset":null},{"offset":214,"width":4,"next":218,"kind":"relative","targetRva":58668427,"expectedTargetSection":"code","internalOffset":59},{"offset":221,"width":4,"next":225,"kind":"relative","targetRva":177820000,"expectedTargetSection":"data","internalOffset":null},{"offset":226,"width":4,"next":230,"kind":"relative","targetRva":94177412,"expectedTargetSection":"code","internalOffset":null},{"offset":232,"width":4,"next":237,"kind":"relative","targetRva":177820000,"expectedTargetSection":"data","internalOffset":null},{"offset":239,"width":4,"next":243,"kind":"relative","targetRva":58668427,"expectedTargetSection":"code","internalOffset":59},{"offset":246,"width":4,"next":250,"kind":"relative","targetRva":58144224,"expectedTargetSection":"code","internalOffset":null},{"offset":276,"width":4,"next":280,"kind":"relative","targetRva":3864640,"expectedTargetSection":"code","internalOffset":null},{"offset":289,"width":4,"next":293,"kind":"relative","targetRva":104645664,"expectedTargetSection":"readonly","internalOffset":null},{"offset":302,"width":4,"next":306,"kind":"relative","targetRva":114154208,"expectedTargetSection":"readonly","internalOffset":null},{"offset":311,"width":4,"next":315,"kind":"relative","targetRva":114154224,"expectedTargetSection":"readonly","internalOffset":null},{"offset":325,"width":4,"next":329,"kind":"relative","targetRva":177825984,"expectedTargetSection":"data","internalOffset":null},{"offset":333,"width":4,"next":337,"kind":"relative","targetRva":177826016,"expectedTargetSection":"data","internalOffset":null},{"offset":338,"width":1,"next":339,"kind":"relative","targetRva":58668766,"expectedTargetSection":"code","internalOffset":398},{"offset":342,"width":4,"next":346,"kind":"relative","targetRva":177826016,"expectedTargetSection":"data","internalOffset":null},{"offset":347,"width":4,"next":351,"kind":"relative","targetRva":94177412,"expectedTargetSection":"code","internalOffset":null},{"offset":353,"width":4,"next":358,"kind":"relative","targetRva":177826016,"expectedTargetSection":"data","internalOffset":null},{"offset":359,"width":1,"next":360,"kind":"relative","targetRva":58668766,"expectedTargetSection":"code","internalOffset":398},{"offset":369,"width":4,"next":373,"kind":"relative","targetRva":61733360,"expectedTargetSection":"code","internalOffset":null},{"offset":376,"width":4,"next":380,"kind":"relative","targetRva":103275760,"expectedTargetSection":"code","internalOffset":null},{"offset":381,"width":4,"next":385,"kind":"relative","targetRva":94177100,"expectedTargetSection":"code","internalOffset":null},{"offset":389,"width":4,"next":393,"kind":"relative","targetRva":177826016,"expectedTargetSection":"data","internalOffset":null},{"offset":394,"width":4,"next":398,"kind":"relative","targetRva":94177304,"expectedTargetSection":"code","internalOffset":null},{"offset":401,"width":4,"next":406,"kind":"relative","targetRva":177826008,"expectedTargetSection":"data","internalOffset":null},{"offset":410,"width":4,"next":414,"kind":"relative","targetRva":177825984,"expectedTargetSection":"data","internalOffset":null},{"offset":425,"width":4,"next":429,"kind":"relative","targetRva":58668455,"expectedTargetSection":"code","internalOffset":87},{"offset":448,"width":1,"next":449,"kind":"relative","targetRva":58668862,"expectedTargetSection":"code","internalOffset":494},{"offset":469,"width":1,"next":470,"kind":"relative","targetRva":58668862,"expectedTargetSection":"code","internalOffset":494},{"offset":489,"width":4,"next":493,"kind":"relative","targetRva":104291080,"expectedTargetSection":"readonly","internalOffset":null},{"offset":495,"width":4,"next":499,"kind":"relative","targetRva":3870928,"expectedTargetSection":"code","internalOffset":null},{"offset":500,"width":4,"next":504,"kind":"relative","targetRva":58668455,"expectedTargetSection":"code","internalOffset":87}]} +{"referenceRva":58668368,"length":505,"normalizedSha256":"99CE202773165E54B18CEDB69D9E0626223AB757F86F308014D087E822D6E37E","anchorHex":"48895C24104889742418574881EC80","anchorOffset":0,"section":"code","relocations":[{"offset":20,"width":4,"next":24,"kind":"relative","targetRva":180139504,"expectedTargetSection":"data","internalOffset":null},{"offset":34,"width":4,"next":38,"kind":"threadLocalSlot","targetRva":0,"expectedTargetSection":"none","internalOffset":null},{"offset":49,"width":4,"next":53,"kind":"relative","targetRva":177820000,"expectedTargetSection":"data","internalOffset":null},{"offset":55,"width":4,"next":59,"kind":"relative","targetRva":58668586,"expectedTargetSection":"code","internalOffset":218},{"offset":62,"width":4,"next":66,"kind":"relative","targetRva":177819824,"expectedTargetSection":"data","internalOffset":null},{"offset":91,"width":4,"next":95,"kind":"relative","targetRva":104645616,"expectedTargetSection":"readonly","internalOffset":null},{"offset":130,"width":4,"next":134,"kind":"relative","targetRva":98646732,"expectedTargetSection":"code","internalOffset":null},{"offset":146,"width":4,"next":150,"kind":"relative","targetRva":3864832,"expectedTargetSection":"code","internalOffset":null},{"offset":179,"width":4,"next":183,"kind":"relative","targetRva":177819824,"expectedTargetSection":"data","internalOffset":null},{"offset":184,"width":4,"next":188,"kind":"relative","targetRva":61564832,"expectedTargetSection":"code","internalOffset":null},{"offset":191,"width":4,"next":195,"kind":"relative","targetRva":103282304,"expectedTargetSection":"code","internalOffset":null},{"offset":196,"width":4,"next":200,"kind":"relative","targetRva":94177100,"expectedTargetSection":"code","internalOffset":null},{"offset":204,"width":4,"next":208,"kind":"relative","targetRva":177820000,"expectedTargetSection":"data","internalOffset":null},{"offset":209,"width":4,"next":213,"kind":"relative","targetRva":94177304,"expectedTargetSection":"code","internalOffset":null},{"offset":214,"width":4,"next":218,"kind":"relative","targetRva":58668427,"expectedTargetSection":"code","internalOffset":59},{"offset":221,"width":4,"next":225,"kind":"relative","targetRva":177820000,"expectedTargetSection":"data","internalOffset":null},{"offset":226,"width":4,"next":230,"kind":"relative","targetRva":94177412,"expectedTargetSection":"code","internalOffset":null},{"offset":232,"width":4,"next":237,"kind":"relative","targetRva":177820000,"expectedTargetSection":"data","internalOffset":null},{"offset":239,"width":4,"next":243,"kind":"relative","targetRva":58668427,"expectedTargetSection":"code","internalOffset":59},{"offset":246,"width":4,"next":250,"kind":"relative","targetRva":58144224,"expectedTargetSection":"code","internalOffset":null},{"offset":276,"width":4,"next":280,"kind":"relative","targetRva":3864640,"expectedTargetSection":"code","internalOffset":null},{"offset":289,"width":4,"next":293,"kind":"relative","targetRva":104645664,"expectedTargetSection":"readonly","internalOffset":null},{"offset":302,"width":4,"next":306,"kind":"relative","targetRva":114154208,"expectedTargetSection":"readonly","internalOffset":null},{"offset":311,"width":4,"next":315,"kind":"relative","targetRva":114154224,"expectedTargetSection":"readonly","internalOffset":null},{"offset":325,"width":4,"next":329,"kind":"relative","targetRva":177825984,"expectedTargetSection":"data","internalOffset":null},{"offset":333,"width":4,"next":337,"kind":"relative","targetRva":177826016,"expectedTargetSection":"data","internalOffset":null},{"offset":338,"width":1,"next":339,"kind":"relative","targetRva":58668766,"expectedTargetSection":"code","internalOffset":398},{"offset":342,"width":4,"next":346,"kind":"relative","targetRva":177826016,"expectedTargetSection":"data","internalOffset":null},{"offset":347,"width":4,"next":351,"kind":"relative","targetRva":94177412,"expectedTargetSection":"code","internalOffset":null},{"offset":353,"width":4,"next":358,"kind":"relative","targetRva":177826016,"expectedTargetSection":"data","internalOffset":null},{"offset":359,"width":1,"next":360,"kind":"relative","targetRva":58668766,"expectedTargetSection":"code","internalOffset":398},{"offset":369,"width":4,"next":373,"kind":"relative","targetRva":61733360,"expectedTargetSection":"code","internalOffset":null},{"offset":376,"width":4,"next":380,"kind":"relative","targetRva":103275760,"expectedTargetSection":"code","internalOffset":null},{"offset":381,"width":4,"next":385,"kind":"relative","targetRva":94177100,"expectedTargetSection":"code","internalOffset":null},{"offset":389,"width":4,"next":393,"kind":"relative","targetRva":177826016,"expectedTargetSection":"data","internalOffset":null},{"offset":394,"width":4,"next":398,"kind":"relative","targetRva":94177304,"expectedTargetSection":"code","internalOffset":null},{"offset":401,"width":4,"next":406,"kind":"relative","targetRva":177826008,"expectedTargetSection":"data","internalOffset":null},{"offset":410,"width":4,"next":414,"kind":"relative","targetRva":177825984,"expectedTargetSection":"data","internalOffset":null},{"offset":425,"width":4,"next":429,"kind":"relative","targetRva":58668455,"expectedTargetSection":"code","internalOffset":87},{"offset":448,"width":1,"next":449,"kind":"relative","targetRva":58668862,"expectedTargetSection":"code","internalOffset":494},{"offset":469,"width":1,"next":470,"kind":"relative","targetRva":58668862,"expectedTargetSection":"code","internalOffset":494},{"offset":489,"width":4,"next":493,"kind":"relative","targetRva":104291080,"expectedTargetSection":"readonly","internalOffset":null},{"offset":495,"width":4,"next":499,"kind":"relative","targetRva":3870928,"expectedTargetSection":"code","internalOffset":null},{"offset":500,"width":4,"next":504,"kind":"relative","targetRva":58668455,"expectedTargetSection":"code","internalOffset":87}]} """; } diff --git a/src/Wisp.App/NativeCompatibility/NativeAdaptiveResolver.cs b/src/Wisp.App/NativeCompatibility/NativeAdaptiveResolver.cs index aa63b66..dcd596a 100644 --- a/src/Wisp.App/NativeCompatibility/NativeAdaptiveResolver.cs +++ b/src/Wisp.App/NativeCompatibility/NativeAdaptiveResolver.cs @@ -10,6 +10,10 @@ namespace Wisp.App; // Only address operands may move; opcodes, member offsets and constants must match. internal sealed class NativeAdaptiveResolver { + // A thread-local storage slot offset the game recompiles into its field readers. It is masked + // like an address operand, but it has no target; every reader must carry the same value. + internal const string ThreadLocalSlotKind = "threadLocalSlot"; + internal const uint ThreadLocalSlotLimit = 0x10000; private readonly NativeAdaptiveImage _image; private readonly JsonElement _profile; private readonly Dictionary _signatures; @@ -265,6 +269,11 @@ private bool Match(Signature signature, uint rva, bool remember) var normalized = bytes.ToArray(); foreach (var relocation in signature.Relocations) { + if (relocation.Kind == ThreadLocalSlotKind) + { + normalized.AsSpan(relocation.Offset, relocation.Width).Clear(); + continue; + } uint target; try { target = Target(signature, rva, relocation); } catch (Exception e) when (e is InvalidDataException or OverflowException) { return false; } @@ -281,7 +290,7 @@ private bool Match(Signature signature, uint rva, bool remember) _matched[signature.Id] = rva; BindRelationship(signature.ReferenceRva, rva); foreach (var relocation in signature.Relocations) - BindRelationship(relocation.TargetRva, Target(signature, rva, relocation)); + if (relocation.Kind != ThreadLocalSlotKind) BindRelationship(relocation.TargetRva, Target(signature, rva, relocation)); } return true; } @@ -289,10 +298,29 @@ private bool Match(Signature signature, uint rva, bool remember) private void BindRelationship(uint previous, uint current) { if (_relationships.TryGetValue(previous, out var existing) && existing != current) + { + // A build may route one call site of a shared helper through a single jump thunk. The + // reference then names the helper through the thunk on one side and directly on the + // other; keep the helper itself. Anything else is a disagreement. + if (JumpAliasTarget(_image, existing) == current) { _relationships[previous] = current; return; } + if (JumpAliasTarget(_image, current) == existing) return; throw new InvalidDataException("Adaptive references disagree"); + } _relationships[previous] = current; } + // The target of a 5-byte relative jump at rva, when both ends are code; the thunk bytes join the proof. + internal static uint? JumpAliasTarget(NativeAdaptiveImage image, uint rva) + { + if (image.Kind(rva, 5) != "code") return null; + var bytes = image.Bytes(rva, 5); + if (bytes[0] != 0xE9) return null; + var target = (long)rva + 5 + BinaryPrimitives.ReadInt32LittleEndian(bytes[1..]); + if (target <= 0 || target >= image.ImageSize || image.Kind((uint)target, 1) != "code") return null; + image.Remember(rva, bytes); + return (uint)target; + } + private void ValidateRelationships() { // References to the middle of a guarded instruction range must move with @@ -614,6 +642,7 @@ private void ValidateFields() throw new InvalidDataException("Adaptive native gauge fields could not be verified"); } var required = capability.GetProperty("requiredFieldWitnessPaths").EnumerateArray().Select(p => p.GetString()!).ToHashSet(StringComparer.Ordinal); + var threadLocalSlots = new HashSet(); foreach (var field in fields.EnumerateArray()) { var path = field.GetProperty("path").GetString()!; @@ -642,8 +671,20 @@ private void ValidateFields() Match(getter, Target(initializer, selected, getterOperand), remember: true); _image.Remember(Target(initializer, selected, nameOperand), name); _image.Pointer(typeSlot, proof: true); + threadLocalSlots.UnionWith(ThreadLocalSlots(initializer, selected)); } if (required.Count != 0) throw new InvalidDataException("Adaptive field witness is missing"); + CheckThreadLocalSlots(threadLocalSlots); + } + + private IEnumerable ThreadLocalSlots(Signature signature, uint rva) => + signature.Relocations.Where(r => r.Kind == ThreadLocalSlotKind) + .Select(r => BinaryPrimitives.ReadUInt32LittleEndian(_image.Bytes(checked(rva + (uint)r.Offset), 4))); + + internal static void CheckThreadLocalSlots(IReadOnlySet slots) + { + if (slots.Count > 1) throw new InvalidDataException("Adaptive thread-local slot differs between field readers"); + if (slots.Any(slot => slot % 4 != 0 || slot >= ThreadLocalSlotLimit)) throw new InvalidDataException("Adaptive thread-local slot bounds"); } private static void Set(JsonObject root, string path, JsonNode? value) @@ -675,7 +716,8 @@ internal Signature(JsonElement value) if (Length is < 1 or > 8192 || Hash.Length != 32 || AnchorOffset < 0 || AnchorOffset > Length - Anchor.Length || CanDiscover && Anchor.Length < 4 || Relocations.Any(r => r.Width is not (1 or 4 or 8) || r.Offset < 0 || r.Offset > Length - r.Width || r.Kind == "relative" && (r.Next < r.Offset + r.Width || r.Next > Length) || - r.Kind == "absolute" && r.Width != 8 || r.Kind == "image" && r.Width != 4 || r.Kind == "relative" && r.Width == 8)) + r.Kind == "absolute" && r.Width != 8 || r.Kind == "image" && r.Width != 4 || r.Kind == "relative" && r.Width == 8 || + r.Kind == ThreadLocalSlotKind && r.Width != 4)) throw new InvalidDataException("Adaptive signature bounds"); var occupied = new bool[Length]; foreach (var relocation in Relocations) diff --git a/src/Wisp.App/NativeCompatibility/adaptive-store.json b/src/Wisp.App/NativeCompatibility/adaptive-store.json index beecf72..6870bad 100644 --- a/src/Wisp.App/NativeCompatibility/adaptive-store.json +++ b/src/Wisp.App/NativeCompatibility/adaptive-store.json @@ -63719,7 +63719,7 @@ "id": "code-0380A750-261", "referenceRva": 58763088, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -63733,6 +63733,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -64125,7 +64134,7 @@ "id": "code-0380B520-261", "referenceRva": 58766624, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -64139,6 +64148,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -64495,7 +64513,7 @@ "id": "code-0380B630-261", "referenceRva": 58766896, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -64509,6 +64527,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -64865,7 +64892,7 @@ "id": "code-0380B740-261", "referenceRva": 58767168, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -64879,6 +64906,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -65235,7 +65271,7 @@ "id": "code-0380B850-261", "referenceRva": 58767440, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -65249,6 +65285,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -65605,7 +65650,7 @@ "id": "code-0380D880-261", "referenceRva": 58775680, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -65619,6 +65664,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -65975,7 +66029,7 @@ "id": "code-0380D990-261", "referenceRva": 58775952, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -65989,6 +66043,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -66345,7 +66408,7 @@ "id": "code-0380DEE0-261", "referenceRva": 58777312, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -66359,6 +66422,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -66715,7 +66787,7 @@ "id": "code-0380E0F0-261", "referenceRva": 58777840, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -66729,6 +66801,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -67085,7 +67166,7 @@ "id": "code-0380E200-252", "referenceRva": 58778112, "length": 252, - "normalizedSha256": "4BF969725E2C6AF25E63F08E050946D25988532066FEA7BB77C490EFC7E9DA0A", + "normalizedSha256": "4F35BCDB6AFA548C5E06E0A08C3ED5E8C6994898FDD18E2221BFA7AE414CA142", "anchorOffset": 143, "anchorHex": "33C0488905", "section": "code", @@ -67099,6 +67180,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -67446,7 +67536,7 @@ "id": "code-0380F1B0-261", "referenceRva": 58782128, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -67460,6 +67550,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -67816,7 +67915,7 @@ "id": "code-0380F2C0-261", "referenceRva": 58782400, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -67830,6 +67929,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -68186,7 +68294,7 @@ "id": "code-0380F3D0-261", "referenceRva": 58782672, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -68200,6 +68308,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -68556,7 +68673,7 @@ "id": "code-0380F4E0-261", "referenceRva": 58782944, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -68570,6 +68687,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -68962,7 +69088,7 @@ "id": "code-0380F5F0-261", "referenceRva": 58783216, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -68976,6 +69102,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -69368,7 +69503,7 @@ "id": "code-0380F700-261", "referenceRva": 58783488, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -69382,6 +69517,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -69774,7 +69918,7 @@ "id": "code-0380F910-261", "referenceRva": 58784016, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -69788,6 +69932,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, @@ -70144,7 +70297,7 @@ "id": "code-03810B90-261", "referenceRva": 58788752, "length": 261, - "normalizedSha256": "37037C8FB2480AF44B36E6EC64DFFB56DBA25A2B8C86627922B7BE795A254352", + "normalizedSha256": "30F63D48BED9BDD718FDCAA910610F3C8A579C728DFCDD99808E2B1F7D0CB75B", "anchorOffset": 0, "anchorHex": "48895C24084889742410574883EC208B0D", "section": "code", @@ -70158,6 +70311,15 @@ "expectedTargetSection": "data", "internalOffset": null }, + { + "offset": 31, + "width": 4, + "next": 35, + "kind": "threadLocalSlot", + "targetRva": 0, + "expectedTargetSection": "none", + "internalOffset": null + }, { "offset": 44, "width": 4, diff --git a/tests/Wisp.App.Tests/NativeAdaptiveCompatibilityTests.cs b/tests/Wisp.App.Tests/NativeAdaptiveCompatibilityTests.cs index 62b9738..940b5d9 100644 --- a/tests/Wisp.App.Tests/NativeAdaptiveCompatibilityTests.cs +++ b/tests/Wisp.App.Tests/NativeAdaptiveCompatibilityTests.cs @@ -10,6 +10,16 @@ namespace Wisp.App.Tests; public sealed class NativeAdaptiveCompatibilityTests { + [Fact] + public void ThreadLocalSlotsMustAgreeAndStayBounded() + { + NativeAdaptiveResolver.CheckThreadLocalSlots(new HashSet()); + NativeAdaptiveResolver.CheckThreadLocalSlots(new HashSet { 0x42c }); + Assert.Throws(() => NativeAdaptiveResolver.CheckThreadLocalSlots(new HashSet { 0x420, 0x42c })); + Assert.Throws(() => NativeAdaptiveResolver.CheckThreadLocalSlots(new HashSet { 0x422 })); + Assert.Throws(() => NativeAdaptiveResolver.CheckThreadLocalSlots(new HashSet { 0x10000 })); + } + private static int _nextProcess = 600_000; [Theory] diff --git a/tests/Wisp.App.Tests/NativeAdaptiveFieldVerifierTests.cs b/tests/Wisp.App.Tests/NativeAdaptiveFieldVerifierTests.cs index fd72aee..9045ebc 100644 --- a/tests/Wisp.App.Tests/NativeAdaptiveFieldVerifierTests.cs +++ b/tests/Wisp.App.Tests/NativeAdaptiveFieldVerifierTests.cs @@ -92,6 +92,21 @@ public void SharedTemplateReferencesCannotResolveToDifferentTargets() Assert.Equal("template-shared-reference-conflict", error.Message); } + [Fact] + public void ASharedHelperMayBeReachedThroughOneJumpThunk() + { + using var fixture = new Fixture(); + // Getter 1 calls the shared helper through a 5-byte jump at 0x1700 instead of directly. + fixture.Bytes[0x1700] = 0xe9; + fixture.Relative(0x1700, 1, 5, 0x1600); + fixture.Relative(Fixture.Getter(1), 83, 87, 0x1700); + var image = fixture.Image(TestContext.Current.CancellationToken); + fixture.Validate(image); + // The thunk is part of the proof: a changed jump invalidates the accepted evidence. + fixture.Bytes[0x1701] ^= 1; + Assert.Throws(() => image.FinishProof()); + } + [Fact] public void ReferencesIntoAnotherMatchedRangeMustKeepTheirInteriorOffset() { @@ -167,6 +182,40 @@ public void UnreadableFinalProofIsTransient() Assert.Throws(() => image.FinishProof()); } + [Theory] + [InlineData(0x420u)] + [InlineData(0x42cu)] + public void ThreadLocalSlotMayDifferFromTheReference(uint slot) + { + using var fixture = new Fixture(slotValue: slot); + var image = fixture.Image(TestContext.Current.CancellationToken); + fixture.Validate(image); + Assert.True(image.FinishProof().Matches(fixture.Read, Fixture.Module, + new NativeAdaptiveBudget(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken))); + } + + [Theory] + [InlineData("initializer")] + [InlineData("type-body")] + public void ThreadLocalSlotMustAgreeAcrossTheTypeFunctionAndEveryInitializer(string changed) + { + using var fixture = new Fixture(); + var offset = changed == "initializer" ? Fixture.Initializer(3) + 27 : Fixture.TypeBody + 9; + BinaryPrimitives.WriteUInt32LittleEndian(fixture.Bytes.AsSpan(offset), 0x42c); + var error = Assert.Throws(() => fixture.Validate(fixture.Image(TestContext.Current.CancellationToken))); + Assert.Equal("thread-slot-inconsistent", error.Message); + } + + [Theory] + [InlineData(0x422u)] + [InlineData(0x10000u)] + public void ThreadLocalSlotMustBeAlignedAndBelow64K(uint slot) + { + using var fixture = new Fixture(slotValue: slot); + var error = Assert.Throws(() => fixture.Validate(fixture.Image(TestContext.Current.CancellationToken))); + Assert.Equal("thread-slot-bounds", error.Message); + } + [Fact] public void CancellationIsNotConvertedIntoStructuralRejection() { @@ -178,7 +227,8 @@ public void CancellationIsNotConvertedIntoStructuralRejection() } private sealed record Field(string Name, string Path, ulong Offset, string Kind); - private sealed record Relocation(int Offset, int Width, int Next, uint ReferenceTarget, string Section, uint? Inner = null); + private sealed record Relocation(int Offset, int Width, int Next, uint ReferenceTarget, string Section, uint? Inner = null, + string Kind = "relative"); private sealed class Fixture : IDisposable { @@ -200,8 +250,11 @@ private sealed class Fixture : IDisposable internal static int Getter(int index) => Initializer(index) + 0x100; internal static int Name(int index) => 0x11400 + index * 0x40; - internal Fixture(bool alias = true, bool getterUsesBody = false) + internal uint SlotValue { get; } + + internal Fixture(bool alias = true, bool getterUsesBody = false, uint slotValue = 0x420) { + SlotValue = slotValue; Write16(0, 0x5a4d); Write32(0x3c, 0x80); Write32(0x80, 0x4550); @@ -217,10 +270,13 @@ internal Fixture(bool alias = true, bool getterUsesBody = false) Bytes[TypeEntry] = 0xe9; Relative(TypeEntry, 1, 5, TypeBody); Convert.FromHexString("4831C0909090909090909090909090C3").CopyTo(Bytes, TypeBody); + // mov edx, imm32: the game's thread-local slot offset, masked by the template. + Bytes[TypeBody + 8] = 0xba; + BinaryPrimitives.WriteUInt32LittleEndian(Bytes.AsSpan(TypeBody + 9), slotValue); Bytes[SharedHelper] = 0xc3; Bytes[0x1700] = 0xc3; TypeTemplate = JsonDocument.Parse(JsonSerializer.Serialize( - Signature("type", ReferenceBody, TypeBody, 16, []))); + Signature("type", ReferenceBody, TypeBody, 16, [new(9, 4, 13, 0, "none", null, "threadLocalSlot")]))); var signatures = new List(); var fields = new List(); @@ -246,6 +302,8 @@ internal Fixture(bool alias = true, bool getterUsesBody = false) Relative(initializer, 19, 23, name); Bytes[initializer + 24] = 0xeb; Bytes[initializer + 25] = 2; + Bytes[initializer + 26] = 0xba; + BinaryPrimitives.WriteUInt32LittleEndian(Bytes.AsSpan(initializer + 27), slotValue); Bytes[initializer + BodyLength - 1] = 0xc3; Convert.FromHexString("534889CB").CopyTo(Bytes, getter); Bytes[getter + 23] = 0xe8; @@ -265,7 +323,8 @@ internal Fixture(bool alias = true, bool getterUsesBody = false) new(5, 4, 9, ReferenceType, "code"), new(12, 4, 16, referenceGetter, "code"), new(19, 4, 23, referenceName, "readonly"), - new(25, 1, 26, referenceInitializer + 28, "code", 28) + new(25, 1, 26, referenceInitializer + 28, "code", 28), + new(27, 4, 31, 0, "none", null, "threadLocalSlot") ])); signatures.Add(Signature("getter-" + index, referenceGetter, getter, BodyLength, [ @@ -324,7 +383,7 @@ private object Signature(string id, uint referenceRva, int actualRva, int length offset = r.Offset, width = r.Width, next = r.Next, - kind = "relative", + kind = r.Kind, targetRva = r.ReferenceTarget, expectedTargetSection = r.Section, internalOffset = r.Inner