/** * @file Pt.c * @author Masoud Rahimi Jafari (Masoodrahimy1379@gmail.com) * @brief Processor Trace (PT) tracing implementation for HyperTrace module * @details Programs Intel PT MSRs and manages per-CPU ToPA / output buffers. * The engine half (PtEngine*) deals with a single PT_PER_CPU at a * time and is OS-agnostic. The HyperDbg wrappers (PtCheck, PtStart, * PtStop, PtPause, PtResume, PtSize, PtDump, PtFlush) operate on the * global per-CPU state list (g_PtStateList) and mirror the LBR API * surface. * @version 0.19 * @date 2026-04-29 * * @copyright This project is released under the GNU Public License v3. */ #include "pch.h" ////////////////////////////////////////////////// // Internal allocation helpers // ////////////////////////////////////////////////// /** * @brief Allocate physically contiguous memory for ToPA / output buffers. * * Intel PT requires the output region's physical address to be aligned to * its own size. We use MmAllocateContiguousMemorySpecifyCache with a * BoundaryAddressMultipleOf equal to the requested size — the allocator * then picks a base that does not cross any size-aligned boundary, which * for a power-of-two size means the base is a multiple of that size. * * @param Size Bytes to allocate. Must be a power of two for size * alignment. * @param Alignment Requested alignment in bytes. Pass `Size` for the * ToPA output region; pass PT_PAGE_SIZE for the ToPA * table itself and the overflow page. * * @return PVOID Virtual address of the allocation, or NULL on failure. */ static PVOID PtAllocateContiguousAligned(UINT64 Size, UINT64 Alignment) { PHYSICAL_ADDRESS Lo = {0}; PHYSICAL_ADDRESS Hi = {0}; PHYSICAL_ADDRESS Boundary = {0}; PVOID Result; Hi.QuadPart = MAXULONG64; Boundary.QuadPart = (LONGLONG)Alignment; Result = MmAllocateContiguousMemorySpecifyCache( (SIZE_T)Size, Lo, Hi, Boundary, MmCached); if (Result != NULL) { RtlSecureZeroMemory(Result, (SIZE_T)Size); } return Result; } /** * @brief Free a physically contiguous allocation made by PtAllocateContiguousAligned. */ static VOID PtFreeContiguous(PVOID Va) { if (Va != NULL) { MmFreeContiguousMemory(Va); } } /** * @brief Translate a kernel virtual address to a physical address. */ static UINT64 PtVaToPa(PVOID Va) { PHYSICAL_ADDRESS Pa = MmGetPhysicalAddress(Va); return (UINT64)Pa.QuadPart; } ////////////////////////////////////////////////// // User-mode mmap helpers // ////////////////////////////////////////////////// /** * @brief Build an MDL whose PFN list concatenates the main output * buffer and the overflow page, then map the combined region * into the current user process as one virtually contiguous * range. * * Main and overflow come from separate * MmAllocateContiguousMemorySpecifyCache calls so they are each * physically contiguous but not contiguous with each other. By * constructing one MDL whose PFN array is [main pfns | overflow * pfns], MmMapLockedPagesSpecifyCache produces a single user VA * range of MainSize + OverflowSize bytes that the consumer reads * as one stream (main first, overflow second). Pages are * non-paged so MDL_PAGES_LOCKED is enough to satisfy the mapper. * * Wrapped in SEH because MmMapLockedPagesSpecifyCache raises on * quota / VAD failures rather than returning NULL. * * @return INT32 0 on success, -1 on failure (outputs untouched). */ static INT32 PtMmapCpuRegionToUser(PVOID MainVa, UINT64 MainPhysical, SIZE_T MainSize, UINT64 OverflowPhysical, SIZE_T OverflowSize, PMDL * OutMdl, PVOID * OutUserVa) { SIZE_T TotalSize = MainSize + OverflowSize; ULONG MainPages; ULONG OverflowPages; ULONG i; PFN_NUMBER * Pfns; PFN_NUMBER MainPfnBase; PFN_NUMBER OverflowPfnBase; PMDL Mdl; PVOID UserVa = NULL; if (MainVa == NULL || MainSize == 0 || OverflowSize == 0 || TotalSize == 0) return -1; // // Sizes must be whole pages — ToPA-backed buffers always are. // if ((MainSize & (PT_PAGE_SIZE - 1)) != 0 || (OverflowSize & (PT_PAGE_SIZE - 1)) != 0) return -1; // // IoAllocateMdl sizes the embedded PFN array from the byte range we // pass in. The seed VA only sets the byte-offset within the first // page (0 for our page-aligned contiguous allocations); we overwrite // the PFN list below so it stops describing MainVa past its own end. // Mdl = IoAllocateMdl(MainVa, (ULONG)TotalSize, FALSE, FALSE, NULL); if (Mdl == NULL) return -1; MainPages = (ULONG)(MainSize >> PAGE_SHIFT); OverflowPages = (ULONG)(OverflowSize >> PAGE_SHIFT); MainPfnBase = (PFN_NUMBER)(MainPhysical >> PAGE_SHIFT); OverflowPfnBase = (PFN_NUMBER)(OverflowPhysical >> PAGE_SHIFT); Pfns = MmGetMdlPfnArray(Mdl); for (i = 0; i < MainPages; i++) Pfns[i] = MainPfnBase + i; for (i = 0; i < OverflowPages; i++) Pfns[MainPages + i] = OverflowPfnBase + i; // // Pages are physically present and non-pageable (contiguous memory); // tag the MDL so MmMapLockedPagesSpecifyCache accepts it. // Mdl->MdlFlags |= MDL_PAGES_LOCKED; __try { UserVa = MmMapLockedPagesSpecifyCache( Mdl, UserMode, MmCached, NULL, FALSE, NormalPagePriority); } __except (EXCEPTION_EXECUTE_HANDLER) { UserVa = NULL; } if (UserVa == NULL) { IoFreeMdl(Mdl); return -1; } *OutMdl = Mdl; *OutUserVa = UserVa; return 0; } /** * @brief Tear down the combined user mapping produced by * PtMmapCpuRegionToUser. Caller must be in the mapping process; * see the cooperative single-process contract. */ static VOID PtUnmapCpuRegionFromUser(PMDL Mdl, PVOID UserVa) { if (UserVa != NULL && Mdl != NULL) { __try { MmUnmapLockedPages(UserVa, Mdl); } __except (EXCEPTION_EXECUTE_HANDLER) { // // Mapping process may have already exited. // } } if (Mdl != NULL) { IoFreeMdl(Mdl); } } ////////////////////////////////////////////////// // Engine routines // ////////////////////////////////////////////////// /** * @brief Convert a buffer size in bytes to the ToPA Size field encoding. * Valid sizes are 4KB * 2^N for N = 0..15. * * @param SizeInBytes * @return INT32 Encoding 0..15, or -1 if the size is not supported. */ INT32 PtEngineSizeToTopaEncoding(UINT64 SizeInBytes) { UINT64 Pages; INT32 N; if (SizeInBytes < PT_PAGE_SIZE) return -1; Pages = SizeInBytes / PT_PAGE_SIZE; // // Must be an exact power-of-two number of pages // if ((Pages & (Pages - 1)) != 0) return -1; N = 0; while ((Pages >> N) != 1) N++; if (N > 15) return -1; return N; } /** * @brief Probe Intel PT capabilities via CPUID leaf 7 / leaf 0x14. * * @param OutCaps Optional. If non-NULL, populated with detailed capabilities. * * @return INT32 0 on success (PT supported), -1 if PT is not supported. */ INT32 PtEngineQueryCapabilities(PT_CAPABILITIES * OutCaps) { int Info[4] = {0}; int MaxSubleaf = 0; UINT64 VmxMisc; // // Check PT support: CPUID.(EAX=07H,ECX=0):EBX[25] // __cpuidex(Info, 0x07, 0); if ((Info[1] & (1 << 25)) == 0) return -1; if (OutCaps == NULL) return 0; RtlZeroMemory(OutCaps, sizeof(*OutCaps)); // // Leaf 0x14, sub-leaf 0: capability flags // Info[0] = Info[1] = Info[2] = Info[3] = 0; __cpuidex(Info, 0x14, 0); MaxSubleaf = Info[0]; /* EAX: max valid sub-leaf */ // // EBX capabilities // OutCaps->Cr3Filtering = (Info[1] >> 0) & 1; OutCaps->PsbCycConfigurable = (Info[1] >> 1) & 1; OutCaps->IpFiltering = (Info[1] >> 2) & 1; OutCaps->MtcSupport = (Info[1] >> 3) & 1; OutCaps->PtwriteSupport = (Info[1] >> 4) & 1; OutCaps->PowerEventTrace = (Info[1] >> 5) & 1; // // ECX capabilities // OutCaps->TopaOutput = (Info[2] >> 0) & 1; OutCaps->TopaMultiEntry = (Info[2] >> 1) & 1; OutCaps->SingleRangeOutput = (Info[2] >> 2) & 1; OutCaps->TransportOutput = (Info[2] >> 3) & 1; OutCaps->IpPayloadsAreLip = (Info[2] >> 31) & 1; // // Sub-leaf 1: detailed numerics // if (MaxSubleaf >= 1) { Info[0] = Info[1] = Info[2] = Info[3] = 0; __cpuidex(Info, 0x14, 1); OutCaps->NumAddrRanges = (UINT32)(Info[0] & 0x7); OutCaps->MtcPeriodBitmap = (UINT16)((Info[0] >> 16) & 0xFFFF); OutCaps->CycThresholdBitmap = (UINT16)(Info[1] & 0xFFFF); OutCaps->PsbFreqBitmap = (UINT16)((Info[1] >> 16) & 0xFFFF); } // // VMX support: IA32_VMX_MISC[14] indicates Intel PT may be used in VMX // operation. Reading IA32_VMX_MISC is unconditional on a VMX-capable // CPU; if the CPU isn't VMX-capable the bit is meaningless and the // value falls through as 0. // VmxMisc = __readmsr(0x00000485 /* IA32_VMX_MISC */); OutCaps->VmxSupport = (VmxMisc >> 14) & 1; return 0; } /** * @brief Initialize a PT_TRACE_CONFIG with sensible defaults. * Trace user + kernel, branch + TSC packets, 2 MB output buffer. */ VOID PtEngineInitDefaultConfig(PT_TRACE_CONFIG * Config) { UINT32 i; Config->TraceUser = TRUE; Config->TraceKernel = TRUE; Config->TargetCr3 = 0; Config->NumAddrRanges = 0; Config->EnableBranch = TRUE; Config->EnableTsc = FALSE; Config->EnableMtc = FALSE; Config->EnableCyc = FALSE; Config->EnableRetCompression = FALSE; Config->MtcFreq = 0; Config->CycThresh = 0; Config->PsbFreq = 0; Config->BufferSize = PT_DEFAULT_BUFFER_SIZE; for (i = 0; i < PT_MAX_ADDR_RANGES; i++) { Config->AddrRanges[i].Start = 0; Config->AddrRanges[i].End = 0; Config->AddrRanges[i].IsStopRange = FALSE; } } /** * @brief Allocate the ToPA table, output buffer, and overflow zone for one * per-CPU PT context, then build the ToPA entries. * * ToPA layout: * [0] main output buffer (Config->BufferSize), INT=1 * [1] overflow page (4 KB), INT=0 * [2] END, wraps back to ToPA table (circular) * * @return INT32 0 on success, -1 on allocation failure, -2 on bad config. */ INT32 PtEngineAllocateBuffers(PT_PER_CPU * Cpu, const PT_TRACE_CONFIG * Config) { INT32 SizeEncoding; UINT64 BufSize; PT_TOPA_ENTRY * Topa; if (Cpu == NULL || Config == NULL) return -2; BufSize = Config->BufferSize; if (BufSize == 0) BufSize = PT_DEFAULT_BUFFER_SIZE; // // Validate buffer size is a valid ToPA encoding // SizeEncoding = PtEngineSizeToTopaEncoding(BufSize); if (SizeEncoding < 0) return -2; // // 1. ToPA table — one 4KB page, must be 4KB-aligned // Cpu->Buffer.TopaVa = (PT_TOPA_ENTRY *)PtAllocateContiguousAligned(PT_PAGE_SIZE, PT_PAGE_SIZE); if (Cpu->Buffer.TopaVa == NULL) return -1; Cpu->Buffer.TopaPhysical = PtVaToPa(Cpu->Buffer.TopaVa); // // 2. Main output buffer — must be aligned to its own size // Cpu->Buffer.OutputVa = PtAllocateContiguousAligned(BufSize, BufSize); if (Cpu->Buffer.OutputVa == NULL) { PtFreeContiguous(Cpu->Buffer.TopaVa); Cpu->Buffer.TopaVa = NULL; return -1; } Cpu->Buffer.OutputPhysical = PtVaToPa(Cpu->Buffer.OutputVa); Cpu->Buffer.OutputSize = BufSize; // // 3. Overflow landing zone — 4KB // Cpu->Buffer.OverflowVa = PtAllocateContiguousAligned(PT_OVERFLOW_SIZE, PT_PAGE_SIZE); if (Cpu->Buffer.OverflowVa == NULL) { PtFreeContiguous(Cpu->Buffer.OutputVa); Cpu->Buffer.OutputVa = NULL; PtFreeContiguous(Cpu->Buffer.TopaVa); Cpu->Buffer.TopaVa = NULL; return -1; } Cpu->Buffer.OverflowPhysical = PtVaToPa(Cpu->Buffer.OverflowVa); // // 4. Build the ToPA table — 3 entries // Topa = Cpu->Buffer.TopaVa; // // Entry 0: main data buffer, INT=1 (trigger PMI when full) // Topa[0].Value = 0; Topa[0].BaseAddr = Cpu->Buffer.OutputPhysical >> 12; Topa[0].Size = (UINT64)SizeEncoding; Topa[0].Int = 1; Topa[0].Stop = 0; // // Entry 1: overflow landing zone (4 KB), no interrupt // Topa[1].Value = 0; Topa[1].BaseAddr = Cpu->Buffer.OverflowPhysical >> 12; Topa[1].Size = PT_TOPA_SIZE_4K; Topa[1].Int = 0; Topa[1].Stop = 0; // // Entry 2: END — circular, points back to this ToPA table // Topa[2].Value = 0; Topa[2].End = 1; Topa[2].BaseAddr = Cpu->Buffer.TopaPhysical >> 12; // // Copy config and initialize state // Cpu->Config = *Config; Cpu->Config.BufferSize = BufSize; Cpu->SavedCtl.Value = 0; Cpu->State = PT_STATE_READY; Cpu->TotalBytesCaptured = 0; return 0; } /** * @brief Free all PT buffers belonging to one per-CPU context. * Must not be called while State == PT_STATE_TRACING. */ VOID PtEngineFreeBuffers(PT_PER_CPU * Cpu) { if (Cpu == NULL) return; // // Refuse to free while tracing — caller must stop first // if (Cpu->State == PT_STATE_TRACING) return; if (Cpu->Buffer.OverflowVa) { PtFreeContiguous(Cpu->Buffer.OverflowVa); Cpu->Buffer.OverflowVa = NULL; Cpu->Buffer.OverflowPhysical = 0; } if (Cpu->Buffer.OutputVa) { PtFreeContiguous(Cpu->Buffer.OutputVa); Cpu->Buffer.OutputVa = NULL; Cpu->Buffer.OutputPhysical = 0; Cpu->Buffer.OutputSize = 0; } if (Cpu->Buffer.TopaVa) { PtFreeContiguous(Cpu->Buffer.TopaVa); Cpu->Buffer.TopaVa = NULL; Cpu->Buffer.TopaPhysical = 0; } Cpu->State = PT_STATE_DISABLED; Cpu->TotalBytesCaptured = 0; Cpu->SavedCtl.Value = 0; } /** * @brief Build IA32_RTIT_CTL from a config + capabilities. * Does not set TraceEn; caller is responsible for enabling last. */ static PT_RTIT_CTL_REGISTER PtEngineBuildCtlFromConfig(const PT_TRACE_CONFIG * Cfg, const PT_CAPABILITIES * Caps) { PT_RTIT_CTL_REGISTER Ctl; Ctl.Value = 0; // // Core settings // Ctl.TraceEn = 0; /* enabled last, after everything is set */ Ctl.ToPA = 1; /* always use ToPA output */ Ctl.FabricEn = 0; /* always output to memory */ // // Privilege filter // Ctl.Os = Cfg->TraceKernel ? 1 : 0; Ctl.User = Cfg->TraceUser ? 1 : 0; // // CR3 filtering // Ctl.Cr3Filter = (Cfg->TargetCr3 != 0) ? 1 : 0; // // Branch tracing — the core functionality // Ctl.BranchEn = Cfg->EnableBranch ? 1 : 0; // // Timing packets // Ctl.TscEn = Cfg->EnableTsc ? 1 : 0; if (Caps->MtcSupport) { Ctl.MtcEn = Cfg->EnableMtc ? 1 : 0; if (Cfg->EnableMtc && ((1 << Cfg->MtcFreq) & Caps->MtcPeriodBitmap)) Ctl.MtcFreq = Cfg->MtcFreq; } if (Caps->PsbCycConfigurable) { Ctl.CycEn = Cfg->EnableCyc ? 1 : 0; if (Cfg->EnableCyc && ((1 << Cfg->CycThresh) & Caps->CycThresholdBitmap)) Ctl.CycThresh = Cfg->CycThresh; if ((1 << Cfg->PsbFreq) & Caps->PsbFreqBitmap) Ctl.PsbFreq = Cfg->PsbFreq; } // // RET compression // Ctl.DisRetc = Cfg->EnableRetCompression ? 0 : 1; // // IP filter range configuration // Ctl.Addr0Cfg = 0; Ctl.Addr1Cfg = 0; Ctl.Addr2Cfg = 0; Ctl.Addr3Cfg = 0; if (Cfg->NumAddrRanges > 0 && Caps->IpFiltering) { if (Cfg->NumAddrRanges >= 1 && Caps->NumAddrRanges >= 1) Ctl.Addr0Cfg = Cfg->AddrRanges[0].IsStopRange ? 2 : 1; if (Cfg->NumAddrRanges >= 2 && Caps->NumAddrRanges >= 2) Ctl.Addr1Cfg = Cfg->AddrRanges[1].IsStopRange ? 2 : 1; if (Cfg->NumAddrRanges >= 3 && Caps->NumAddrRanges >= 3) Ctl.Addr2Cfg = Cfg->AddrRanges[2].IsStopRange ? 2 : 1; if (Cfg->NumAddrRanges >= 4 && Caps->NumAddrRanges >= 4) Ctl.Addr3Cfg = Cfg->AddrRanges[3].IsStopRange ? 2 : 1; } return Ctl; } /** * @brief Start tracing on the CURRENT CPU using the passed PT_PER_CPU. * Programs all PT MSRs and sets TraceEn=1. * * Must be called from the target CPU (DPC or VMX root). * * @return INT32 0 on success, -1 on error. */ INT32 PtEngineStart(PT_PER_CPU * Cpu) { PT_RTIT_CTL_REGISTER Ctl; PT_CAPABILITIES Caps; UINT32 i; BOOLEAN IsOnVmxRootMode = FALSE; if (Cpu == NULL) return -1; if (Cpu->State != PT_STATE_READY && Cpu->State != PT_STATE_STOPPED) return -1; if (Cpu->Buffer.TopaVa == NULL || Cpu->Buffer.OutputVa == NULL) return -1; if (PtEngineQueryCapabilities(&Caps) != 0) return -1; // // Detect VMX-root context if running under HyperDbg's hypervisor. // // Intel PT MSRs (IA32_RTIT_*) are NOT in the default MSR bitmap, so a // direct WRMSR to them in either VMX root or VMX non-root passes // through to hardware without trapping. This means the same MSR // sequence below works correctly from a DPC running in the guest as // well as from a VMX-root packet handler. We still read the mode for // logging and for any future hypervisor-only code (e.g. setting the // "Clear IA32_RTIT_CTL on VM-exit" VMCS control or saving guest-side // RTIT state across VM transitions). // if (g_RunningOnHypervisorEnvironment) { IsOnVmxRootMode = g_Callbacks.VmFuncVmxGetCurrentExecutionMode(); // LogInfo("PT: PtEngineStart on core %u (vmx-root=%u)\n", // KeGetCurrentProcessorNumberEx(NULL), // (UINT32)IsOnVmxRootMode); } // // Don't request more IP ranges or features than the CPU supports // if (Cpu->Config.NumAddrRanges > Caps.NumAddrRanges) return -1; if (Cpu->Config.NumAddrRanges > 0 && !Caps.IpFiltering) return -1; if (Cpu->Config.TargetCr3 != 0 && !Caps.Cr3Filtering) return -1; // // Step 1: Ensure tracing is off and clear status // { PT_RTIT_CTL_REGISTER Cur; Cur.Value = __readmsr(MSR_IA32_RTIT_CTL); if (Cur.TraceEn) { Cur.TraceEn = 0; __writemsr(MSR_IA32_RTIT_CTL, Cur.Value); } } __writemsr(MSR_IA32_RTIT_STATUS, 0); // // Step 2: Set output base and reset output position // __writemsr(MSR_IA32_RTIT_OUTPUT_BASE, Cpu->Buffer.TopaPhysical); { PT_OUTPUT_MASK_PTRS_REGISTER InitMask; InitMask.Value = 0; InitMask.LowerMask = 0x7F; InitMask.MaskOrTableOffset = 0; InitMask.OutputOffset = 0; __writemsr(MSR_IA32_RTIT_OUTPUT_MASK_PTRS, InitMask.Value); } // // Step 3: Set CR3 filter // __writemsr(MSR_IA32_RTIT_CR3_MATCH, Cpu->Config.TargetCr3); // // Step 4: Set IP address ranges. IMPORTANT: only touch ADDRn MSRs the // CPU actually supports; writing to non-existent ADDRn MSRs causes #GP. // for (i = 0; i < Caps.NumAddrRanges; i++) { UINT32 MsrA = MSR_IA32_RTIT_ADDR0_A + (i * 2); UINT32 MsrB = MSR_IA32_RTIT_ADDR0_B + (i * 2); if (i < Cpu->Config.NumAddrRanges) { __writemsr(MsrA, Cpu->Config.AddrRanges[i].Start); __writemsr(MsrB, Cpu->Config.AddrRanges[i].End); } else { __writemsr(MsrA, 0); __writemsr(MsrB, 0); } } // // Step 5: Build IA32_RTIT_CTL and enable // Ctl = PtEngineBuildCtlFromConfig(&Cpu->Config, &Caps); // // Save the CTL value for resume // Cpu->SavedCtl = Ctl; // // Enable tracing — MUST be the last MSR write // Ctl.TraceEn = 1; Cpu->SavedCtl.TraceEn = 1; __writemsr(MSR_IA32_RTIT_CTL, Ctl.Value); // // Step 6: Verify tracing started // { PT_RTIT_STATUS_REGISTER Status; Status.Value = __readmsr(MSR_IA32_RTIT_STATUS); if (Status.Error) { Ctl.TraceEn = 0; __writemsr(MSR_IA32_RTIT_CTL, Ctl.Value); Cpu->State = PT_STATE_ERROR; return -1; } } Cpu->State = PT_STATE_TRACING; return 0; } /** * @brief Read current output position, calculate bytes written, and * optionally copy trace data into Out starting at Out->WriteOffset, * then advance WriteOffset. */ static UINT64 PtEngineReadBytesWritten(const PT_BUFFER * Buf, PT_OUTPUT_BUFFER * Out) { PT_OUTPUT_MASK_PTRS_REGISTER Mask; UINT32 TopaIndex; UINT32 BytesInEntry; UINT64 Total = 0; Mask.Value = __readmsr(MSR_IA32_RTIT_OUTPUT_MASK_PTRS); TopaIndex = (UINT32)(Mask.MaskOrTableOffset >> 0); BytesInEntry = (UINT32)Mask.OutputOffset; // // ToPA layout: // Entry[0] = main buffer (OutputSize bytes) // Entry[1] = overflow (4KB) // Entry[2] = END // if (TopaIndex == 0) { Total = BytesInEntry; } else if (TopaIndex >= 1) { Total = Buf->OutputSize + BytesInEntry; } // // Copy trace data if the caller provided a buffer with enough room // if (Out != NULL && Out->Buffer != NULL && Total > 0) { if (Out->WriteOffset + Total <= Out->Length) { UINT8 * Dest = (UINT8 *)Out->Buffer + Out->WriteOffset; UINT64 MainBytes = (Total < Buf->OutputSize) ? Total : Buf->OutputSize; UINT64 SpillBytes = (Total > Buf->OutputSize) ? (Total - Buf->OutputSize) : 0; RtlCopyMemory(Dest, Buf->OutputVa, (SIZE_T)MainBytes); if (SpillBytes > 0) RtlCopyMemory(Dest + MainBytes, Buf->OverflowVa, (SIZE_T)SpillBytes); Out->WriteOffset += Total; } // else: not enough space — skip copy, WriteOffset unchanged } return Total; } /** * @brief Stop tracing on the CURRENT CPU. Reads final output position, * copies trace data if requested, resets PT MSRs. * * @return UINT64 Total bytes captured (across all PMIs + final), or 0 on error. */ UINT64 PtEngineStop(PT_PER_CPU * Cpu, PT_OUTPUT_BUFFER * Out) { UINT64 BytesThisRun; UINT32 i; BOOLEAN IsOnVmxRootMode = FALSE; if (Cpu == NULL) return 0; if (Cpu->State != PT_STATE_TRACING && Cpu->State != PT_STATE_PAUSED) return 0; // // Detect VMX-root context for symmetry with PtEngineStart. Direct WRMSR // works in both modes for PT MSRs (not in MSR bitmap by default). // if (g_RunningOnHypervisorEnvironment) { IsOnVmxRootMode = g_Callbacks.VmFuncVmxGetCurrentExecutionMode(); // LogInfo("PT: PtEngineStop on core %u (vmx-root=%u)\n", // KeGetCurrentProcessorNumberEx(NULL), // (UINT32)IsOnVmxRootMode); } // // Disable tracing // { PT_RTIT_CTL_REGISTER Ctl; Ctl.Value = __readmsr(MSR_IA32_RTIT_CTL); Ctl.TraceEn = 0; __writemsr(MSR_IA32_RTIT_CTL, Ctl.Value); } // // Read final output position and copy data if requested // BytesThisRun = PtEngineReadBytesWritten(&Cpu->Buffer, Out); Cpu->TotalBytesCaptured += BytesThisRun; // // Check for hardware error // { PT_RTIT_STATUS_REGISTER Status; Status.Value = __readmsr(MSR_IA32_RTIT_STATUS); if (Status.Error) { Cpu->State = PT_STATE_ERROR; return 0; } } // // Reset all PT MSRs // __writemsr(MSR_IA32_RTIT_CTL, 0); __writemsr(MSR_IA32_RTIT_STATUS, 0); __writemsr(MSR_IA32_RTIT_OUTPUT_BASE, 0); __writemsr(MSR_IA32_RTIT_OUTPUT_MASK_PTRS, 0); __writemsr(MSR_IA32_RTIT_CR3_MATCH, 0); // // Clear all ADDRn MSRs that this CPU supports. // NumAddrRanges was validated against caps in PtEngineStart. // for (i = 0; i < Cpu->Config.NumAddrRanges; i++) { __writemsr(MSR_IA32_RTIT_ADDR0_A + (i * 2), 0); __writemsr(MSR_IA32_RTIT_ADDR0_B + (i * 2), 0); } Cpu->State = PT_STATE_STOPPED; return Cpu->TotalBytesCaptured; } /** * @brief Pause tracing on the CURRENT CPU. Preserves buffer state. */ INT32 PtEnginePause(PT_PER_CPU * Cpu) { PT_RTIT_CTL_REGISTER Ctl; if (Cpu == NULL || Cpu->State != PT_STATE_TRACING) return -1; Ctl.Value = __readmsr(MSR_IA32_RTIT_CTL); Ctl.TraceEn = 0; __writemsr(MSR_IA32_RTIT_CTL, Ctl.Value); Cpu->State = PT_STATE_PAUSED; return 0; } /** * @brief Resume tracing on the CURRENT CPU after pause. */ INT32 PtEngineResume(PT_PER_CPU * Cpu) { PT_RTIT_CTL_REGISTER Ctl; if (Cpu == NULL || Cpu->State != PT_STATE_PAUSED) return -1; Ctl = Cpu->SavedCtl; Ctl.TraceEn = 1; __writemsr(MSR_IA32_RTIT_CTL, Ctl.Value); Cpu->State = PT_STATE_TRACING; return 0; } /** * @brief Check whether the latest PMI was raised by Intel PT * (IA32_PERF_GLOBAL_STATUS bit 55). */ BOOLEAN PtEngineIsPtPmi() { UINT64 GlobalStatus = __readmsr(MSR_IA32_PERF_GLOBAL_STATUS); return (GlobalStatus & PERF_GLOBAL_STATUS_TOPA_PMI) ? TRUE : FALSE; } /** * @brief Handle a ToPA PMI on the CURRENT CPU. Caller is responsible for * having already disabled tracing (e.g. via VMCS clear of RTIT_CTL). * * @return UINT64 number of bytes captured in this PMI event. */ UINT64 PtEngineHandlePmi(PT_PER_CPU * Cpu, PT_OUTPUT_BUFFER * Out) { UINT64 BytesThisEvent; if (Cpu == NULL) return 0; // // 1. Read how many bytes were written and copy them out // BytesThisEvent = PtEngineReadBytesWritten(&Cpu->Buffer, Out); Cpu->TotalBytesCaptured += BytesThisEvent; // // 2. Reset output position to start of buffer BEFORE acknowledging the // PMI. PT keeps tracing during the handler (TraceEn stays 1) and the // hardware is free to raise another ToPA PMI as soon as PendTopaPmi / // PERF_GLOBAL_STATUS.TopaPmi are cleared. If MASK_PTRS still points // deep into the overflow page (or past wrap) at that moment, a // back-to-back PMI would observe stale state. // { PT_OUTPUT_MASK_PTRS_REGISTER ResetMask; ResetMask.Value = 0; ResetMask.LowerMask = 0x7F; ResetMask.MaskOrTableOffset = 0; ResetMask.OutputOffset = 0; __writemsr(MSR_IA32_RTIT_OUTPUT_MASK_PTRS, ResetMask.Value); } // // 3. Clear PT pending PMI bits in IA32_RTIT_STATUS // { PT_RTIT_STATUS_REGISTER Status; Status.Value = __readmsr(MSR_IA32_RTIT_STATUS); Status.PendTopaPmi = 0; Status.PendPsbPmi = 0; Status.Error = 0; Status.Stopped = 0; __writemsr(MSR_IA32_RTIT_STATUS, Status.Value); } // // 4. Acknowledge the global PMI bit // __writemsr(MSR_IA32_PERF_GLOBAL_OVF_CTRL, PERF_GLOBAL_STATUS_TOPA_PMI); return BytesThisEvent; } ////////////////////////////////////////////////// // HyperDbg-style wrappers // // (mirror of Lbr.c API surface) // ////////////////////////////////////////////////// /** * @brief Check whether Intel PT is supported on the current CPU. * Mirrors LbrCheck — must be called once before any Pt* operation. * * If running under HyperDbg's hypervisor we also verify that the * platform's IA32_VMX_MISC MSR advertises PT-in-VMX support, since * without that bit set we may not be able to keep PT alive across * VM transitions in a future VMCS-controlled implementation. This * is a soft check — the current direct-MSR path still works because * PT MSRs aren't trapped — but it surfaces the situation in the log. * * @return BOOLEAN TRUE if Intel PT is available. */ BOOLEAN PtCheck() { PT_CAPABILITIES Caps = {0}; if (PtEngineQueryCapabilities(&Caps) != 0) return FALSE; // // We require ToPA output for our buffer scheme // if (!Caps.TopaOutput) { LogInfo("PT: CPU does not support ToPA output; PT cannot be used here.\n"); return FALSE; } if (g_RunningOnHypervisorEnvironment && !Caps.VmxSupport) { // LogInfo("PT: IA32_VMX_MISC[14] is clear - Intel PT in VMX is not " // "advertised on this CPU. Direct MSR programming still works " // "from VMX non-root because PT MSRs are not trapped.\n"); } return TRUE; } /** * @brief Allocate ToPA / output / overflow buffers for every active CPU. * * Must be called at IRQL == PASSIVE_LEVEL (before broadcasting the * per-core enable DPC), because MmAllocateContiguousMemorySpecifyCache * is paged. * * Idempotent: cores that already have buffers (State != DISABLED) * are skipped. * * @return BOOLEAN TRUE if every core ended up with a usable buffer set. */ BOOLEAN PtAllocateAllCpuBuffers() { UINT32 ProcessorsCount; UINT32 i; if (g_PtStateList == NULL) return FALSE; ProcessorsCount = KeQueryActiveProcessorCount(0); for (i = 0; i < ProcessorsCount; i++) { PT_PER_CPU * Cpu = &g_PtStateList[i]; PT_TRACE_CONFIG Cfg = Cpu->Config; if (Cpu->State != PT_STATE_DISABLED) continue; if (Cfg.BufferSize == 0) PtEngineInitDefaultConfig(&Cfg); if (PtEngineAllocateBuffers(Cpu, &Cfg) != 0) { // LogInfo("PT: buffer allocation failed on core %u\n", i); return FALSE; } } return TRUE; } /** * @brief Free ToPA / output / overflow buffers for every active CPU. * * Must be called at IRQL == PASSIVE_LEVEL (after broadcasting any * per-core disable DPC), because MmFreeContiguousMemory is paged. */ VOID PtFreeAllCpuBuffers() { UINT32 ProcessorsCount; UINT32 i; if (g_PtStateList == NULL) return; // // Drop any live user mappings before the underlying contiguous // memory goes away. Must run in the same process that called the // mmap IOCTL — see HYPERTRACE_PT_MMAP_PACKETS for the contract. // PtUnmapAllCpuBuffersFromUser(); ProcessorsCount = KeQueryActiveProcessorCount(0); for (i = 0; i < ProcessorsCount; i++) { PtEngineFreeBuffers(&g_PtStateList[i]); } } /** * @brief Map every per-CPU PT main output buffer and 4 KB overflow page * into the current user process as a single virtually contiguous * region per CPU, and fill OutDescs[i] with the base UserVa and * the total Size (main + overflow) for that CPU. * * PT buffers must already exist (PtAllocateAllCpuBuffers must * have run, i.e. PT is enabled). Idempotent within an enable * cycle: a second call returns the already-cached mappings. On * any per-CPU failure the partial work is rolled back and the * function returns -1. * * @return INT32 0 on success, -1 on failure. */ INT32 PtMmapAllCpuBuffersToUser(PT_USER_BUFFER_DESC * OutDescs, UINT32 MaxDescs, UINT32 * OutNumCpus) { UINT32 ProcessorsCount; UINT32 i; if (OutDescs == NULL || OutNumCpus == NULL || g_PtStateList == NULL) return -1; ProcessorsCount = KeQueryActiveProcessorCount(0); if (ProcessorsCount == 0 || ProcessorsCount > MaxDescs || ProcessorsCount > PT_MAX_CPUS_FOR_MMAP) return -1; if (!g_PtUserMappingsActive) { for (i = 0; i < ProcessorsCount; i++) { PT_PER_CPU * Cpu = &g_PtStateList[i]; if (Cpu->Buffer.OutputVa == NULL || Cpu->Buffer.OverflowVa == NULL) { PtUnmapAllCpuBuffersFromUser(); return -1; } if (PtMmapCpuRegionToUser(Cpu->Buffer.OutputVa, Cpu->Buffer.OutputPhysical, (SIZE_T)Cpu->Buffer.OutputSize, Cpu->Buffer.OverflowPhysical, (SIZE_T)PT_OVERFLOW_SIZE, &g_PtUserMappings[i].Mdl, &g_PtUserMappings[i].UserVa) != 0) { PtUnmapAllCpuBuffersFromUser(); return -1; } } g_PtUserMappingsActive = TRUE; } for (i = 0; i < ProcessorsCount; i++) { OutDescs[i].CpuId = i; OutDescs[i].Reserved = 0; OutDescs[i].UserVa = (UINT64)(ULONG_PTR)g_PtUserMappings[i].UserVa; OutDescs[i].Size = g_PtStateList[i].Buffer.OutputSize + PT_OVERFLOW_SIZE; } *OutNumCpus = ProcessorsCount; return 0; } /** * @brief Release every user mapping created by PtMmapAllCpuBuffersToUser. * Called by PtFreeAllCpuBuffers (i.e. on PT disable / flush) so * user VAs stop being usable before the backing memory is freed. * Also used as a rollback path on partial mmap failure. * * Always walks the full table (PtUnmapCpuRegionFromUser is * NULL-safe) so rollback after a half-finished mapping still * cleans up the CPUs that were mapped before the failure. */ VOID PtUnmapAllCpuBuffersFromUser() { UINT32 i; for (i = 0; i < PT_MAX_CPUS_FOR_MMAP; i++) { PtUnmapCpuRegionFromUser(g_PtUserMappings[i].Mdl, g_PtUserMappings[i].UserVa); g_PtUserMappings[i].Mdl = NULL; g_PtUserMappings[i].UserVa = NULL; } g_PtUserMappingsActive = FALSE; } /** * @brief Start PT tracing on the CURRENT CPU. Buffers must already be * allocated by PtAllocateAllCpuBuffers (called at PASSIVE_LEVEL). * * Mirrors LbrStart but takes no parameters: per-CPU configuration is * sourced from g_PtStateList[core].Config (defaulted at init time). * * @return BOOLEAN TRUE on success. */ BOOLEAN PtStart() { UINT32 CurrentCore; PT_PER_CPU * Cpu; if (g_PtStateList == NULL) { LogInfo("PT: per-CPU state not initialized.\n"); return FALSE; } CurrentCore = KeGetCurrentProcessorNumberEx(NULL); Cpu = &g_PtStateList[CurrentCore]; if (Cpu->State == PT_STATE_DISABLED) { // // Buffers should have been allocated at PASSIVE_LEVEL beforehand. // Allocating from a DPC is unsafe (MmAllocateContiguousMemory* is // a paged routine) so just bail out. // // LogInfo("PT: buffers not allocated for core %u\n", CurrentCore); return FALSE; } if (PtEngineStart(Cpu) != 0) { // LogInfo("PT: PtEngineStart failed on core %u (state=%d)\n", CurrentCore, Cpu->State); return FALSE; } return TRUE; } /** * @brief Stop PT tracing on the CURRENT CPU. * Trace data accumulated in the per-CPU output buffer is left in * place; PtSize / PtDump can read it later. */ VOID PtStop() { UINT32 CurrentCore; PT_PER_CPU * Cpu; if (g_PtStateList == NULL) return; CurrentCore = KeGetCurrentProcessorNumberEx(NULL); Cpu = &g_PtStateList[CurrentCore]; // LogInfo("PT: stopping trace on core %d\n", CurrentCore); PtEngineStop(Cpu, NULL); } /** * @brief Pause PT tracing on the CURRENT CPU. Buffer state is preserved * so a subsequent PtResume picks up where this left off. */ VOID PtPause() { UINT32 CurrentCore; PT_PER_CPU * Cpu; if (g_PtStateList == NULL) return; CurrentCore = KeGetCurrentProcessorNumberEx(NULL); Cpu = &g_PtStateList[CurrentCore]; // LogInfo("PT: pausing trace on core %u\n", CurrentCore); PtEnginePause(Cpu); } /** * @brief Resume PT tracing on the CURRENT CPU after a prior PtPause. */ VOID PtResume() { UINT32 CurrentCore; PT_PER_CPU * Cpu; if (g_PtStateList == NULL) return; CurrentCore = KeGetCurrentProcessorNumberEx(NULL); Cpu = &g_PtStateList[CurrentCore]; // LogInfo("PT: resuming trace on core %u\n", CurrentCore); PtEngineResume(Cpu); } /** * @brief Snapshot the current PT output position on the CURRENT CPU * without disturbing tracing state. The returned value is the * number of bytes of valid trace data sitting in this CPU's * main + overflow buffer, i.e. the offset a decoder should stop * at when reading from the user mapping. */ UINT64 PtSize() { UINT32 CurrentCore; PT_PER_CPU * Cpu; PT_OUTPUT_MASK_PTRS_REGISTER Mask; UINT32 TopaIndex; UINT32 BytesInEntry; if (g_PtStateList == NULL) return 0; CurrentCore = KeGetCurrentProcessorNumberEx(NULL); Cpu = &g_PtStateList[CurrentCore]; if (Cpu->State != PT_STATE_TRACING && Cpu->State != PT_STATE_PAUSED && Cpu->State != PT_STATE_STOPPED) return 0; // // Read MASK_PTRS without touching MSRs that would disturb tracing. // For an active trace this gives the live byte count; for stopped // traces it returns the position at the moment tracing was disabled. // Mask.Value = __readmsr(MSR_IA32_RTIT_OUTPUT_MASK_PTRS); TopaIndex = (UINT32)Mask.MaskOrTableOffset; BytesInEntry = (UINT32)Mask.OutputOffset; if (TopaIndex == 0) return BytesInEntry; return Cpu->Buffer.OutputSize + BytesInEntry; } /** * @brief Print PT trace summary for the CURRENT CPU. * * PT packets are a compressed binary stream that requires a decoder * (libipt or similar) to be human-readable, so this only emits the * per-CPU statistics; bulk packet data is preserved in the output * buffer for offline retrieval. */ VOID PtDump() { UINT32 CurrentCore; PT_PER_CPU * Cpu; if (g_PtStateList == NULL) return; CurrentCore = KeGetCurrentProcessorNumberEx(NULL); Cpu = &g_PtStateList[CurrentCore]; Log("PT trace summary for core %u\n", CurrentCore); Log(" State : %d\n", Cpu->State); Log(" TotalBytesCaptured : 0x%llx\n", Cpu->TotalBytesCaptured); Log(" OutputBufferSize : 0x%llx\n", Cpu->Buffer.OutputSize); Log(" TopaPhysical : 0x%llx\n", Cpu->Buffer.TopaPhysical); Log(" OutputPhysical : 0x%llx\n", Cpu->Buffer.OutputPhysical); Log(" TraceUser/Kernel : %u / %u\n", Cpu->Config.TraceUser, Cpu->Config.TraceKernel); Log(" TargetCr3 : 0x%llx\n", Cpu->Config.TargetCr3); } /** * @brief LBR-style filter wrapper: refresh tracing on the CURRENT CPU with * a fresh PT_FILTER_OPTIONS. * * Mirrors LbrFilter — the caller hands in only the fields a user is * allowed to drive (TraceUser, TraceKernel, TargetCr3, BufferSize, * NumAddrRanges, AddrRanges) and PtFilter writes them into the * per-CPU PT_TRACE_CONFIG one at a time. Engine-managed options * (BranchEn, TscEn, MtcEn, CycEn, RetCompression, *Freq) are left * alone, so a filter call can never accidentally turn off the * packet types the engine relies on. * * BufferSize == 0 keeps the per-CPU value already in place, so * pure filter changes (privilege bits, CR3, IP ranges) skip any * reallocation of the ToPA / output / overflow buffers and can * run entirely from a DPC. Genuine buffer-size changes still need * a PASSIVE_LEVEL caller to free + reallocate; HyperTracePtFilter * handles that case before broadcasting. */ VOID PtFilter(const PT_APPLY_CORE_FILTER_REQUEST * FilterRequest) { UINT32 CurrentCore; PT_PER_CPU * Cpu; UINT32 i; PT_CAPABILITIES Caps = {0}; if (g_PtStateList == NULL || FilterRequest == NULL) return; if (PtEngineQueryCapabilities(&Caps) != 0) return; CurrentCore = KeGetCurrentProcessorNumberEx(NULL); Cpu = &g_PtStateList[CurrentCore]; // LogInfo("PT: applying filter on core %u\n", CurrentCore); // // Stop tracing on this CPU first so we can safely mutate Cpu->Config // and reprogram the RTIT_CTL bits. // if (Cpu->State == PT_STATE_TRACING || Cpu->State == PT_STATE_PAUSED) { PtEngineStop(Cpu, NULL); } // // Apply only the user-tunable fields to this CPU's per-CPU config. // Cpu->Config.TraceUser = (BOOLEAN)FilterRequest->FilterOptions.TraceUser; Cpu->Config.TraceKernel = (BOOLEAN)FilterRequest->FilterOptions.TraceKernel; if (FilterRequest->EnableOptions.Cr3 != 0 && !Caps.Cr3Filtering) { // LogInfo("PT: CR3 filtering requested but not supported by CPU\n"); Cpu->Config.TargetCr3 = 0; } else { Cpu->Config.TargetCr3 = FilterRequest->EnableOptions.Cr3; } if (FilterRequest->FilterOptions.NumAddrRanges > Caps.NumAddrRanges) { // LogInfo("PT: requested %u IP filter ranges, but CPU only supports %u\n", FilterRequest->FilterOptions.NumAddrRanges, Caps.NumAddrRanges); Cpu->Config.NumAddrRanges = Caps.NumAddrRanges; } else if (FilterRequest->FilterOptions.NumAddrRanges > 0 && !Caps.IpFiltering) { // LogInfo("PT: IP filtering requested but not supported by CPU\n"); Cpu->Config.NumAddrRanges = 0; } else { Cpu->Config.NumAddrRanges = FilterRequest->FilterOptions.NumAddrRanges; } if (FilterRequest->BufferSize != 0) { Cpu->Config.BufferSize = FilterRequest->BufferSize; } for (i = 0; i < PT_MAX_ADDR_RANGES; i++) { Cpu->Config.AddrRanges[i] = FilterRequest->FilterOptions.AddrRanges[i]; } // // If the per-CPU buffers haven't been allocated yet, leave the slot // configured so the next PtStart picks it up — skip starting here // because PtEngineStart needs ToPA / output / overflow already // allocated at PASSIVE_LEVEL. // if (Cpu->State == PT_STATE_DISABLED) return; PtEngineStart(Cpu); } /** * @brief Flush PT trace state on the CURRENT CPU — disables tracing and * clears the bytes-captured counter so the next PtStart begins from * a fresh baseline. Buffer freeing happens at PASSIVE_LEVEL via * PtFreeAllCpuBuffers; this is safe to call from a DPC. * * Mirrors LbrFlush. */ VOID PtFlush() { UINT32 CurrentCore; PT_PER_CPU * Cpu; if (g_PtStateList == NULL) return; CurrentCore = KeGetCurrentProcessorNumberEx(NULL); Cpu = &g_PtStateList[CurrentCore]; // LogInfo("PT: flush on core %u\n", CurrentCore); if (Cpu->State == PT_STATE_TRACING || Cpu->State == PT_STATE_PAUSED) { PtEngineStop(Cpu, NULL); } Cpu->TotalBytesCaptured = 0; }