File: Contracts\StackWalk\Context\Wasm\WasmUnwinder.cs
Web Access
Project: Microsoft.Diagnostics.DataContractReader.Contracts.csproj (Microsoft.Diagnostics.DataContractReader.Contracts)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm;

/// <summary>
/// Information the WASM ReadyToRun unwinder needs about R2R function-table entries.
/// Mirrors the <c>ExecutionManager</c> APIs used by the native WASM stack walk in
/// <c>src/coreclr/vm/wasm/helpers.cpp</c> (<c>GetWasmVirtualIPFromFunctionTableIndex</c>) plus
/// access to the per-function unwind data from which the fixed frame size is decoded.
/// </summary>
internal interface IWasmR2RInfo
{
    /// <summary>
    /// Returns the base virtual IP represented by a frame identity. Returns false, or a base of 0,
    /// when the identity does not map to a known R2R function.
    /// </summary>
    bool TryGetVirtualIPBase(uint frameIdentity, out ulong baseVirtualIP);

    /// <summary>
    /// Returns the address of the WASM unwind blob for the function containing
    /// <paramref name="controlPC"/>. The blob begins with a ULEB128 fixed frame size.
    /// </summary>
    bool TryGetUnwindData(TargetCodePointer controlPC, out TargetPointer unwindDataAddress);
}

/// <summary>
/// Walks CoreCLR WASM ReadyToRun frames over the managed linear stack (<c>$sp</c>), mirroring
/// the native implementation in <c>src/coreclr/vm/wasm/helpers.cpp</c>. The frame offsets and
/// sentinels below are defined in <c>src/coreclr/vm/wasm/callhelpers.hpp</c>, and the calling
/// convention is documented in <c>docs/design/coreclr/botr/clr-abi.md</c>.
/// </summary>
/// <remarks>
/// Each R2R frame base stores its frame identity at offset 0 and its function-local virtual IP
/// (divided by 2) at offset 4. A frame whose first word is
/// <see cref="StackWalkIndirectToFramePointer"/> is a <c>localloc</c> frame whose real base
/// pointer is stored one pointer-sized slot later. A frame whose first word is
/// <see cref="TerminateR2RStackWalk"/> is not R2R code (an interpreter transition or the stack
/// top), at which point R2R walking stops and the caller falls back to the explicit Frame chain
/// / interpreter frame chain.
/// </remarks>
internal sealed class WasmUnwinder
{
    // Sp values at or below the lowest linear-memory page carry nothing meaningful.
    private const ulong LinearStackFloor = 0x1000;

    // WASM_STACKFRAME_FUNCTION_INDEX_OFFSET: tagged frame identity (32-bit).
    private const ulong FrameIdentityOffset = 0;

    // WASM_STACKFRAME_VIRTUALIP_OFFSET: function-local virtual IP / 2 (always 32-bit).
    private const ulong VirtualIpOffset = 4;

    // STACK_WALK_INDIRECT_TO_FRAMEPOINTER: this slot is not the frame base; the real base
    // pointer follows one pointer-sized slot later (localloc frames).
    private const uint StackWalkIndirectToFramePointer = 0;

    // TERMINATE_R2R_STACK_WALK: this frame is not R2R-generated managed code.
    private const uint TerminateR2RStackWalk = 1;

    private const uint FrameIdentityTagMask = 3;
    private const uint EstablishingFrameTag = 2;
    private const int MaxEstablishingFrameDepth = 4096;

    private readonly Target _target;
    private readonly IWasmR2RInfo _r2rInfo;
    private readonly ulong _pointerSize;

    public WasmUnwinder(Target target, IWasmR2RInfo r2rInfo)
    {
        _target = target;
        _r2rInfo = r2rInfo;
        _pointerSize = (ulong)target.PointerSize;
    }

    /// <summary>
    /// Resolves the R2R frame base for a stack pointer, mirroring
    /// <c>GetWasmFramePointerFromStackPointer_Internal</c>. Returns false when there is no R2R
    /// frame at <paramref name="sp"/> (below the linear-stack floor, or a
    /// <see cref="TerminateR2RStackWalk"/> marker).
    /// </summary>
    public bool TryGetFramePointer(TargetPointer sp, out TargetPointer frameBase)
    {
        frameBase = TargetPointer.Null;
        if (sp.Value <= LinearStackFloor)
            return false;

        ulong current = sp.Value;
        if (_target.Read<uint>(current + FrameIdentityOffset) == StackWalkIndirectToFramePointer)
        {
            current = _target.ReadPointer(current + _pointerSize).Value;
            // Re-apply the linear-stack floor after following the localloc indirection: a null or
            // out-of-range saved frame pointer is not a valid frame base.
            if (current <= LinearStackFloor)
                return false;
        }

        if (_target.Read<uint>(current + FrameIdentityOffset) == TerminateR2RStackWalk)
            return false;

        frameBase = new TargetPointer(current);
        return true;
    }

    /// <summary>
    /// Resolves any establishing-frame identity tags to the root method frame.
    /// </summary>
    private bool TryGetEstablishingFramePointer(TargetPointer frameBase, out TargetPointer establishingFrame)
    {
        establishingFrame = frameBase;
        for (int i = 0; i < MaxEstablishingFrameDepth; i++)
        {
            uint identity = _target.Read<uint>(establishingFrame.Value + FrameIdentityOffset);
            if ((identity & FrameIdentityTagMask) != EstablishingFrameTag)
                return true;

            ulong next = identity - EstablishingFrameTag;
            if (next <= establishingFrame.Value)
            {
                establishingFrame = TargetPointer.Null;
                return false;
            }

            establishingFrame = new TargetPointer(next);
        }

        establishingFrame = TargetPointer.Null;
        return false;
    }

    /// <summary>
    /// Computes the current R2R virtual IP for a stack pointer, mirroring
    /// <c>GetWasmVirtualIPFromStackPointer</c>. Returns <see cref="TargetCodePointer.Null"/> when
    /// there is no R2R frame or the function index does not map to a known base virtual IP.
    /// </summary>
    public TargetCodePointer GetVirtualIP(TargetPointer sp)
    {
        if (!TryGetFramePointer(sp, out TargetPointer frameBase))
            return TargetCodePointer.Null;

        if (!TryGetEstablishingFramePointer(frameBase, out TargetPointer establishingFrame))
            return TargetCodePointer.Null;

        uint frameIdentity = _target.Read<uint>(establishingFrame.Value + FrameIdentityOffset);
        // Virtual IPs are stored divided by 2; the low bit distinguishes virtual IPs from
        // interpreter addresses / portable entrypoints.
        uint functionLocalVirtualIP = _target.Read<uint>(frameBase.Value + VirtualIpOffset) * 2;

        if (!_r2rInfo.TryGetVirtualIPBase(frameIdentity, out ulong baseVirtualIP) || baseVirtualIP == 0)
            return TargetCodePointer.Null;

        return new TargetCodePointer(baseVirtualIP + functionLocalVirtualIP);
    }

    /// <summary>
    /// Advances <paramref name="sp"/> by one R2R frame and produces the caller's virtual IP,
    /// mirroring <c>WasmUnwindStackFrameCore</c>. Returns false when the R2R walk cannot advance
    /// (no R2R frame at <paramref name="sp"/>, missing unwind data, or a zero frame size), in
    /// which case <paramref name="sp"/> is set to <see cref="TargetPointer.Null"/>. Otherwise
    /// <paramref name="sp"/> is the caller's stack pointer, and <paramref name="ip"/> is null when
    /// the caller is not ReadyToRun code.
    /// </summary>
    public bool TryUnwindOneFrame(ref TargetPointer sp, TargetCodePointer controlPC, out TargetCodePointer ip)
    {
        ip = TargetCodePointer.Null;
        if (!TryGetFramePointer(sp, out TargetPointer frameBase))
        {
            sp = TargetPointer.Null;
            return false;
        }

        if (!_r2rInfo.TryGetUnwindData(controlPC, out TargetPointer unwindData))
        {
            sp = TargetPointer.Null;
            return false;
        }

        uint frameIdentity = _target.Read<uint>(frameBase.Value + FrameIdentityOffset);
        uint frameSize = DecodeULEB128(unwindData.Value);
        if (frameSize == 0)
        {
            // A zero frame size makes no progress; terminate rather than risk an unbounded walk.
            sp = TargetPointer.Null;
            return false;
        }

        sp = new TargetPointer(frameBase.Value + frameSize);
        if ((frameIdentity & 1) != 0 && (int)frameIdentity > 0)
        {
            // Positive odd identities are reverse P/Invoke roots. Their caller is native, so
            // preserve the caller SP for the Frame-chain walk but do not interpret it as an R2R
            // shadow frame.
            return true;
        }

        // A null IP means the caller is not R2R code (an interpreter transition or the stack top).
        // Like native, keep the caller's SP so the walk continues through the Frame chain.
        ip = GetVirtualIP(sp);

        return true;
    }

    /// <summary>
    /// Returns the logical (establishing) frame pointer for the frame at <paramref name="sp"/>,
    /// mirroring <c>GetWasmFramePointerFromStackPointer</c> in
    /// <c>src/coreclr/vm/wasm/helpers.cpp</c>.
    /// </summary>
    /// <remarks>
    /// For a method's root function this is its own frame base. A funclet stores its establishing
    /// frame pointer tagged with <see cref="EstablishingFrameTag"/>.
    /// </remarks>
    public bool TryGetLogicalFramePointer(TargetPointer sp, out TargetPointer framePointer)
    {
        framePointer = TargetPointer.Null;

        return TryGetFramePointer(sp, out TargetPointer frameBase)
            && TryGetEstablishingFramePointer(frameBase, out framePointer);
    }

    // Standard little-endian base-128 varint, matching the native DecodeULEB128AsU32. A ULEB128
    // uint32 is at most 5 bytes (5 * 7 = 35 >= 32 bits); a longer encoding is malformed.
    private uint DecodeULEB128(ulong address)
    {
        const int MaxBytes = 5;
        uint result = 0;
        int shift = 0;
        for (ulong offset = 0; offset < MaxBytes; offset++)
        {
            byte b = _target.Read<byte>(address + offset);
            result |= (uint)(b & 0x7F) << shift;
            if ((b & 0x80) == 0)
                return result;
            shift += 7;
        }

        throw new InvalidOperationException("Malformed ULEB128 value in WASM unwind data.");
    }
}