From 19138e08a6d25efed24da70edfe82f576656772e Mon Sep 17 00:00:00 2001 From: AdvDebug <90452585+AdvDebug@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:27:10 +0300 Subject: [PATCH 1/2] Add AudioSrv/ALPC audio support and fixes Implement a host-backed audio subsystem and ALPC audio support: add AudioManager, platform sinks (Windows/ALSA/AAudio), AudioSrv and AudioClientRpc handlers, NDR/LRPC helpers, shared-section stream setup and AudioStreamEngine to feed host devices. Add ALPC handle attribute support and PortReply plumbing. Also: add cursor warp (host/client) integration, scheduler/timing and emulated QPC fixes, various Win/syscall additions and robustness fixes (NtOpenEvent, NtQueryPerformanceCounter, NtMapViewOfSection view refcounting, registry enumeration improvements, GDI table validation, TEB impersonation default, CW_USEDEFAULT handling), and a --install-dxvk CLI option. In addition to fixing quite a few bugs. --- Brovan/Android/AndroidAudioSink.cs | 103 +++ Brovan/Android/AndroidWinManager.cs | 5 + Brovan/Core/Emulation/BinaryEmulator.cs | 74 +- Brovan/Core/Emulation/Guests/WindowsGuest.cs | 4 +- .../AudioManager/AudioManager.cs | 95 +++ .../AudioManager/LinuxAudioSink.cs | 85 +++ .../AudioManager/WindowsAudioSink.cs | 185 +++++ .../WindowManager/GuiThreadManager.cs | 16 + .../WindowManager/LinuxWinManager.cs | 12 + .../WindowManager/WindowManager.cs | 2 + .../WindowManager/WindowsWinManager.cs | 17 + .../OS/Windows/Files/NtMapViewOfSection.cs | 7 + .../OS/Windows/Misc/NtCreateEvent.cs | 6 +- .../OS/Windows/Misc/NtDeleteWnfStateName.cs | 10 + .../Emulation/OS/Windows/Misc/NtOpenEvent.cs | 61 ++ .../Windows/Misc/NtQueryPerformanceCounter.cs | 2 +- .../Misc/NtUnsubscribeWnfStateChange.cs | 10 + .../Process/NtQueryInformationToken.cs | 13 +- .../Emulation/OS/Windows/RPC/LrpcPacket.cs | 108 +++ Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs | 236 +++++++ .../OS/Windows/RPC/NtAlpcConnectPortEx.cs | 5 + .../RPC/NtAlpcQueryInformationMessage.cs | 77 ++ .../Windows/RPC/NtAlpcSendWaitReceivePort.cs | 83 ++- .../Emulation/OS/Windows/RPC/NtConnectPort.cs | 4 + .../Emulation/OS/Windows/RPC/Ports/ApiPort.cs | 24 +- .../OS/Windows/RPC/Ports/AudioSrvPort.cs | 656 ++++++++++++++++++ .../OS/Windows/RPC/Ports/AudioStreamEngine.cs | 116 ++++ .../OS/Windows/Registry/NtOpenKeyEx.cs | 5 +- .../OS/Windows/Registry/NtQueryKey.cs | 8 + .../OS/Windows/Registry/NtQueryValueKey.cs | 47 +- .../OS/Windows/Win32k/NtUserCreateWindowEx.cs | 15 +- .../OS/Windows/Win32k/NtUserSetCursorPos.cs | 19 + .../OS/Windows/Win32k/Win32kHelper.cs | 15 + .../OS/Windows/WinHelperConstants.cs | 32 +- .../Emulation/OS/Windows/WinInternalHelper.cs | 69 +- .../Emulation/OS/Windows/WinSyscallsHelper.cs | 192 ++++- Brovan/Core/Helpers/RegistryManager.cs | 120 +++- Brovan/Program.cs | 28 +- 38 files changed, 2419 insertions(+), 147 deletions(-) create mode 100644 Brovan/Android/AndroidAudioSink.cs create mode 100644 Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/AudioManager.cs create mode 100644 Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/LinuxAudioSink.cs create mode 100644 Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/WindowsAudioSink.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/Misc/NtDeleteWnfStateName.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/Misc/NtOpenEvent.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/Misc/NtUnsubscribeWnfStateChange.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/RPC/LrpcPacket.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcQueryInformationMessage.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioSrvPort.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioStreamEngine.cs create mode 100644 Brovan/Core/Emulation/OS/Windows/Win32k/NtUserSetCursorPos.cs diff --git a/Brovan/Android/AndroidAudioSink.cs b/Brovan/Android/AndroidAudioSink.cs new file mode 100644 index 0000000..aafbb80 --- /dev/null +++ b/Brovan/Android/AndroidAudioSink.cs @@ -0,0 +1,103 @@ +using System; +using System.Runtime.InteropServices; +using Brovan.Core.Emulation.OS.SharedHelpers; + +namespace Brovan.Android +{ + internal sealed unsafe class AndroidAudioSink : IAudioSink + { + private const int FormatPcmFloat = 2; + private const int PerformanceModeLowLatency = 12; + private const long WriteTimeoutNanoseconds = 1_000_000_000; + + [DllImport("libaaudio.so")] + private static extern int AAudio_createStreamBuilder(out IntPtr Builder); + + [DllImport("libaaudio.so")] + private static extern void AAudioStreamBuilder_setFormat(IntPtr Builder, int Format); + + [DllImport("libaaudio.so")] + private static extern void AAudioStreamBuilder_setChannelCount(IntPtr Builder, int Channels); + + [DllImport("libaaudio.so")] + private static extern void AAudioStreamBuilder_setSampleRate(IntPtr Builder, int SampleRate); + + [DllImport("libaaudio.so")] + private static extern void AAudioStreamBuilder_setPerformanceMode(IntPtr Builder, int Mode); + + [DllImport("libaaudio.so")] + private static extern int AAudioStreamBuilder_openStream(IntPtr Builder, out IntPtr Stream); + + [DllImport("libaaudio.so")] + private static extern int AAudioStreamBuilder_delete(IntPtr Builder); + + [DllImport("libaaudio.so")] + private static extern int AAudioStream_requestStart(IntPtr Stream); + + [DllImport("libaaudio.so")] + private static extern int AAudioStream_write(IntPtr Stream, void* Buffer, int Frames, long TimeoutNanoseconds); + + [DllImport("libaaudio.so")] + private static extern int AAudioStream_close(IntPtr Stream); + + private readonly int BlockAlign; + private IntPtr Stream; + + public AndroidAudioSink(AudioSinkFormat Format) + { + BlockAlign = Format.BlockAlign; + + if (AAudio_createStreamBuilder(out IntPtr Builder) != 0 || Builder == IntPtr.Zero) + throw new InvalidOperationException("AAudio_createStreamBuilder failed."); + + try + { + AAudioStreamBuilder_setFormat(Builder, FormatPcmFloat); + AAudioStreamBuilder_setChannelCount(Builder, Format.Channels); + AAudioStreamBuilder_setSampleRate(Builder, (int)Format.SampleRate); + AAudioStreamBuilder_setPerformanceMode(Builder, PerformanceModeLowLatency); + + if (AAudioStreamBuilder_openStream(Builder, out Stream) != 0 || Stream == IntPtr.Zero) + throw new InvalidOperationException("AAudioStreamBuilder_openStream failed."); + } + finally + { + AAudioStreamBuilder_delete(Builder); + } + + AAudioStream_requestStart(Stream); + } + + public void Write(ReadOnlySpan Samples) + { + if (Stream == IntPtr.Zero) + return; + + fixed (byte* Base = Samples) + { + int Offset = 0; + while (Offset < Samples.Length) + { + int Frames = (Samples.Length - Offset) / BlockAlign; + if (Frames == 0) + return; + + int Written = AAudioStream_write(Stream, Base + Offset, Frames, WriteTimeoutNanoseconds); + if (Written <= 0) + return; + + Offset += Written * BlockAlign; + } + } + } + + public void Dispose() + { + if (Stream == IntPtr.Zero) + return; + + AAudioStream_close(Stream); + Stream = IntPtr.Zero; + } + } +} diff --git a/Brovan/Android/AndroidWinManager.cs b/Brovan/Android/AndroidWinManager.cs index 63405e2..3124539 100644 --- a/Brovan/Android/AndroidWinManager.cs +++ b/Brovan/Android/AndroidWinManager.cs @@ -197,6 +197,11 @@ public void Present() { } + // Touch input has no pointer to move; the guest's own cursor position is authoritative here. + public void WarpCursor(int clientX, int clientY) + { + } + public void Show() => Visible = true; public void Hide() => Visible = false; diff --git a/Brovan/Core/Emulation/BinaryEmulator.cs b/Brovan/Core/Emulation/BinaryEmulator.cs index b4c45e3..778eee4 100644 --- a/Brovan/Core/Emulation/BinaryEmulator.cs +++ b/Brovan/Core/Emulation/BinaryEmulator.cs @@ -395,6 +395,12 @@ public int Compare(MemoryRegion x, MemoryRegion y) private ulong _timestampCounter = 0x100000000UL; private const ulong TscCyclesPerInstruction = 3; + + /// + /// How long the scheduler blocks in one go when no guest thread can run. + /// + private const int IdleWaitSliceMs = 5; + private const ulong TscCyclesPerMillisecond = 3_000_000UL; private const ulong RdtscReadCycles = 60; private const ulong RdtscpReadCycles = 90; @@ -451,6 +457,26 @@ internal bool IsEmulatedDeadlineExpired(long Deadline) return Deadline != -1 && EmulatedTickCount64 >= Deadline; } + /// + /// The performance counter, on the same timebase as and at finer + /// resolution than it. + /// + internal ulong GetEmulatedPerformanceCounter() + { + const long QpcFrequency = OS.Windows.KuserSharedDataManager.QpcFrequency; + + long HostTicks = _wallClock.ElapsedTicks; + long HostFrequency = System.Diagnostics.Stopwatch.Frequency; + long Elapsed = HostFrequency == QpcFrequency + ? HostTicks + : (long)((decimal)HostTicks * QpcFrequency / HostFrequency); + + long Skew = Volatile.Read(ref _emulatedTimeSkewMilliseconds); + long SkewCounts = Skew > long.MaxValue / (QpcFrequency / 1000) ? long.MaxValue : Skew * (QpcFrequency / 1000); + + return unchecked((ulong)(Elapsed > long.MaxValue - SkewCounts ? long.MaxValue : Elapsed + SkewCounts)); + } + /// /// Advances guest time for a wait that was not served in real time. /// @@ -474,6 +500,10 @@ internal void AdvanceEmulatedTimeMilliseconds(long Milliseconds, bool AdvanceTim else _timestampCounter += Ticks * TscCyclesPerMillisecond; } + + // A skew jump is the one moment the page is guaranteed stale, and the guest usually reads it + // immediately afterwards: the wait it was serving has just come due. + WinHelper?.KuserSharedData?.RefreshIfUnhooked(); } /// @@ -2246,7 +2276,11 @@ private bool TryGetNextWaitSleepMs(out int SleepMs, int MaxSleepMs = 10) continue; long Delta = Thread.WaitDeadline - Now; - if (Delta > 0 && Delta < BestDelta) + + if (Delta <= 0) + return true; + + if (Delta < BestDelta) BestDelta = Delta; } @@ -2531,19 +2565,17 @@ public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels = if (TryGetNextWaitSleepMs(out int SleepMs, int.MaxValue)) { if (Debug) - TriggerDebugMessage($"scheduler: no runnable thread, advancing guest time by {SleepMs}ms"); - if (HasActiveGetMessageWait()) - Thread.Sleep(Math.Min(SleepMs, 16)); - AdvanceEmulatedTimeMilliseconds(SleepMs, AdvanceTimestampCounter: true); + TriggerDebugMessage($"scheduler: no runnable thread, waiting up to {SleepMs}ms"); + Thread.Sleep(Math.Min(SleepMs, IdleWaitSliceMs)); + WinHelper?.KuserSharedData?.RefreshIfUnhooked(); WakeupScanRequired = true; continue; } if (HasActiveGetMessageWait()) { - const int MessagePumpPollMs = 10; - Thread.Sleep(MessagePumpPollMs); - AdvanceEmulatedTimeMilliseconds(MessagePumpPollMs, AdvanceTimestampCounter: true); + Thread.Sleep(IdleWaitSliceMs); + WinHelper?.KuserSharedData?.RefreshIfUnhooked(); WakeupScanRequired = true; continue; } @@ -2625,30 +2657,20 @@ public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels = ImmaBeEmulatedOOO.InstructionsExecuted += SchedulerSliceWork; Total += SchedulerSliceWork; AdvanceTimestampCounter(SchedulerSliceWork); - bool AdvancedEmulatedTime = false; + + bool TimedWaitRescanDue = false; if (SchedulerSliceWork > 1) { PendingSchedulerTimeCycles += (ulong)SchedulerSliceWork * TscCyclesPerInstruction; if (PendingSchedulerTimeCycles >= SchedulerCyclesPerMillisecond) { - ulong TimeBudgetMs = PendingSchedulerTimeCycles / SchedulerCyclesPerMillisecond; - int MaxAdvanceMs = TimeBudgetMs > int.MaxValue ? int.MaxValue : (int)TimeBudgetMs; - - if (TryGetNextWaitSleepMs(out int SliceSleepMs, MaxAdvanceMs)) - { - AdvanceEmulatedTimeMilliseconds(SliceSleepMs); - AdvancedEmulatedTime = true; - ulong ConsumedCycles = (ulong)SliceSleepMs * SchedulerCyclesPerMillisecond; - PendingSchedulerTimeCycles = ConsumedCycles >= PendingSchedulerTimeCycles ? 0 : PendingSchedulerTimeCycles - ConsumedCycles; - } - else - { - PendingSchedulerTimeCycles = 0; - } + PendingSchedulerTimeCycles = 0; + TimedWaitRescanDue = true; } } Slices++; + WinHelper?.KuserSharedData?.RefreshIfUnhooked(); ImmaBeEmulatedOOO.LastRunTick = SchedulerTick; if (ImmaBeEmulatedOOO.Context?.RIP == 0) @@ -2687,11 +2709,11 @@ public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels = KnownThreadOrderCount = ThreadOrder.Count; } - WakeupScanRequired = SliceRequestedRefresh || AdvancedEmulatedTime || ThreadOrder.Count != KnownThreadOrderCount || ImmaBeEmulatedOOO.State == EmulatedThreadState.Waiting; + WakeupScanRequired = SliceRequestedRefresh || TimedWaitRescanDue || ThreadOrder.Count != KnownThreadOrderCount || ImmaBeEmulatedOOO.State == EmulatedThreadState.Waiting; - if (Debug && (Slices <= 64 || (Slices & 0xFF) == 0 || ImmaBeEmulatedOOO.State != StateBeforeSlice || SliceRequestedRefresh || AdvancedEmulatedTime)) + if (Debug && (Slices <= 64 || (Slices & 0xFF) == 0 || ImmaBeEmulatedOOO.State != StateBeforeSlice || SliceRequestedRefresh || TimedWaitRescanDue)) { - TriggerDebugMessage($"scheduler: slice tid={ImmaBeEmulatedOOO.ThreadId} {StateBeforeSlice}->{ImmaBeEmulatedOOO.State} work={SchedulerSliceWork} total={Total} rip=0x{RipBeforeSlice:X}->0x{ImmaBeEmulatedOOO.Context?.RIP ?? 0:X} refresh={SliceRequestedRefresh} advancedTime={AdvancedEmulatedTime} boost={ImmaBeEmulatedOOO.DynamicBoost}"); + TriggerDebugMessage($"scheduler: slice tid={ImmaBeEmulatedOOO.ThreadId} {StateBeforeSlice}->{ImmaBeEmulatedOOO.State} work={SchedulerSliceWork} total={Total} rip=0x{RipBeforeSlice:X}->0x{ImmaBeEmulatedOOO.Context?.RIP ?? 0:X} refresh={SliceRequestedRefresh} rescanDue={TimedWaitRescanDue} boost={ImmaBeEmulatedOOO.DynamicBoost}"); } if (MaxTotalInstructions != 0 && Total >= MaxTotalInstructions) diff --git a/Brovan/Core/Emulation/Guests/WindowsGuest.cs b/Brovan/Core/Emulation/Guests/WindowsGuest.cs index 4d3f1ba..23229be 100644 --- a/Brovan/Core/Emulation/Guests/WindowsGuest.cs +++ b/Brovan/Core/Emulation/Guests/WindowsGuest.cs @@ -913,7 +913,7 @@ public ulong AllocateAndInitializeTEB(BinaryEmulator Instance, EmulatedThread Th Instance._emulator.WriteMemory(Teb + 0x68, (uint)0u); Instance._emulator.WriteMemory(Teb + 0x108, (uint)0x0409u); Instance._emulator.WriteMemory(Teb + 0x1760, (uint)0u); - Instance._emulator.WriteMemory(Teb + 0x179C, (uint)1u); + Instance._emulator.WriteMemory(Teb + 0x179C, (uint)0u); Instance._emulator.WriteMemory(Teb + 0x17A0, (ulong)0ul); ushort SameTebFlags = 0; if (InitialThread) @@ -1169,7 +1169,7 @@ public byte[] BuildEnvironment(BinaryEmulator Instance, out ulong size) if (string.IsNullOrEmpty(Name)) continue; - if (!Name.StartsWith("DXVK_", StringComparison.OrdinalIgnoreCase) && !Name.StartsWith("VK_", StringComparison.OrdinalIgnoreCase)) + if (!Name.StartsWith("DXVK_", StringComparison.OrdinalIgnoreCase) && !Name.StartsWith("VK_", StringComparison.OrdinalIgnoreCase) && !Name.StartsWith("SDL_", StringComparison.OrdinalIgnoreCase)) continue; Env[Name] = HostVariable.Value as string ?? string.Empty; diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/AudioManager.cs b/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/AudioManager.cs new file mode 100644 index 0000000..e3ef086 --- /dev/null +++ b/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/AudioManager.cs @@ -0,0 +1,95 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace Brovan.Core.Emulation.OS.SharedHelpers +{ + /// + /// A host playback device. blocks until the device has taken the samples. + /// + public interface IAudioSink : IDisposable + { + void Write(ReadOnlySpan Samples); + } + + public readonly struct AudioSinkFormat + { + public readonly uint SampleRate; + public readonly ushort Channels; + public readonly ushort BitsPerSample; + + public AudioSinkFormat(uint SampleRate, ushort Channels, ushort BitsPerSample) + { + this.SampleRate = SampleRate; + this.Channels = Channels; + this.BitsPerSample = BitsPerSample; + } + + public int BlockAlign => Channels * (BitsPerSample / 8); + + public int BytesPerSecond => (int)SampleRate * BlockAlign; + } + + public static class AudioSinkFactory + { + public static IAudioSink Create(AudioSinkFormat Format, out string Backend) + { + try + { + if (Android.AndroidHost.IsActive) + { + Backend = "AAudio"; + return new Android.AndroidAudioSink(Format); + } + + if (OperatingSystem.IsWindows()) + { + Backend = "waveOut"; + return new WindowsAudioSink(Format); + } + + if (OperatingSystem.IsLinux()) + { + Backend = "ALSA"; + return new LinuxAudioSink(Format); + } + } + catch (Exception) + { + } + + Backend = "silent"; + return new SilentAudioSink(Format); + } + } + + /// + /// Used when the host has no usable device. It still has to consume at real time, because the guest's + /// ring only drains as fast as the sink accepts. + /// + internal sealed class SilentAudioSink : IAudioSink + { + private readonly AudioSinkFormat Format; + private readonly Stopwatch Elapsed = Stopwatch.StartNew(); + private long WrittenBytes; + + public SilentAudioSink(AudioSinkFormat Format) + { + this.Format = Format; + } + + public void Write(ReadOnlySpan Samples) + { + WrittenBytes += Samples.Length; + + long DueMs = WrittenBytes * 1000 / Format.BytesPerSecond; + long BehindMs = DueMs - Elapsed.ElapsedMilliseconds; + if (BehindMs > 0) + Thread.Sleep((int)Math.Min(BehindMs, 100)); + } + + public void Dispose() + { + } + } +} diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/LinuxAudioSink.cs b/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/LinuxAudioSink.cs new file mode 100644 index 0000000..cac85e2 --- /dev/null +++ b/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/LinuxAudioSink.cs @@ -0,0 +1,85 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Brovan.Core.Emulation.OS.SharedHelpers +{ + [SupportedOSPlatform("linux")] + internal sealed unsafe class LinuxAudioSink : IAudioSink + { + private const int StreamPlayback = 0; + private const int FormatFloatLe = 14; + private const int AccessRwInterleaved = 3; + private const uint LatencyMicroseconds = 100_000; + + [DllImport("libasound.so.2", CharSet = CharSet.Ansi)] + private static extern int snd_pcm_open(out IntPtr Pcm, string Name, int Stream, int Mode); + + [DllImport("libasound.so.2")] + private static extern int snd_pcm_set_params(IntPtr Pcm, int Format, int Access, uint Channels, uint Rate, int SoftResample, uint Latency); + + [DllImport("libasound.so.2")] + private static extern nint snd_pcm_writei(IntPtr Pcm, void* Buffer, nuint Frames); + + [DllImport("libasound.so.2")] + private static extern int snd_pcm_recover(IntPtr Pcm, int Error, int Silent); + + [DllImport("libasound.so.2")] + private static extern int snd_pcm_close(IntPtr Pcm); + + private readonly int BlockAlign; + private IntPtr Pcm; + + public LinuxAudioSink(AudioSinkFormat Format) + { + BlockAlign = Format.BlockAlign; + + if (snd_pcm_open(out Pcm, "default", StreamPlayback, 0) < 0) + throw new InvalidOperationException("snd_pcm_open failed."); + + if (snd_pcm_set_params(Pcm, FormatFloatLe, AccessRwInterleaved, Format.Channels, Format.SampleRate, 1, LatencyMicroseconds) < 0) + { + snd_pcm_close(Pcm); + Pcm = IntPtr.Zero; + throw new InvalidOperationException("snd_pcm_set_params failed."); + } + } + + public void Write(ReadOnlySpan Samples) + { + if (Pcm == IntPtr.Zero) + return; + + fixed (byte* Base = Samples) + { + int Offset = 0; + while (Offset < Samples.Length) + { + nuint Frames = (nuint)((Samples.Length - Offset) / BlockAlign); + if (Frames == 0) + return; + + nint Written = snd_pcm_writei(Pcm, Base + Offset, Frames); + if (Written < 0) + { + if (snd_pcm_recover(Pcm, (int)Written, 1) < 0) + return; + + continue; + } + + Offset += (int)Written * BlockAlign; + } + } + } + + public void Dispose() + { + if (Pcm == IntPtr.Zero) + return; + + snd_pcm_close(Pcm); + Pcm = IntPtr.Zero; + } + } +} diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/WindowsAudioSink.cs b/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/WindowsAudioSink.cs new file mode 100644 index 0000000..6c8a1c1 --- /dev/null +++ b/Brovan/Core/Emulation/OS/SharedHelpers/AudioManager/WindowsAudioSink.cs @@ -0,0 +1,185 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; + +namespace Brovan.Core.Emulation.OS.SharedHelpers +{ + [SupportedOSPlatform("windows")] + internal sealed unsafe class WindowsAudioSink : IAudioSink + { + private const uint WaveMapper = 0xFFFFFFFF; + private const uint WaveFormatExtensible = 0xFFFE; + private const uint WhdrDone = 0x00000001; + private const uint CallbackEvent = 0x00050000; + private const uint MmsysErrNoError = 0; + + private const int BufferCount = 4; + private const int BufferMilliseconds = 20; + + [StructLayout(LayoutKind.Sequential)] + private struct WaveHdr + { + public IntPtr Data; + public uint BufferLength; + public uint BytesRecorded; + public IntPtr User; + public uint Flags; + public uint Loops; + public IntPtr Next; + public IntPtr Reserved; + } + + [DllImport("winmm.dll")] + private static extern uint waveOutOpen(out IntPtr Device, uint DeviceId, byte[] Format, IntPtr Callback, IntPtr Instance, uint Flags); + + [DllImport("winmm.dll")] + private static extern uint waveOutPrepareHeader(IntPtr Device, WaveHdr* Header, uint Size); + + [DllImport("winmm.dll")] + private static extern uint waveOutUnprepareHeader(IntPtr Device, WaveHdr* Header, uint Size); + + [DllImport("winmm.dll")] + private static extern uint waveOutWrite(IntPtr Device, WaveHdr* Header, uint Size); + + [DllImport("winmm.dll")] + private static extern uint waveOutReset(IntPtr Device); + + [DllImport("winmm.dll")] + private static extern uint waveOutClose(IntPtr Device); + + private readonly int ChunkBytes; + private readonly int QueueMilliseconds = BufferMilliseconds * BufferCount; + private readonly AutoResetEvent BufferDone = new AutoResetEvent(false); + private IntPtr Device; + private readonly IntPtr Headers; + private readonly IntPtr[] Buffers = new IntPtr[BufferCount]; + private int NextBuffer; + private bool Disposed; + + public WindowsAudioSink(AudioSinkFormat Format) + { + ChunkBytes = Format.BytesPerSecond * BufferMilliseconds / 1000 / Format.BlockAlign * Format.BlockAlign; + + uint Status = waveOutOpen(out Device, WaveMapper, BuildFormat(Format), + BufferDone.SafeWaitHandle.DangerousGetHandle(), IntPtr.Zero, CallbackEvent); + if (Status != MmsysErrNoError) + throw new InvalidOperationException($"waveOutOpen failed with {Status}."); + + Headers = Marshal.AllocHGlobal(sizeof(WaveHdr) * BufferCount); + new Span((void*)Headers, sizeof(WaveHdr) * BufferCount).Clear(); + + for (int Index = 0; Index < BufferCount; Index++) + { + Buffers[Index] = Marshal.AllocHGlobal(ChunkBytes); + + WaveHdr* Header = (WaveHdr*)Headers + Index; + Header->Data = Buffers[Index]; + Header->BufferLength = (uint)ChunkBytes; + Header->Flags = WhdrDone; + } + } + + public void Write(ReadOnlySpan Samples) + { + int Offset = 0; + while (Offset < Samples.Length && !Disposed) + { + WaveHdr* Header = (WaveHdr*)Headers + NextBuffer; + + // A device that stops completing buffers must not wedge the engine thread, which would + // stall the guest's ring behind it; give up on the chunk instead. + while ((Volatile.Read(ref Header->Flags) & WhdrDone) == 0 && !Disposed) + { + if (!BufferDone.WaitOne(QueueMilliseconds * 2)) + return; + } + + if (Disposed) + return; + + if ((Header->Flags & ~WhdrDone) != 0) + waveOutUnprepareHeader(Device, Header, (uint)sizeof(WaveHdr)); + + int Count = Math.Min(ChunkBytes, Samples.Length - Offset); + Samples.Slice(Offset, Count).CopyTo(new Span((void*)Header->Data, Count)); + + Header->BufferLength = (uint)Count; + Header->Flags = 0; + + if (waveOutPrepareHeader(Device, Header, (uint)sizeof(WaveHdr)) != MmsysErrNoError + || waveOutWrite(Device, Header, (uint)sizeof(WaveHdr)) != MmsysErrNoError) + { + Header->Flags = WhdrDone; + return; + } + + NextBuffer = (NextBuffer + 1) % BufferCount; + Offset += Count; + } + } + + /// + /// waveOut wants a WAVEFORMATEXTENSIBLE to accept float samples. a bare tag-3 WAVEFORMATEX is + /// rejected by several drivers. + /// + private static byte[] BuildFormat(AudioSinkFormat Format) + { + byte[] Bytes = new byte[40]; + Span Cursor = Bytes; + + BitConverter.TryWriteBytes(Cursor.Slice(0x00, 2), (ushort)WaveFormatExtensible); + BitConverter.TryWriteBytes(Cursor.Slice(0x02, 2), Format.Channels); + BitConverter.TryWriteBytes(Cursor.Slice(0x04, 4), Format.SampleRate); + BitConverter.TryWriteBytes(Cursor.Slice(0x08, 4), (uint)Format.BytesPerSecond); + BitConverter.TryWriteBytes(Cursor.Slice(0x0C, 2), (ushort)Format.BlockAlign); + BitConverter.TryWriteBytes(Cursor.Slice(0x0E, 2), Format.BitsPerSample); + BitConverter.TryWriteBytes(Cursor.Slice(0x10, 2), (ushort)22); + BitConverter.TryWriteBytes(Cursor.Slice(0x12, 2), Format.BitsPerSample); + BitConverter.TryWriteBytes(Cursor.Slice(0x14, 4), Format.Channels == 1 ? 0x4u : 0x3u); + KsDataFormatSubtypeIeeeFloat.CopyTo(Cursor.Slice(0x18, 16)); + + return Bytes; + } + + private static readonly byte[] KsDataFormatSubtypeIeeeFloat = + { + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71, + }; + + public void Dispose() + { + if (Disposed) + return; + + Disposed = true; + BufferDone.Set(); + + if (Device != IntPtr.Zero) + { + waveOutReset(Device); + + for (int Index = 0; Index < BufferCount; Index++) + waveOutUnprepareHeader(Device, (WaveHdr*)Headers + Index, (uint)sizeof(WaveHdr)); + + waveOutClose(Device); + Device = IntPtr.Zero; + } + + for (int Index = 0; Index < BufferCount; Index++) + { + if (Buffers[Index] != IntPtr.Zero) + { + Marshal.FreeHGlobal(Buffers[Index]); + Buffers[Index] = IntPtr.Zero; + } + } + + if (Headers != IntPtr.Zero) + Marshal.FreeHGlobal(Headers); + + BufferDone.Dispose(); + } + } +} diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/GuiThreadManager.cs b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/GuiThreadManager.cs index 9c5fd59..038545c 100644 --- a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/GuiThreadManager.cs +++ b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/GuiThreadManager.cs @@ -12,6 +12,7 @@ internal enum GuiCommandKind : byte RenderText, GdiPrimitive, CreateWindow, + WarpCursor, Shutdown, } @@ -141,6 +142,17 @@ public IWindow CreateWindow(WindowOptions options) } } + /// + /// Moves the host pointer to a point in the window's client area. + /// + public void EnqueueWarpCursor(int clientX, int clientY) + { + if (_disposed) + return; + + Submit(new GuiCommand { Kind = GuiCommandKind.WarpCursor, X = clientX, Y = clientY }); + } + public void EnqueuePresent(string title, int width, int height, bool visible, WindowState state) { if (_disposed) @@ -395,6 +407,10 @@ private void Execute(in GuiCommand command) ExecuteCreateWindow((CreateWindowRequest)command.Request); return; + case GuiCommandKind.WarpCursor: + window?.WarpCursor(command.X, command.Y); + return; + case GuiCommandKind.Shutdown: ExecuteShutdown((TaskCompletionSource)command.Request, window); return; diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/LinuxWinManager.cs b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/LinuxWinManager.cs index ac1dae4..5768bf9 100644 --- a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/LinuxWinManager.cs +++ b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/LinuxWinManager.cs @@ -102,6 +102,10 @@ public static partial int XSendEvent( [LibraryImport("libX11.so.6")] public static partial int XFlush(IntPtr display); + [LibraryImport("libX11.so.6")] + public static partial int XWarpPointer(IntPtr display, IntPtr sourceWindow, IntPtr destinationWindow, + int sourceX, int sourceY, uint sourceWidth, uint sourceHeight, int destinationX, int destinationY); + [LibraryImport("libX11.so.6")] public static partial int XSync(IntPtr display, [MarshalAs(UnmanagedType.I4)] int discard); @@ -1586,6 +1590,14 @@ public bool Decorated } } + public void WarpCursor(int clientX, int clientY) + { + EnsureAlive(); + + X11.XWarpPointer(_display, IntPtr.Zero, _window, 0, 0, 0, 0, clientX, clientY); + X11.XFlush(_display); + } + public void Present() { EnsureAlive(); diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowManager.cs b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowManager.cs index b987df0..d99bb80 100644 --- a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowManager.cs +++ b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowManager.cs @@ -597,6 +597,8 @@ public interface IWindow : IDisposable void Present(); + void WarpCursor(int clientX, int clientY); + IntPtr NativeHandle { get; } void Show(); diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowsWinManager.cs b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowsWinManager.cs index 498f8f6..f6dcac7 100644 --- a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowsWinManager.cs +++ b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowsWinManager.cs @@ -853,6 +853,15 @@ public bool Decorated } } + public void WarpCursor(int clientX, int clientY) + { + EnsureAlive(); + + POINT Point = new POINT { X = clientX, Y = clientY }; + if (ClientToScreen(_hwnd, ref Point)) + SetCursorPos(Point.X, Point.Y); + } + public void Present() { EnsureAlive(); @@ -981,6 +990,14 @@ private struct POINT public int Y; } + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetCursorPos(int X, int Y); + [StructLayout(LayoutKind.Sequential)] private struct RECT { diff --git a/Brovan/Core/Emulation/OS/Windows/Files/NtMapViewOfSection.cs b/Brovan/Core/Emulation/OS/Windows/Files/NtMapViewOfSection.cs index 8edef5f..e62935f 100644 --- a/Brovan/Core/Emulation/OS/Windows/Files/NtMapViewOfSection.cs +++ b/Brovan/Core/Emulation/OS/Windows/Files/NtMapViewOfSection.cs @@ -133,6 +133,10 @@ internal static bool EnsureWindowsSharedSection(BinaryEmulator Instance) } ApplySharedSectionToPeb(Instance, Section.BackingAddress); + + // The PEB keeps pointers into the shared section for the lifetime of the process, so it counts + // as a view of its own. a guest that opens and closes the section handle must not free it. + Section.MappedViewCount++; return true; } @@ -181,6 +185,7 @@ public NTSTATUS Handle(BinaryEmulator Instance) Instance.WinHelper.WritePointer(BaseAddressPtr, Base); Instance.WinHelper.WritePointer(ViewSizePtr, Size); + Section.MappedViewCount++; if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) Instance.TriggerEventMessage($"[+] NtMapViewOfSection: SharedSection Base=0x{Base:X}, Size=0x{Size:X}", LogFlags.Syscall); @@ -295,6 +300,8 @@ public NTSTATUS Handle(BinaryEmulator Instance) if (!Instance.WinHelper.WritePointer(ViewSizePtr, ReturnedSize)) return NTSTATUS.STATUS_ACCESS_VIOLATION; + Section.MappedViewCount++; + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) Instance.TriggerEventMessage($"[+] NtMapViewOfSection: Section=0x{SectionHandle:X}, Base=0x{ReturnedBase:X}, Size=0x{ReturnedSize:X}, Image={Section.IsImage}, Prot=0x{Win32Protect:X}", LogFlags.Syscall); diff --git a/Brovan/Core/Emulation/OS/Windows/Misc/NtCreateEvent.cs b/Brovan/Core/Emulation/OS/Windows/Misc/NtCreateEvent.cs index 22052d5..1553ba1 100644 --- a/Brovan/Core/Emulation/OS/Windows/Misc/NtCreateEvent.cs +++ b/Brovan/Core/Emulation/OS/Windows/Misc/NtCreateEvent.cs @@ -22,8 +22,12 @@ public NTSTATUS Handle(BinaryEmulator Instance) if (EventType > 1) return NTSTATUS.STATUS_INVALID_PARAMETER; + string Name = null; + if (ObjectAttributes != 0) + Instance.WinHelper.TryReadObjectAttributesName(ObjectAttributes, out _, out _, out Name, out _); + AccessMask Permissions = (AccessMask)(uint)DesiredAccess; - WinHandle Handle = Instance.WinHelper.CreateEventHandle(null, EventType, InitialState, Permissions); + WinHandle Handle = Instance.WinHelper.CreateEventHandle(Name, EventType, InitialState, Permissions); if (!Instance.WinHelper.WritePointer(EventHandlePtr, Handle.Handle)) return NTSTATUS.STATUS_ACCESS_VIOLATION; diff --git a/Brovan/Core/Emulation/OS/Windows/Misc/NtDeleteWnfStateName.cs b/Brovan/Core/Emulation/OS/Windows/Misc/NtDeleteWnfStateName.cs new file mode 100644 index 0000000..5c4c82d --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Misc/NtDeleteWnfStateName.cs @@ -0,0 +1,10 @@ +namespace Brovan.Core.Emulation.OS.Windows +{ + internal class NtDeleteWnfStateName : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + return NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Misc/NtOpenEvent.cs b/Brovan/Core/Emulation/OS/Windows/Misc/NtOpenEvent.cs new file mode 100644 index 0000000..cb56688 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Misc/NtOpenEvent.cs @@ -0,0 +1,61 @@ +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows +{ + internal class NtOpenEvent : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + if (Instance._binary.Architecture == BinaryArchitecture.x64) + { + ulong EventHandlePtr = Instance.WinHelper.GetArg(0); + ulong DesiredAccess = (uint)Instance.WinHelper.GetArg(1); + ulong ObjectAttributesPtr = Instance.WinHelper.GetArg(2); + + return Open(Instance, EventHandlePtr, DesiredAccess, ObjectAttributesPtr); + } + else if (Instance._binary.Architecture == BinaryArchitecture.x86) + { + ulong EventHandlePtr = Instance.WinHelper.GetArg32(0); + ulong DesiredAccess = (uint)Instance.WinHelper.GetArg32(1); + ulong ObjectAttributesPtr = Instance.WinHelper.GetArg32(2); + + return Open(Instance, EventHandlePtr, DesiredAccess, ObjectAttributesPtr); + } + + return Instance.WinUnimplemented; + } + + private static NTSTATUS Open(BinaryEmulator Instance, ulong EventHandlePtr, ulong DesiredAccess, ulong ObjectAttributesPtr) + { + if (EventHandlePtr == 0 || ObjectAttributesPtr == 0) + return NTSTATUS.STATUS_INVALID_PARAMETER; + + if (!Instance.IsRegionMapped(EventHandlePtr, (uint)Instance.WinHelper.PointerSize)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + + if (!Instance.WinHelper.TryReadObjectAttributesName(ObjectAttributesPtr, out _, out _, out string FullName, out NTSTATUS ObjectNameStatus)) + return ObjectNameStatus; + + if (string.IsNullOrEmpty(FullName)) + return NTSTATUS.STATUS_OBJECT_NAME_INVALID; + + WinEvent Ev = Instance.WinHelper.HandleManager.GetObjectByObjectId(FullName); + if (Ev == null) + { + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) + Instance.TriggerEventMessage($"[!] NtOpenEvent: no event named \"{FullName}\".", LogFlags.Syscall); + + return NTSTATUS.STATUS_OBJECT_NAME_NOT_FOUND; + } + + WinHandle Handle = Instance.WinHelper.HandleManager.AddHandle(Ev, (AccessMask)(uint)DesiredAccess); + Instance.WinHelper.AddWinHandle(Handle); + + if (!Instance.WinHelper.WritePointer(EventHandlePtr, Handle.Handle)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + + return NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Misc/NtQueryPerformanceCounter.cs b/Brovan/Core/Emulation/OS/Windows/Misc/NtQueryPerformanceCounter.cs index 7520a5d..fb196ad 100644 --- a/Brovan/Core/Emulation/OS/Windows/Misc/NtQueryPerformanceCounter.cs +++ b/Brovan/Core/Emulation/OS/Windows/Misc/NtQueryPerformanceCounter.cs @@ -12,7 +12,7 @@ public NTSTATUS Handle(BinaryEmulator Instance) if (PerformanceCounterPtr == 0 || !Instance.IsRegionMapped(PerformanceCounterPtr, 8)) return NTSTATUS.STATUS_ACCESS_VIOLATION; - Instance._emulator.WriteMemory(PerformanceCounterPtr, WinSysHelper.QueryPerformanceCounterValue(), 8); + Instance._emulator.WriteMemory(PerformanceCounterPtr, Instance.GetEmulatedPerformanceCounter(), 8); if (PerformanceFrequencyPtr != 0) { diff --git a/Brovan/Core/Emulation/OS/Windows/Misc/NtUnsubscribeWnfStateChange.cs b/Brovan/Core/Emulation/OS/Windows/Misc/NtUnsubscribeWnfStateChange.cs new file mode 100644 index 0000000..f49b926 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Misc/NtUnsubscribeWnfStateChange.cs @@ -0,0 +1,10 @@ +namespace Brovan.Core.Emulation.OS.Windows +{ + internal class NtUnsubscribeWnfStateChange : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + return NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Process/NtQueryInformationToken.cs b/Brovan/Core/Emulation/OS/Windows/Process/NtQueryInformationToken.cs index 2ef02e5..251039c 100644 --- a/Brovan/Core/Emulation/OS/Windows/Process/NtQueryInformationToken.cs +++ b/Brovan/Core/Emulation/OS/Windows/Process/NtQueryInformationToken.cs @@ -508,18 +508,7 @@ NTSTATUS WriteSecurityAttributesInfo() } case TOKEN_INFORMATION_CLASS.TokenPrivateNameSpace: - { - uint RequiredSize = 8; - WriteReturnLength(RequiredSize); - - if (TokenInformationLength < RequiredSize) - return NTSTATUS.STATUS_BUFFER_TOO_SMALL; - - if (!Instance._emulator.WriteMemory(TokenInformation, 0u, 4)) - return NTSTATUS.STATUS_ACCESS_VIOLATION; - - return NTSTATUS.STATUS_SUCCESS; - } + return WriteUInt32Info(0); default: return NTSTATUS.STATUS_INVALID_INFO_CLASS; diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/LrpcPacket.cs b/Brovan/Core/Emulation/OS/Windows/RPC/LrpcPacket.cs new file mode 100644 index 0000000..b9d2d2b --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/RPC/LrpcPacket.cs @@ -0,0 +1,108 @@ +using System; +using System.Buffers.Binary; + +namespace Brovan.Core.Emulation.OS.Windows.RPC +{ + internal enum LrpcMessageType : ulong + { + Request = 0, + Bind = 1, + Fault = 2, + Response = 3, + } + + internal readonly struct LrpcMessage + { + public readonly byte[] Raw; + public readonly LrpcMessageType Type; + public readonly uint CallId; + public readonly uint ProcNumber; + public readonly uint SyntaxMask; + + public LrpcMessage(byte[] Raw, LrpcMessageType Type, uint CallId, uint ProcNumber, uint SyntaxMask) + { + this.Raw = Raw; + this.Type = Type; + this.CallId = CallId; + this.ProcNumber = ProcNumber; + this.SyntaxMask = SyntaxMask; + } + + public ReadOnlySpan StubData => + Raw != null && Raw.Length > LrpcPacket.RequestStubDataOffset + ? Raw.AsSpan(LrpcPacket.RequestStubDataOffset) + : ReadOnlySpan.Empty; + } + + internal static class LrpcPacket + { + public const int HeaderSize = 0x28; // x64 PORT_MESSAGE + public const int StubDataOffset = 0x40; + public const int RequestStubDataOffset = StubDataOffset + 0x28; + private const int OffMessageType = 0x00; + private const int OffStatus = 0x08; + private const int OffCallId = 0x0C; + private const int OffProcNumber = 0x14; + private const int OffSyntaxMask = 0x20; + + public static bool TryParse(byte[] Message, out LrpcMessage Parsed) + { + Parsed = default; + + if (Message == null || Message.Length < HeaderSize + 8) + return false; + + ReadOnlySpan Payload = Message.AsSpan(HeaderSize); + LrpcMessageType Type = (LrpcMessageType)BinaryPrimitives.ReadUInt64LittleEndian(Payload.Slice(OffMessageType, 8)); + + Parsed = new LrpcMessage( + Message, + Type, + ReadPayloadU32(Payload, OffCallId), + ReadPayloadU32(Payload, OffProcNumber), + ReadPayloadU32(Payload, OffSyntaxMask)); + + return true; + } + + public static byte[] BuildBindAccept(in LrpcMessage Request, out uint AcceptedSyntax) + { + AcceptedSyntax = Request.SyntaxMask & (uint)(-(int)Request.SyntaxMask); + if (AcceptedSyntax == 0 || Request.Raw.Length < HeaderSize + OffSyntaxMask + 4) + return null; + + byte[] Reply = (byte[])Request.Raw.Clone(); + BinaryPrimitives.WriteUInt32LittleEndian(Reply.AsSpan(HeaderSize + OffSyntaxMask, 4), AcceptedSyntax); + return Reply; + } + + public static byte[] BuildResponse(in LrpcMessage Request, ReadOnlySpan StubData) + { + byte[] Reply = NewReply(Request, StubData.Length, LrpcMessageType.Response, 0); + StubData.CopyTo(Reply.AsSpan(StubDataOffset)); + return Reply; + } + + public static byte[] BuildFault(in LrpcMessage Request, uint Status) + { + return NewReply(Request, 0, LrpcMessageType.Fault, Status); + } + + private static byte[] NewReply(in LrpcMessage Request, int StubLength, LrpcMessageType Type, uint Status) + { + byte[] Reply = new byte[StubDataOffset + StubLength]; + Array.Copy(Request.Raw, Reply, Math.Min(HeaderSize, Request.Raw.Length)); + + Span Payload = Reply.AsSpan(HeaderSize); + BinaryPrimitives.WriteUInt64LittleEndian(Payload.Slice(OffMessageType, 8), (ulong)Type); + BinaryPrimitives.WriteUInt32LittleEndian(Payload.Slice(OffStatus, 4), Status); + BinaryPrimitives.WriteUInt32LittleEndian(Payload.Slice(OffCallId, 4), Request.CallId); + return Reply; + } + + private static uint ReadPayloadU32(ReadOnlySpan Payload, int Offset) + { + return Payload.Length < Offset + 4 ? 0 : BinaryPrimitives.ReadUInt32LittleEndian(Payload.Slice(Offset, 4)); + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs b/Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs new file mode 100644 index 0000000..5a8d3d5 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs @@ -0,0 +1,236 @@ +using System; +using System.Buffers.Binary; +using System.Text; + +namespace Brovan.Core.Emulation.OS.Windows.RPC +{ + /// + /// Marshalling for the NDR 2.0 transfer syntax (8a885d04-1ceb-11c9-9fe8-08002b104860), which is what + /// the audio interfaces negotiate. + /// + internal struct Ndr20Writer + { + public const int ContextHandleSize = 20; + + private byte[] Buffer; + private int Position; + private uint NextReferentId; + + public Ndr20Writer(int Capacity) + { + Buffer = new byte[Math.Max(Capacity, 16)]; + Position = 0; + NextReferentId = 0x00020000; + } + + public void WriteUInt32(uint Value) + { + Reserve(4); + BinaryPrimitives.WriteUInt32LittleEndian(Buffer.AsSpan(Position, 4), Value); + Position += 4; + } + + public void WriteInt32(int Value) + { + WriteUInt32(unchecked((uint)Value)); + } + + public void WriteBytes(ReadOnlySpan Value) + { + Reserve(Value.Length); + Value.CopyTo(Buffer.AsSpan(Position, Value.Length)); + Position += Value.Length; + } + + public void WriteUInt16(ushort Value) + { + Reserve(2); + BinaryPrimitives.WriteUInt16LittleEndian(Buffer.AsSpan(Position, 2), Value); + Position += 2; + } + + public void WriteUInt64(ulong Value) + { + AlignTo(8); + Reserve(8); + BinaryPrimitives.WriteUInt64LittleEndian(Buffer.AsSpan(Position, 8), Value); + Position += 8; + } + + public void WriteFloat(float Value) + { + AlignTo(4); + Reserve(4); + BinaryPrimitives.WriteSingleLittleEndian(Buffer.AsSpan(Position, 4), Value); + Position += 4; + } + + /// + /// Emits the referent id that stands in for a non-null unique pointer. The pointee follows it. + /// + public void WriteUniqueReferent() + { + WriteUInt32(NextReferentId); + NextReferentId += 4; + } + + public void WriteContextHandle(ReadOnlySpan Cookie) + { + AlignTo(4); + Reserve(ContextHandleSize); + Buffer.AsSpan(Position, ContextHandleSize).Clear(); + Cookie.Slice(0, Math.Min(ContextHandleSize, Cookie.Length)).CopyTo(Buffer.AsSpan(Position, ContextHandleSize)); + Position += ContextHandleSize; + } + + public void WriteSystemHandle(int HandleIndex) + { + AlignTo(4); + WriteUInt32(HandleIndex < 0 ? 0u : (uint)HandleIndex + 1); + WriteUInt32(0); + } + + public void AlignTo(int Alignment) + { + int Padded = (Position + Alignment - 1) & ~(Alignment - 1); + Reserve(Padded - Position); + Buffer.AsSpan(Position, Padded - Position).Clear(); + Position = Padded; + } + + public void WriteUniqueWideString(string Value) + { + if (Value == null) + { + WriteUInt32(0); + return; + } + + WriteUInt32(NextReferentId); + NextReferentId += 4; + + uint CharCount = (uint)Value.Length + 1; + WriteUInt32(CharCount); + WriteUInt32(0); + WriteUInt32(CharCount); + + int Bytes = (int)CharCount * 2; + Reserve(Bytes); + Buffer.AsSpan(Position, Bytes).Clear(); + Encoding.Unicode.GetBytes(Value, Buffer.AsSpan(Position, Bytes - 2)); + Position += Bytes; + AlignTo(4); + } + + public byte[] ToArray() + { + byte[] Result = new byte[Position]; + Array.Copy(Buffer, Result, Position); + return Result; + } + + private void Reserve(int Count) + { + if (Position + Count <= Buffer.Length) + return; + + int Capacity = Buffer.Length; + while (Capacity < Position + Count) + Capacity *= 2; + + Array.Resize(ref Buffer, Capacity); + } + } + + internal ref struct Ndr20Reader + { + private readonly ReadOnlySpan Data; + private int Position; + + public Ndr20Reader(ReadOnlySpan Data) + { + this.Data = Data; + Position = 0; + } + + public bool TryReadUInt16(out ushort Value) + { + Value = 0; + if (Position + 2 > Data.Length) + return false; + + Value = BinaryPrimitives.ReadUInt16LittleEndian(Data.Slice(Position, 2)); + Position += 2; + return true; + } + + public bool TryReadUInt32(out uint Value) + { + Value = 0; + if (Position + 4 > Data.Length) + return false; + + Value = BinaryPrimitives.ReadUInt32LittleEndian(Data.Slice(Position, 4)); + Position += 4; + return true; + } + + public bool TryReadUInt64(out ulong Value) + { + Value = 0; + Align(8); + if (Position + 8 > Data.Length) + return false; + + Value = BinaryPrimitives.ReadUInt64LittleEndian(Data.Slice(Position, 8)); + Position += 8; + return true; + } + + public bool TryReadContextHandle(out ReadOnlySpan Cookie) + { + Cookie = default; + Align(4); + if (Position + Ndr20Writer.ContextHandleSize > Data.Length) + return false; + + Cookie = Data.Slice(Position, Ndr20Writer.ContextHandleSize); + Position += Ndr20Writer.ContextHandleSize; + return true; + } + + public void Align(int Alignment) + { + Position = (Position + Alignment - 1) & ~(Alignment - 1); + } + + public bool TryReadConformantWideString(out string Value) + { + Value = null; + + if (!TryReadUInt32(out uint MaxCount) || !TryReadUInt32(out _) || !TryReadUInt32(out uint ActualCount)) + return false; + + if (ActualCount > MaxCount) + return false; + + int Bytes = checked((int)ActualCount * 2); + if (Position + Bytes > Data.Length) + return false; + + Value = Encoding.Unicode.GetString(Data.Slice(Position, Bytes)).TrimEnd('\0'); + Position += (Bytes + 3) & ~3; + return true; + } + + public bool TryReadUniqueWideString(out string Value) + { + Value = null; + + if (!TryReadUInt32(out uint ReferentId)) + return false; + + return ReferentId == 0 || TryReadConformantWideString(out Value); + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcConnectPortEx.cs b/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcConnectPortEx.cs index ce5c815..1aa59ae 100644 --- a/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcConnectPortEx.cs +++ b/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcConnectPortEx.cs @@ -23,7 +23,12 @@ public NTSTATUS Handle(BinaryEmulator Instance) return NTSTATUS.STATUS_OBJECT_NAME_INVALID; if (!Instance.WinHelper.WinPorts.Any(Port => string.Equals(Port.Name, PortName, StringComparison.OrdinalIgnoreCase))) + { + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) + Instance.TriggerEventMessage($"[!] NtAlpcConnectPortEx: no port \"{PortName}\".", LogFlags.Syscall); + return NTSTATUS.STATUS_ACCESS_DENIED; + } return NtAlpcConnectPort.Connect(Instance, PortHandlePtr, PortName); } diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcQueryInformationMessage.cs b/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcQueryInformationMessage.cs new file mode 100644 index 0000000..7cc712f --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcQueryInformationMessage.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows +{ + internal class NtAlpcQueryInformationMessage : IWinSyscall + { + private const uint AlpcMessageHandleInformation = 3; + private const int HandleInformationSize = 20; + + public NTSTATUS Handle(BinaryEmulator Instance) + { + ulong PortHandle = Instance.WinHelper.GetArg(0); + uint InformationClass = (uint)Instance.WinHelper.GetArg(2); + ulong InformationPtr = Instance.WinHelper.GetArg(3); + uint Length = (uint)Instance.WinHelper.GetArg(4); + ulong ReturnLengthPtr = Instance.WinHelper.GetArg(5); + + WinPort Port = Instance.WinHelper.HandleManager.GetObjectByHandle(PortHandle); + if (Port == null) + return NTSTATUS.STATUS_INVALID_HANDLE; + + if (InformationClass != AlpcMessageHandleInformation) + return NTSTATUS.STATUS_INVALID_INFO_CLASS; + + if (ReturnLengthPtr != 0 && Instance.IsRegionMapped(ReturnLengthPtr, 4)) + Instance._emulator.WriteMemory(ReturnLengthPtr, (uint)HandleInformationSize); + + if (Length < HandleInformationSize) + return NTSTATUS.STATUS_BUFFER_TOO_SMALL; + + if (InformationPtr == 0 || !Instance.IsRegionMapped(InformationPtr, HandleInformationSize)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + + List Delivered = Port.DeliveredHandles; + if (Delivered == null || Delivered.Count == 0) + return NTSTATUS.STATUS_NO_MORE_ENTRIES; + + uint Index = Instance._emulator.ReadMemoryUInt(InformationPtr); + if (Index >= (uint)Delivered.Count) + return NTSTATUS.STATUS_NO_MORE_ENTRIES; + + ulong Delivering = Delivered[(int)Index]; + + if ((Instance.Settings.Flags & LogFlags.General) != 0) + Instance.TriggerEventMessage($"[+] ALPC handle 0x{Delivering:X} claimed from \"{Port.Name}\" (index {Index}).", LogFlags.General); + + Instance._emulator.WriteMemory(InformationPtr + 0x00, Index); + Instance._emulator.WriteMemory(InformationPtr + 0x04, 0u); + Instance._emulator.WriteMemory(InformationPtr + 0x08, (uint)Delivering); + Instance._emulator.WriteMemory(InformationPtr + 0x0C, AlpcObjectType(Instance, Delivering)); + Instance._emulator.WriteMemory(InformationPtr + 0x10, (uint)AccessMask.StandardRightsAll); + + return NTSTATUS.STATUS_SUCCESS; + } + + private static uint AlpcObjectType(BinaryEmulator Instance, ulong Handle) + { + IHandleObject Object = Instance.WinHelper.HandleManager.GetObjectByHandle(Handle); + + switch (Object?.ObjectType) + { + case HandleType.FileHandle: return 0x0001; + case HandleType.ThreadHandle: return 0x0004; + case HandleType.SemaphoreHandle: return 0x0008; + case HandleType.EventHandle: return 0x0010; + case HandleType.ProcessHandle: return 0x0020; + case HandleType.MutexHandle: return 0x0040; + case HandleType.SectionHandle: return 0x0080; + case HandleType.RegistryKeyHandle: return 0x0100; + case HandleType.TokenHandle: return 0x0200; + case HandleType.JobHandle: return 0x0800; + default: return 0x0001; + } + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcSendWaitReceivePort.cs b/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcSendWaitReceivePort.cs index 0c52c65..08e02ab 100644 --- a/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcSendWaitReceivePort.cs +++ b/Brovan/Core/Emulation/OS/Windows/RPC/NtAlpcSendWaitReceivePort.cs @@ -8,6 +8,17 @@ namespace Brovan.Core.Emulation.OS.Windows internal sealed class NtAlpcSendWaitReceivePort : IWinSyscall { private const int PortMessageHeaderSize = 0x28; + + private const uint AlpcMessageSecurityAttribute = 0x80000000; + private const uint AlpcMessageViewAttribute = 0x40000000; + private const uint AlpcMessageContextAttribute = 0x20000000; + private const uint AlpcMessageHandleAttribute = 0x10000000; + + private const int AlpcAttributeHeaderSize = 8; + private const int AlpcSecurityAttributeSize = 24; + private const int AlpcViewAttributeSize = 32; + private const int AlpcContextAttributeSize = 32; + private const int AlpcHandleAttributeSize = 24; private const int OffPmDataLength = 0x00; private const int OffPmTotalLength = 0x02; private const int OffPmMessageId = 0x18; @@ -76,28 +87,40 @@ public NTSTATUS Handle(BinaryEmulator Instance) SendBytes = Instance.ReadMemory(SendMessagePtr, SendTotalLength); } + uint AllocatedAttributes = 0; if (ReceiveMessageAttributesPtr != 0 && Instance.IsRegionMapped(ReceiveMessageAttributesPtr, 8)) { - Instance._emulator.WriteMemory(ReceiveMessageAttributesPtr + 0, 0u); + AllocatedAttributes = Instance._emulator.ReadMemoryUInt(ReceiveMessageAttributesPtr); + Instance._emulator.WriteMemory(ReceiveMessageAttributesPtr + 0, AllocatedAttributes); Instance._emulator.WriteMemory(ReceiveMessageAttributesPtr + 4, 0u); } if (SendBytes != null && (Instance.Settings.Flags & LogFlags.General) != 0) - Instance.TriggerEventMessage($"[+] ALPC send \"{Port.Name}\" len=0x{SendBytes.Length:X}", LogFlags.General); + Instance.TriggerEventMessage($"[+] ALPC send \"{Port.Name}\" len=0x{SendBytes.Length:X} reply={(ReceiveMessagePtr != 0 ? "yes" : "no")}", LogFlags.General); + + if (SendBytes == null && ReceiveMessagePtr != 0 && (Instance.Settings.Flags & LogFlags.General) != 0) + Instance.TriggerEventMessage($"[+] ALPC receive-only on \"{Port.Name}\"", LogFlags.General); if (ReceiveMessagePtr != 0) { byte[] ReplyBytes = null; + PortReply Reply = null; if (SendBytes != null && Port.Handler != null) { - Port.Handler(Port, SendBytes, out byte[] HandlerReply, Instance); - ReplyBytes = HandlerReply ?? SendBytes; + Reply = new PortReply(); + Port.ReceivedHandles = ReadHandleAttribute(Instance, SendMessageAttributesPtr); + Port.Handler(Port, SendBytes, Reply, Instance); + ReplyBytes = Reply.Data ?? SendBytes; } else if (SendBytes != null) { ReplyBytes = SendBytes; } + Port.DeliveredHandles = Reply?.Handles; + if (Port.DeliveredHandles != null) + PublishHandleAttribute(Instance, ReceiveMessageAttributesPtr, AllocatedAttributes, Port.DeliveredHandles.Count); + ulong WriteLength = (ulong)(ReplyBytes?.Length ?? PortMessageHeaderSize); if (WriteLength > ReceiveBufferLength) { @@ -156,6 +179,58 @@ public NTSTATUS Handle(BinaryEmulator Instance) return NTSTATUS.STATUS_TIMEOUT; } + /// + /// Announces handles that travel with the reply. + /// + private static void PublishHandleAttribute(BinaryEmulator Instance, ulong AttributesPtr, uint AllocatedAttributes, int HandleCount) + { + if (AttributesPtr == 0 || HandleCount <= 0 || (AllocatedAttributes & AlpcMessageHandleAttribute) == 0) + return; + + ulong Offset = AlpcAttributeHeaderSize; + if ((AllocatedAttributes & AlpcMessageSecurityAttribute) != 0) + Offset += AlpcSecurityAttributeSize; + if ((AllocatedAttributes & AlpcMessageViewAttribute) != 0) + Offset += AlpcViewAttributeSize; + if ((AllocatedAttributes & AlpcMessageContextAttribute) != 0) + Offset += AlpcContextAttributeSize; + + if (!Instance.IsRegionMapped(AttributesPtr + Offset, AlpcHandleAttributeSize)) + return; + + Instance._emulator.WriteMemory(AttributesPtr + 4, AlpcMessageHandleAttribute); + Instance._emulator.WriteMemory(AttributesPtr + Offset + 0x00, 0u); + Instance._emulator.WriteMemory(AttributesPtr + Offset + 0x08, 0UL, 8); + Instance._emulator.WriteMemory(AttributesPtr + Offset + 0x10, (uint)HandleCount); + } + + private static List ReadHandleAttribute(BinaryEmulator Instance, ulong AttributesPtr) + { + if (AttributesPtr == 0 || !Instance.IsRegionMapped(AttributesPtr, AlpcAttributeHeaderSize)) + return null; + + uint ValidAttributes = Instance._emulator.ReadMemoryUInt(AttributesPtr + 4); + if ((ValidAttributes & AlpcMessageHandleAttribute) == 0) + return null; + + ulong Offset = AlpcAttributeHeaderSize; + if ((ValidAttributes & AlpcMessageSecurityAttribute) != 0) + Offset += AlpcSecurityAttributeSize; + if ((ValidAttributes & AlpcMessageViewAttribute) != 0) + Offset += AlpcViewAttributeSize; + if ((ValidAttributes & AlpcMessageContextAttribute) != 0) + Offset += AlpcContextAttributeSize; + + if (!Instance.IsRegionMapped(AttributesPtr + Offset, AlpcHandleAttributeSize)) + return null; + + ulong Handle = Instance._emulator.ReadMemoryULong(AttributesPtr + Offset + 0x08); + if (Handle == 0) + return null; + + return new List { Handle }; + } + private static void FinalizeReplyHeader(Span Reply, ushort TotalLength) { if (Reply.Length < PortMessageHeaderSize) diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/NtConnectPort.cs b/Brovan/Core/Emulation/OS/Windows/RPC/NtConnectPort.cs index d7a2f09..829dfc5 100644 --- a/Brovan/Core/Emulation/OS/Windows/RPC/NtConnectPort.cs +++ b/Brovan/Core/Emulation/OS/Windows/RPC/NtConnectPort.cs @@ -115,6 +115,10 @@ public NTSTATUS Handle(BinaryEmulator Instance) if (StructSerializer.WriteStruct(Instance, ClientViewPtr, ClientView) != WriteStructResult.Ok) return NTSTATUS.STATUS_ACCESS_VIOLATION; + // CsrClientConnectToServer closes the section handle as soon as the connect returns and + // keeps using this view, so the port view has to hold the section alive on its own. + PortSection.MappedViewCount++; + if (ServerViewPtr != 0) { if (!Instance.IsRegionMapped(ServerViewPtr, 0x18)) diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/Ports/ApiPort.cs b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/ApiPort.cs index 281cd4d..2e67521 100644 --- a/Brovan/Core/Emulation/OS/Windows/RPC/Ports/ApiPort.cs +++ b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/ApiPort.cs @@ -88,21 +88,21 @@ private enum EventLogOpnum : ushort ElfrReportEventExA = 26 } - public static NTSTATUS Handle(WinPort Port, byte[] SendData, out byte[] ReplyData, BinaryEmulator Instance) + public static NTSTATUS Handle(WinPort Port, byte[] SendData, PortReply Reply, BinaryEmulator Instance) { if (SendData == null || SendData.Length < PmHeaderSize) { - ReplyData = BuildMinimalReply(); + Reply.Data = BuildMinimalReply(); return NTSTATUS.STATUS_SUCCESS; } if (IsCsrApiPort(Port?.Name)) { - ReplyData = HandleCsrPort(Port, SendData, Instance); + Reply.Data = HandleCsrPort(Port, SendData, Instance); return NTSTATUS.STATUS_SUCCESS; } - ReplyData = HandleGenericRpcPort(Port, SendData, Instance); + Reply.Data = HandleGenericRpcPort(Port, SendData, Instance); return NTSTATUS.STATUS_SUCCESS; } @@ -203,11 +203,27 @@ private static byte[] HandleGenericRpcPort(WinPort Port, byte[] SendData, Binary return RpcReply; } + if (LrpcPacket.TryParse(SendData, out LrpcMessage Message)) + { + if (Message.Type == LrpcMessageType.Bind) + return LrpcPacket.BuildBindAccept(Message, out _); + + if (Message.Type == LrpcMessageType.Request) + { + if ((Instance.Settings.Flags & LogFlags.General) != 0) + Instance.TriggerEventMessage($"[!] No server for proc {Message.ProcNumber} on \"{Port?.Name}\"; faulting.", LogFlags.General); + + return LrpcPacket.BuildFault(Message, RpcSProcnumOutOfRange); + } + } + byte[] Reply = (byte[])SendData.Clone(); PreparePortReply(Reply); return Reply; } + private const uint RpcSProcnumOutOfRange = 1745; + private static void HandleCsrSrvConnect(byte[] Reply, BinaryEmulator Instance) { if (Reply.Length < OffCsrDataStart + 0x18) diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioSrvPort.cs b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioSrvPort.cs new file mode 100644 index 0000000..0f55f77 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioSrvPort.cs @@ -0,0 +1,656 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Text; +using Brovan.Core.Emulation.OS.SharedHelpers; +using Brovan.Core.Helpers; + +namespace Brovan.Core.Emulation.OS.Windows.RPC.Ports +{ + public static class AudioSrvPortHandler + { + public static readonly string[] PortNames = + { + AudioSrvPort, + AudioClientRpcPort, + }; + + private const string AudioSrvPort = "\\RPC Control\\Audiosrv"; + private const string AudioClientRpcPort = "\\RPC Control\\AudioClientRpc"; + + private const uint ProcGetPnpState = 0; + private const uint ProcGetDefaultAudioEndpoint = 25; + private const int PnpStateSize = 8; + private const uint ProcGetMixFormat = 0; + private const uint ProcGetDevicePeriod = 2; + private const uint ProcInitialize = 4; + private const uint ProcRelease = 5; + private const uint ProcSetupStream = 7; + private const uint ProcStartStream = 8; + private const uint ProcStopStream = 9; + + private static readonly byte[] RenderEndpointClass = + new Guid("cd773740-b187-4974-a1d5-e0ff91372277").ToByteArray(); + + private sealed class AudioStream + { + public uint Id; + public string EndpointId; + public uint ShareMode; + public uint StreamFlags; + public ulong ControlBlock; + public ulong SectionHandle; + public uint RingBytes; + public uint PeriodFrames; + public string HandlePortName; + public AudioStreamEngine Engine; + } + + private static readonly Dictionary Streams = new Dictionary(); + private static uint NextStreamId = 1; + + private const uint ContextCookieMagic = 0x4F445541; // 'AUDO' + + private const ulong OffVersion = 0x000; + private const ulong OffTotalSize = 0x004; + private const ulong OffVolatileQueueRead = 0x008; + private const ulong OffVolatileQueueWrite = 0x00C; + private const ulong OffClientCursor = 0x018; + private const ulong OffServerCursor = 0x020; + private const ulong OffVolatileFlags = 0x0AC; + private const ulong OffMagic = 0x0C8; + private const ulong OffStaticSize = 0x0CC; + private const ulong OffHandlePortName = 0x0D0; + private const ulong OffQueueCount = 0x150; + private const ulong OffBufferStart = 0x16C; + private const ulong OffBufferEnd = 0x170; + private const ulong OffBufferLimit = 0x174; + private const ulong OffWaveFormat = 0x180; + + private const uint ControlDataVersion = 1; + private const uint ControlDataMagic = 0x45504344; // 'DCPE' + private const uint StaticControlDataSize = 220; + + private const uint RingBufferOffset = 0x200; + private const uint DefaultBufferHns = 10_000_000; + private const uint DefaultDevicePeriodHns = 100_000; + private const uint MinimumDevicePeriodHns = 30_000; + private const string HandlePortPrefix = "\\BaseNamedObjects\\AudioEngineDuplicateHandleApiPort"; + private const int HandlePortMessageSize = 48; + private const int OffHandlePortStatus = 44; + private const uint MaxRingBytes = 8 * 1024 * 1024; + private const uint HundredNanosecondsPerSecond = 10_000_000; + + private const uint PageReadWrite = 0x04; + private const uint ErrorNotEnoughMemory = 8; + + private const ushort WaveFormatExtensible = 0xFFFE; + private const ushort MixChannels = 2; + private const uint MixSampleRate = 48000; + private const ushort MixBitsPerSample = 32; + private const ushort MixBlockAlign = MixChannels * (MixBitsPerSample / 8); + private const ushort WaveFormatExtensibleTail = 22; + private const uint SpeakerFrontLeftRight = 3; + + /// + /// The engine mix format, as a WAVEFORMATEXTENSIBLE. + /// + private static byte[] BuildMixFormat() + { + byte[] Format = new byte[18 + WaveFormatExtensibleTail]; + Span Cursor = Format; + + BinaryPrimitives.WriteUInt16LittleEndian(Cursor.Slice(0x00, 2), WaveFormatExtensible); + BinaryPrimitives.WriteUInt16LittleEndian(Cursor.Slice(0x02, 2), MixChannels); + BinaryPrimitives.WriteUInt32LittleEndian(Cursor.Slice(0x04, 4), MixSampleRate); + BinaryPrimitives.WriteUInt32LittleEndian(Cursor.Slice(0x08, 4), MixSampleRate * MixBlockAlign); + BinaryPrimitives.WriteUInt16LittleEndian(Cursor.Slice(0x0C, 2), MixBlockAlign); + BinaryPrimitives.WriteUInt16LittleEndian(Cursor.Slice(0x0E, 2), MixBitsPerSample); + BinaryPrimitives.WriteUInt16LittleEndian(Cursor.Slice(0x10, 2), WaveFormatExtensibleTail); + BinaryPrimitives.WriteUInt16LittleEndian(Cursor.Slice(0x12, 2), MixBitsPerSample); + BinaryPrimitives.WriteUInt32LittleEndian(Cursor.Slice(0x14, 4), SpeakerFrontLeftRight); + KsDataFormatSubtypeIeeeFloat.CopyTo(Cursor.Slice(0x18, 16)); + + return Format; + } + + private static readonly byte[] KsDataFormatSubtypeIeeeFloat = + { + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71, + }; + + private const uint RpcSProcnumOutOfRange = 1745; + private const uint ErrorNotFound = 1168; + + private const uint DeviceStateActive = 1; + private const string MMDevicesKey = "\\Registry\\Machine\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\MMDevices\\Audio"; + + private const int MaxDumpBytes = 512; + private const int DumpBytesPerLine = 32; + + public static NTSTATUS Handle(WinPort Port, byte[] SendData, PortReply Reply, BinaryEmulator Instance) + { + if (!LrpcPacket.TryParse(SendData, out LrpcMessage Message)) + return NTSTATUS.STATUS_INVALID_PARAMETER; + + switch (Message.Type) + { + case LrpcMessageType.Bind: + Reply.Data = LrpcPacket.BuildBindAccept(Message, out uint AcceptedSyntax); + Log(Instance, $"bind on \"{Port?.Name}\" accepted with syntax 0x{AcceptedSyntax:X}."); + break; + + case LrpcMessageType.Request: + Reply.Data = DispatchRequest(Port, Message, Reply, Instance); + break; + + default: + Log(Instance, $"unhandled message type {(ulong)Message.Type} on \"{Port?.Name}\" ({SendData.Length} bytes)."); + Dump(Instance, "unhandled", SendData, LrpcPacket.HeaderSize); + break; + } + + if (Reply.Data == null) + { + Reply.Data = new byte[LrpcPacket.HeaderSize]; + Array.Copy(SendData, Reply.Data, Math.Min(LrpcPacket.HeaderSize, SendData.Length)); + } + + return NTSTATUS.STATUS_SUCCESS; + } + + private static byte[] DispatchRequest(WinPort Port, in LrpcMessage Message, PortReply Reply, BinaryEmulator Instance) + { + bool IsStreamPort = string.Equals(Port?.Name, AudioClientRpcPort, StringComparison.OrdinalIgnoreCase); + + byte[] Response = IsStreamPort + ? DispatchAudioClient(Message, Reply, Instance) + : DispatchAudioSrv(Message, Instance); + + if (Response != null) + return Response; + + Log(Instance, $"unimplemented proc {Message.ProcNumber} on \"{Port?.Name}\" ({Message.StubData.Length} stub bytes)."); + DumpStubData(Instance, Message); + return LrpcPacket.BuildFault(Message, RpcSProcnumOutOfRange); + } + + private static byte[] DispatchAudioSrv(in LrpcMessage Message, BinaryEmulator Instance) + { + switch (Message.ProcNumber) + { + case ProcGetPnpState: + return GetPnpState(Message, Instance); + + case ProcGetDefaultAudioEndpoint: + return GetDefaultAudioEndpoint(Message, Instance); + + default: + return null; + } + } + + private static byte[] GetPnpState(in LrpcMessage Message, BinaryEmulator Instance) + { + Log(Instance, "PnP state polled."); + + Ndr20Writer Writer = new Ndr20Writer(32); + Writer.WriteUInt32(PnpStateSize); + Writer.WriteUniqueReferent(); + Writer.WriteUInt32(PnpStateSize); + Writer.WriteBytes(new byte[PnpStateSize]); + Writer.WriteInt32(0); + return LrpcPacket.BuildResponse(Message, Writer.ToArray()); + } + + + private static byte[] DispatchAudioClient(in LrpcMessage Message, PortReply Reply, BinaryEmulator Instance) + { + switch (Message.ProcNumber) + { + case ProcGetMixFormat: + return GetMixFormat(Message, Instance); + + case ProcGetDevicePeriod: + return GetDevicePeriod(Message, Instance); + + case ProcInitialize: + return InitializeStream(Message, Instance); + + case ProcRelease: + return ReleaseStream(Message, Instance); + + case ProcSetupStream: + return SetupStream(Message, Reply, Instance); + + case ProcStartStream: + case ProcStopStream: + return AcknowledgeStreamTransition(Message, Instance); + + default: + return null; + } + } + + private static byte[] AcknowledgeStreamTransition(in LrpcMessage Message, BinaryEmulator Instance) + { + Ndr20Reader Reader = new Ndr20Reader(Message.StubData); + Reader.TryReadContextHandle(out ReadOnlySpan Cookie); + + if (TryGetStream(Cookie, out AudioStream Stream)) + Log(Instance, $"stream {Stream.Id} {(Message.ProcNumber == ProcStartStream ? "started" : "stopped")}."); + + Ndr20Writer Writer = new Ndr20Writer(16); + Writer.WriteInt32(0); + return LrpcPacket.BuildResponse(Message, Writer.ToArray()); + } + + private static byte[] InitializeStream(in LrpcMessage Message, BinaryEmulator Instance) + { + Ndr20Reader Reader = new Ndr20Reader(Message.StubData); + Reader.TryReadConformantWideString(out string EndpointId); + Reader.TryReadUInt16(out ushort ShareMode); + Reader.Align(4); + Reader.TryReadUInt32(out uint StreamFlags); + + AudioStream Stream = new AudioStream + { + Id = NextStreamId++, + EndpointId = EndpointId, + ShareMode = ShareMode, + StreamFlags = StreamFlags, + }; + + Streams[Stream.Id] = Stream; + Log(Instance, $"stream {Stream.Id} initialize: share mode {ShareMode}, flags 0x{StreamFlags:X}, endpoint {EndpointId}."); + + Ndr20Writer Writer = new Ndr20Writer(128); + Writer.WriteUniqueWideString(Stream.EndpointId); + Writer.WriteContextHandle(BuildContextCookie(Stream.Id)); + Writer.WriteInt32(0); + return LrpcPacket.BuildResponse(Message, Writer.ToArray()); + } + + private static byte[] SetupStream(in LrpcMessage Message, PortReply Reply, BinaryEmulator Instance) + { + Ndr20Reader Reader = new Ndr20Reader(Message.StubData); + Reader.TryReadContextHandle(out ReadOnlySpan Cookie); + Reader.TryReadUInt16(out ushort ShareMode); + Reader.TryReadUInt64(out ulong BufferHns); + Reader.TryReadUInt64(out ulong PeriodHns); + + if (!TryGetStream(Cookie, out AudioStream Stream)) + { + Log(Instance, "stream setup for an unknown context handle."); + return LrpcPacket.BuildFault(Message, ErrorNotFound); + } + + if (BufferHns == 0) + BufferHns = DefaultBufferHns; + + Stream.RingBytes = RingBytesForDuration(BufferHns); + Stream.PeriodFrames = FramesForDuration(PeriodHns != 0 ? PeriodHns : BufferHns); + Stream.ShareMode = ShareMode; + + if (!TryCreateSharedBuffer(Instance, Stream)) + return LrpcPacket.BuildFault(Message, ErrorNotEnoughMemory); + + int SectionIndex = Reply.Handles?.Count ?? 0; + Reply.AttachHandle(Stream.SectionHandle); + + Log(Instance, $"stream {Stream.Id} setup: {Stream.RingBytes} byte ring, {Stream.PeriodFrames} frames per period, section handle 0x{Stream.SectionHandle:X}."); + + Ndr20Writer Writer = new Ndr20Writer(256); + WriteSystemAudioStream(ref Writer, Stream, SectionIndex); + Writer.WriteInt32(0); + return LrpcPacket.BuildResponse(Message, Writer.ToArray()); + } + + private static byte[] ReleaseStream(in LrpcMessage Message, BinaryEmulator Instance) + { + Ndr20Reader Reader = new Ndr20Reader(Message.StubData); + Reader.TryReadContextHandle(out ReadOnlySpan Cookie); + + if (TryGetStream(Cookie, out AudioStream Stream)) + { + Log(Instance, $"stream {Stream.Id} released after {Stream.Engine?.RenderedBytes ?? 0} bytes rendered."); + Stream.Engine?.Dispose(); + Streams.Remove(Stream.Id); + } + + Ndr20Writer Writer = new Ndr20Writer(32); + Writer.WriteContextHandle(ReadOnlySpan.Empty); + Writer.WriteInt32(0); + return LrpcPacket.BuildResponse(Message, Writer.ToArray()); + } + + private static byte[] GetMixFormat(in LrpcMessage Message, BinaryEmulator Instance) + { + Log(Instance, $"mix format {MixChannels}ch {MixSampleRate}Hz float{MixBitsPerSample}."); + + Ndr20Writer Writer = new Ndr20Writer(96); + Writer.WriteUniqueReferent(); + Writer.WriteUInt32(WaveFormatExtensibleTail); + Writer.WriteBytes(BuildMixFormat()); + Writer.AlignTo(4); + Writer.WriteInt32(0); + return LrpcPacket.BuildResponse(Message, Writer.ToArray()); + } + + private static byte[] GetDevicePeriod(in LrpcMessage Message, BinaryEmulator Instance) + { + Log(Instance, $"device period {DefaultDevicePeriodHns / 10_000} ms default, {MinimumDevicePeriodHns / 10_000} ms minimum."); + + Ndr20Writer Writer = new Ndr20Writer(48); + Writer.WriteUniqueReferent(); + Writer.WriteUInt64(DefaultDevicePeriodHns); + Writer.WriteUniqueReferent(); + Writer.WriteUInt64(MinimumDevicePeriodHns); + Writer.WriteInt32(0); + return LrpcPacket.BuildResponse(Message, Writer.ToArray()); + } + + private static byte[] GetDefaultAudioEndpoint(in LrpcMessage Message, BinaryEmulator Instance) + { + Ndr20Reader Reader = new Ndr20Reader(Message.StubData); + Reader.TryReadUInt16(out ushort DataFlow); + + string Direction = DataFlow == 1 ? "Capture" : "Render"; + if (!TryFindActiveEndpoint(Instance, Direction, out string EndpointId)) + { + Log(Instance, $"no active {Direction} endpoint in the registry."); + return LrpcPacket.BuildFault(Message, ErrorNotFound); + } + + Log(Instance, $"default {Direction} endpoint -> {EndpointId}."); + + Ndr20Writer Writer = new Ndr20Writer(96); + Writer.WriteUniqueWideString(EndpointId); + Writer.WriteUInt32(0); + Writer.WriteInt32(0); + return LrpcPacket.BuildResponse(Message, Writer.ToArray()); + } + + private static void WriteSystemAudioStream(ref Ndr20Writer Writer, AudioStream Stream, int SectionIndex) + { + Writer.AlignTo(8); + Writer.WriteBytes(RenderEndpointClass); + Writer.WriteUInt32(0); + Writer.WriteSystemHandle(-1); + Writer.WriteUInt64(0); + Writer.WriteUInt64(0); + WriteHandleBlob(ref Writer, -1); + WriteHandleBlob(ref Writer, -1); + WriteHandleBlob(ref Writer, -1); + Writer.WriteUInt32(0); + Writer.WriteUInt32(0); + WriteHandleBlob(ref Writer, SectionIndex); + Writer.WriteUInt32(1); + Writer.WriteUInt32(Stream.PeriodFrames); + Writer.WriteFloat(MixSampleRate); + Writer.WriteUInt32(0); + } + + private static void WriteHandleBlob(ref Ndr20Writer Writer, int HandleIndex) + { + uint Tag = HandleIndex < 0 ? 0u : 1u; + + Writer.AlignTo(4); + Writer.WriteUInt16((ushort)Tag); + Writer.AlignTo(4); + Writer.WriteUInt32(Tag); + + if (HandleIndex >= 0) + Writer.WriteSystemHandle(HandleIndex); + } + + private static bool TryCreateSharedBuffer(BinaryEmulator Instance, AudioStream Stream) + { + ulong SectionSize = RingBufferOffset + Stream.RingBytes; + + ulong Backing = Instance.MapUniqueAddress(SectionSize, MemoryProtection.ReadWrite); + if (Backing == 0) + return false; + + Stream.ControlBlock = Backing; + WriteStreamControlBlock(Instance, Stream); + PublishHandlePort(Instance, Stream); + + Stream.SectionHandle = Instance.WinHelper.CreateSectionHandle( + null, SectionSize, PageReadWrite, 0, null, Backing, AccessMask.StandardRightsAll).Handle; + + // The engine walks the ring from a host thread, so it needs a host pointer into the section + // rather than the emulator's memory accessors, which are only safe from the guest thread. + IntPtr Host = Instance.GetHostPointer(Backing, SectionSize); + if (Host == IntPtr.Zero) + { + Log(Instance, "no host pointer for the shared section; stream will not be audible."); + return true; + } + + Stream.Engine = new AudioStreamEngine( + Host, RingBufferOffset, Stream.RingBytes, + new AudioSinkFormat(MixSampleRate, MixChannels, MixBitsPerSample)); + + Log(Instance, $"stream {Stream.Id} engine started on the {Stream.Engine.Backend} backend."); + return true; + } + + private static void PublishHandlePort(BinaryEmulator Instance, AudioStream Stream) + { + Stream.HandlePortName = HandlePortPrefix + Stream.Id; + + Instance.WinHelper.WinPorts.Add(new WinPort + { + Name = Stream.HandlePortName, + Handler = HandleEventPortMessage, + }); + + Instance.WriteMemory(Stream.ControlBlock + OffHandlePortName, + Encoding.Unicode.GetBytes(Stream.HandlePortName + "\0")); + } + + private static NTSTATUS HandleEventPortMessage(WinPort Port, byte[] SendData, PortReply Reply, BinaryEmulator Instance) + { + ulong EventHandle = Port.ReceivedHandles is { Count: > 0 } ? Port.ReceivedHandles[0] : 0; + + if (EventHandle != 0 && TryGetStreamByHandlePort(Port.Name, out AudioStream Stream)) + { + WinEvent Event = Instance.WinHelper.GetEventByHandle(EventHandle, AccessMask.EventAllAccess); + Stream.Engine?.SetPeriodEvent(Event); + Log(Instance, $"stream {Stream.Id} render event 0x{EventHandle:X} {(Event != null ? "attached" : "not resolvable")}."); + } + + byte[] Response = new byte[Math.Max(SendData.Length, HandlePortMessageSize)]; + Array.Copy(SendData, Response, SendData.Length); + BinaryPrimitives.WriteInt32LittleEndian(Response.AsSpan(OffHandlePortStatus, 4), 0); + + Reply.Data = Response; + return NTSTATUS.STATUS_SUCCESS; + } + + private static bool TryGetStreamByHandlePort(string PortName, out AudioStream Stream) + { + foreach (KeyValuePair Entry in Streams) + { + if (string.Equals(Entry.Value.HandlePortName, PortName, StringComparison.OrdinalIgnoreCase)) + { + Stream = Entry.Value; + return true; + } + } + + Stream = null; + return false; + } + + private static uint FramesForDuration(ulong DurationHns) + { + ulong Frames = DurationHns * MixSampleRate / HundredNanosecondsPerSecond; + uint MaxFrames = MaxRingBytes / MixBlockAlign; + return Frames == 0 ? 1 : (uint)Math.Min(Frames, MaxFrames); + } + + private static uint RingBytesForDuration(ulong DurationHns) + { + return FramesForDuration(DurationHns) * MixBlockAlign; + } + + private static void WriteStreamControlBlock(BinaryEmulator Instance, AudioStream Stream) + { + ulong Block = Stream.ControlBlock; + uint BufferEnd = RingBufferOffset + Stream.RingBytes; + + Instance._emulator.WriteMemory(Block + OffVersion, ControlDataVersion); + Instance._emulator.WriteMemory(Block + OffTotalSize, BufferEnd); + Instance._emulator.WriteMemory(Block + OffMagic, ControlDataMagic); + + Instance._emulator.WriteMemory(Block + OffVolatileQueueRead, 0u); + Instance._emulator.WriteMemory(Block + OffVolatileQueueWrite, 0u); + Instance._emulator.WriteMemory(Block + OffClientCursor, 0UL, 8); + Instance._emulator.WriteMemory(Block + OffServerCursor, 0UL, 8); + Instance._emulator.WriteMemory(Block + OffVolatileFlags, 0u); + + Instance._emulator.WriteMemory(Block + OffStaticSize, StaticControlDataSize); + Instance._emulator.WriteMemory(Block + OffQueueCount, 0u); + Instance._emulator.WriteMemory(Block + OffBufferStart, RingBufferOffset); + Instance._emulator.WriteMemory(Block + OffBufferEnd, BufferEnd); + Instance._emulator.WriteMemory(Block + OffBufferLimit, BufferEnd); + + Instance.WriteMemory(Block + OffWaveFormat, BuildMixFormat()); + } + + private static byte[] BuildContextCookie(uint StreamId) + { + byte[] Cookie = new byte[Ndr20Writer.ContextHandleSize]; + BinaryPrimitives.WriteUInt32LittleEndian(Cookie.AsSpan(0, 4), ContextCookieMagic); + BinaryPrimitives.WriteUInt32LittleEndian(Cookie.AsSpan(4, 4), StreamId); + return Cookie; + } + + private static bool TryGetStream(ReadOnlySpan Cookie, out AudioStream Stream) + { + Stream = null; + + if (Cookie.Length < 8 || BinaryPrimitives.ReadUInt32LittleEndian(Cookie) != ContextCookieMagic) + return false; + + return Streams.TryGetValue(BinaryPrimitives.ReadUInt32LittleEndian(Cookie.Slice(4, 4)), out Stream); + } + + private static bool TryFindActiveEndpoint(BinaryEmulator Instance, string Direction, out string EndpointId) + { + EndpointId = null; + + WinSysHelper Helper = Instance.WinHelper; + string DirectionKey = MMDevicesKey + "\\" + Direction; + + WinHandle RootHandle = Helper.OpenRegistryKey(DirectionKey, AccessMask.GenericRead); + if (RootHandle == null) + return false; + + try + { + WinRegKey Root = Helper.HandleManager.GetObjectByHandle(RootHandle.Handle); + if (Root == null || !Helper.TryCollectRegistrySubKeyNames(Root, out List Names)) + return false; + + foreach (string Name in Names) + { + if (IsEndpointActive(Helper, DirectionKey + "\\" + Name)) + { + EndpointId = Name; + return true; + } + } + } + finally + { + Helper.CloseHandle(RootHandle.Handle); + } + + return false; + } + + private static bool IsEndpointActive(WinSysHelper Helper, string EndpointKey) + { + WinHandle Handle = Helper.OpenRegistryKey(EndpointKey, AccessMask.GenericRead); + if (Handle == null) + return false; + + try + { + WinRegKey Key = Helper.HandleManager.GetObjectByHandle(Handle.Handle); + if (Key == null || !Helper.TryGetRegistryValue(Key, "DeviceState", out ValueNode State)) + return false; + + return State.Data != null + && State.Data.Length >= 4 + && BinaryPrimitives.ReadUInt32LittleEndian(State.Data) == DeviceStateActive; + } + finally + { + Helper.CloseHandle(Handle.Handle); + } + } + + public static void DumpConnectionMessage(BinaryEmulator Instance, string TargetPort, ulong ConnectionMessagePtr) + { + if (ConnectionMessagePtr == 0 || !IsAudioPort(TargetPort)) + return; + + if (!Instance.IsRegionMapped(ConnectionMessagePtr, LrpcPacket.HeaderSize)) + return; + + byte[] Header = Instance.ReadMemory(ConnectionMessagePtr, LrpcPacket.HeaderSize); + if (Header == null) + return; + + ushort TotalLength = (ushort)(Header[0x02] | (Header[0x03] << 8)); + if (TotalLength < LrpcPacket.HeaderSize || !Instance.IsRegionMapped(ConnectionMessagePtr, TotalLength)) + return; + + Dump(Instance, "connect", Instance.ReadMemory(ConnectionMessagePtr, TotalLength), LrpcPacket.HeaderSize); + } + + private static bool IsAudioPort(string Name) + { + for (int Index = 0; Index < PortNames.Length; Index++) + { + if (string.Equals(PortNames[Index], Name, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + private static void DumpStubData(BinaryEmulator Instance, in LrpcMessage Message) + { + if (Message.Raw != null) + Dump(Instance, "stub", Message.Raw, LrpcPacket.RequestStubDataOffset); + } + + private static void Log(BinaryEmulator Instance, string Text) + { + if ((Instance.Settings.Flags & LogFlags.General) != 0) + Instance.TriggerEventMessage($"[AudioSrv] {Text}", LogFlags.General); + } + + private static void Dump(BinaryEmulator Instance, string Label, byte[] Data, int Offset) + { + if ((Instance.Settings.Flags & LogFlags.General) == 0 || Data == null || Data.Length <= Offset) + return; + + int Length = Data.Length - Offset; + int Shown = Math.Min(Length, MaxDumpBytes); + Instance.TriggerEventMessage($"[AudioSrv] {Label}: {Length} bytes.", LogFlags.General); + + for (int Pos = 0; Pos < Shown; Pos += DumpBytesPerLine) + { + int Count = Math.Min(DumpBytesPerLine, Shown - Pos); + Instance.TriggerEventMessage($"[AudioSrv] +{Pos:X4} {Convert.ToHexString(Data, Offset + Pos, Count)}", LogFlags.General); + } + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioStreamEngine.cs b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioStreamEngine.cs new file mode 100644 index 0000000..53885d3 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioStreamEngine.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading; +using Brovan.Core.Emulation.OS.SharedHelpers; + +namespace Brovan.Core.Emulation.OS.Windows.RPC.Ports +{ + internal sealed class AudioStreamEngine : IDisposable + { + private const int IdleSleepMilliseconds = 5; + private const int ChunkMilliseconds = 10; + private const int StopTimeoutMilliseconds = 200; + private const int OffClientCursor = 0x018; + private const int OffServerCursor = 0x020; + private const int OffVolatileFlags = 0x0AC; + + private const uint FlagRunning = 1; + + private readonly IntPtr Block; + private readonly uint BufferStart; + private readonly uint RingBytes; + private readonly IAudioSink Sink; + private readonly byte[] Chunk; + private readonly byte[] Silence; + private readonly Thread Worker; + + private volatile bool Stopping; + private volatile WinEvent PeriodEvent; + + public string Backend { get; } + + public void SetPeriodEvent(WinEvent Event) => PeriodEvent = Event; + + public long RenderedBytes { get; private set; } + + public AudioStreamEngine(IntPtr Block, uint BufferStart, uint RingBytes, AudioSinkFormat Format) + { + this.Block = Block; + this.BufferStart = BufferStart; + this.RingBytes = RingBytes; + + Sink = AudioSinkFactory.Create(Format, out string SinkBackend); + Backend = SinkBackend; + + int ChunkBytes = Format.BytesPerSecond * ChunkMilliseconds / 1000 / Format.BlockAlign * Format.BlockAlign; + Chunk = new byte[ChunkBytes]; + Silence = new byte[ChunkBytes]; + + Worker = new Thread(Run) + { + IsBackground = true, + Priority = ThreadPriority.AboveNormal, + Name = "Brovan audio engine", + }; + + Worker.Start(); + } + + private unsafe void Run() + { + byte* Base = (byte*)Block; + + while (!Stopping) + { + if ((*(uint*)(Base + OffVolatileFlags) & FlagRunning) == 0) + { + Thread.Sleep(IdleSleepMilliseconds); + continue; + } + + long Written = Interlocked.CompareExchange(ref *(long*)(Base + OffClientCursor), 0, 0); + long Read = *(long*)(Base + OffServerCursor); + long Available = Written - Read; + + // Feed the device anyway when the guest is behind, both to keep it from underrunning and + // because the write is what advances real time for this loop. + if (Available <= 0) + { + Sink.Write(Silence); + SignalPeriod(); + continue; + } + + int Take = (int)Math.Min(Available, Chunk.Length); + int Offset = (int)(Read % RingBytes); + int Contiguous = Math.Min(Take, (int)RingBytes - Offset); + + new ReadOnlySpan(Base + BufferStart + Offset, Contiguous).CopyTo(Chunk); + if (Contiguous < Take) + new ReadOnlySpan(Base + BufferStart, Take - Contiguous).CopyTo(Chunk.AsSpan(Contiguous)); + + Sink.Write(Chunk.AsSpan(0, Take)); + + Interlocked.Exchange(ref *(long*)(Base + OffServerCursor), Read + Take); + RenderedBytes += Take; + SignalPeriod(); + } + } + + private void SignalPeriod() + { + WinEvent Event = PeriodEvent; + if (Event != null) + Event.Signaled = true; + } + + public void Dispose() + { + if (Stopping) + return; + + Stopping = true; + Worker.Join(StopTimeoutMilliseconds); + Sink.Dispose(); + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Registry/NtOpenKeyEx.cs b/Brovan/Core/Emulation/OS/Windows/Registry/NtOpenKeyEx.cs index dea1178..1ee4c03 100644 --- a/Brovan/Core/Emulation/OS/Windows/Registry/NtOpenKeyEx.cs +++ b/Brovan/Core/Emulation/OS/Windows/Registry/NtOpenKeyEx.cs @@ -46,10 +46,11 @@ public NTSTATUS Handle(BinaryEmulator Instance) return NTSTATUS.STATUS_SUCCESS; } + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) + Instance.TriggerEventMessage($"[!] NtOpenKeyEx: no key \"{KeyPath}\".", LogFlags.Syscall); + return NTSTATUS.STATUS_OBJECT_NAME_NOT_FOUND; } - - return Instance.WinUnimplemented; } } } diff --git a/Brovan/Core/Emulation/OS/Windows/Registry/NtQueryKey.cs b/Brovan/Core/Emulation/OS/Windows/Registry/NtQueryKey.cs index 87a0a9a..6190411 100644 --- a/Brovan/Core/Emulation/OS/Windows/Registry/NtQueryKey.cs +++ b/Brovan/Core/Emulation/OS/Windows/Registry/NtQueryKey.cs @@ -42,7 +42,15 @@ public NTSTATUS Handle(BinaryEmulator Instance) WinRegKey RegKey = Instance.WinHelper.HandleManager.GetObjectByHandle(KeyHandle); if (RegKey == null) + { + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) + { + IHandleObject Existing = Instance.WinHelper.HandleManager.GetObjectByHandle(KeyHandle); + Instance.TriggerEventMessage($"[!] NtQueryKey: handle 0x{KeyHandle:X} is {(Existing == null ? "unknown" : Existing.ObjectType + " \"" + Existing.ObjectId + "\"")}, not a registry key (class {KeyInformationClass}).", LogFlags.Syscall); + } + return NTSTATUS.STATUS_INVALID_HANDLE; + } if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) Instance.TriggerEventMessage($"[+] NtQueryKey Running with the FullPath: {RegKey.FullPath}", LogFlags.Syscall); diff --git a/Brovan/Core/Emulation/OS/Windows/Registry/NtQueryValueKey.cs b/Brovan/Core/Emulation/OS/Windows/Registry/NtQueryValueKey.cs index 1179053..710269d 100644 --- a/Brovan/Core/Emulation/OS/Windows/Registry/NtQueryValueKey.cs +++ b/Brovan/Core/Emulation/OS/Windows/Registry/NtQueryValueKey.cs @@ -60,6 +60,9 @@ public NTSTATUS Handle(BinaryEmulator Instance) if (!Instance.WinHelper.TryGetRegistryValue(RegKey, ValueName, out ValueNode Value)) { + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) + Instance.TriggerEventMessage($"[!] NtQueryValueKey: \"{RegKey.FullPath}\" has no value \"{ValueName}\".", LogFlags.Syscall); + Instance._emulator.WriteMemory(ResultLengthPtr, 0u); return NTSTATUS.STATUS_OBJECT_NAME_NOT_FOUND; } @@ -72,7 +75,6 @@ public NTSTATUS Handle(BinaryEmulator Instance) switch (KeyValueInformationClass) { case KEY_VALUE_INFORMATION_CLASS.KeyValuePartialInformation: - case KEY_VALUE_INFORMATION_CLASS.KeyValuePartialInformationAlign64: { uint HeaderSize = 12; uint Required = HeaderSize + DataLen; @@ -104,7 +106,50 @@ public NTSTATUS Handle(BinaryEmulator Instance) } if (Length < Required) + { + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) + Instance.TriggerEventMessage($"[!] NtQueryValueKey: \"{RegKey.FullPath}\" value \"{ValueName}\" needs {Required} bytes, got {Length}.", LogFlags.Syscall); + return Length < HeaderSize ? NTSTATUS.STATUS_BUFFER_TOO_SMALL : NTSTATUS.STATUS_BUFFER_OVERFLOW; + } + + return NTSTATUS.STATUS_SUCCESS; + } + + case KEY_VALUE_INFORMATION_CLASS.KeyValuePartialInformationAlign64: + { + uint HeaderSize = 8; + uint Required = HeaderSize + DataLen; + + Instance._emulator.WriteMemory(ResultLengthPtr, Required); + + if (Length == 0) + return NTSTATUS.STATUS_BUFFER_TOO_SMALL; + + if (Length >= HeaderSize) + { + if (!Instance._emulator.WriteMemory(KeyValueInformationPtr + 0, (uint)Value.Type)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + + if (!Instance._emulator.WriteMemory(KeyValueInformationPtr + 4, DataLen)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + + uint WritableDataLength = Math.Min(DataLen, Length - HeaderSize); + if (WritableDataLength != 0) + { + ulong DataOut = KeyValueInformationPtr + HeaderSize; + if (!Instance._emulator.WriteMemory(DataOut, DataBytes.AsSpan(0, (int)WritableDataLength))) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + } + } + + if (Length < Required) + { + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) + Instance.TriggerEventMessage($"[!] NtQueryValueKey: \"{RegKey.FullPath}\" value \"{ValueName}\" needs {Required} bytes, got {Length}.", LogFlags.Syscall); + + return Length < HeaderSize ? NTSTATUS.STATUS_BUFFER_TOO_SMALL : NTSTATUS.STATUS_BUFFER_OVERFLOW; + } return NTSTATUS.STATUS_SUCCESS; } diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserCreateWindowEx.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserCreateWindowEx.cs index d234e50..a17f2bc 100644 --- a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserCreateWindowEx.cs +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserCreateWindowEx.cs @@ -15,8 +15,16 @@ public NTSTATUS Handle(BinaryEmulator Instance) ulong ClassVersionPtr = Instance.WinHelper.GetArg(2); ulong WindowNamePtr = Instance.WinHelper.GetArg(3); ulong StyleArg = Instance.WinHelper.GetArg(4); + const int UseDefaultPosition = unchecked((int)0x80000000); + int x = unchecked((int)Instance.WinHelper.GetArg(5)); int y = unchecked((int)Instance.WinHelper.GetArg(6)); + + if (x == UseDefaultPosition) + x = 0; + + if (y == UseDefaultPosition) + y = 0; int width = unchecked((int)Instance.WinHelper.GetArg(7)); int height = unchecked((int)Instance.WinHelper.GetArg(8)); ulong ParentHwnd = Instance.WinHelper.GetArg(9); @@ -24,7 +32,12 @@ public NTSTATUS Handle(BinaryEmulator Instance) ulong InstanceHandle = Instance.WinHelper.GetArg(11); ulong CreateParam = Instance.WinHelper.GetArg(12); - if (ParentHwnd != 0 && Instance.WinHelper.GetWindow(ParentHwnd) == null) + if (Win32kMessageOnlyParent.IsHwndMessage(ParentHwnd)) + { + Win32kMessageOnlyParent.Ensure(Instance); + ParentHwnd = Win32kMessageOnlyParent.HwndMessage; + } + else if (ParentHwnd != 0 && Instance.WinHelper.GetWindow(ParentHwnd) == null) { Instance.SetLastWinError(ERROR_INVALID_WINDOW_HANDLE); Instance.SetRawSyscallReturn(0); diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserSetCursorPos.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserSetCursorPos.cs new file mode 100644 index 0000000..4ac0026 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserSetCursorPos.cs @@ -0,0 +1,19 @@ +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows.Win32k +{ + internal class NtUserSetCursorPos : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + int X = unchecked((int)Instance.WinHelper.GetArg(0)); + int Y = unchecked((int)Instance.WinHelper.GetArg(1)); + + Win32kHelper.SetCursorPosition(Instance, X, Y); + + Instance.SetLastWinError(0); + Instance.SetBooleanSyscallReturn(true); + return NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kHelper.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kHelper.cs index efbd29c..b3d9def 100644 --- a/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kHelper.cs +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kHelper.cs @@ -897,6 +897,21 @@ internal static void GetCursorPosition(BinaryEmulator Instance, out int X, out i Y += Foreground.Y; } + internal static void SetCursorPosition(BinaryEmulator Instance, int X, int Y) + { + DrainHostEvents(Instance); + + WinWindow Foreground = Instance.WinHelper.GetWindow(Instance.WinHelper.GetForegroundWindow()); + int ClientX = Foreground == null ? X : X - Foreground.X; + int ClientY = Foreground == null ? Y : Y - Foreground.Y; + + Win32kState State = GetState(Instance); + State.CursorX = ClientX; + State.CursorY = ClientY; + + Instance.WinHelper.WarpHostCursor(ClientX, ClientY); + } + internal static bool InvalidateWindow(BinaryEmulator Instance, ulong Hwnd) { if (Hwnd == 0) diff --git a/Brovan/Core/Emulation/OS/Windows/WinHelperConstants.cs b/Brovan/Core/Emulation/OS/Windows/WinHelperConstants.cs index 4ede1fd..2c5e9f6 100644 --- a/Brovan/Core/Emulation/OS/Windows/WinHelperConstants.cs +++ b/Brovan/Core/Emulation/OS/Windows/WinHelperConstants.cs @@ -883,6 +883,7 @@ public enum AccessMask : uint TimerQueryState = 0x00000001, TimerModifyState = 0x00000002, TimerAllAccess = StandardRightsRequired | Synchronize | TimerQueryState | TimerModifyState, + EventAllAccess = StandardRightsRequired | Synchronize | 0x3, StandardRightsRequired = Delete | ReadControl | WriteDAC | WriteOwner, StandardRightsAll = StandardRightsRequired | Synchronize, ProcessTerminate = 0x00000001, @@ -1345,6 +1346,7 @@ public class WinSection : IHandleObject public WindowsFileStream FileStream; public ulong BackingAddress; public ulong ImageSectionId; + public int MappedViewCount; public bool IsImage => ((Attributes & 0x01000000) != 0); public bool Initialized; @@ -1692,13 +1694,22 @@ public class WinSymbolicLink : IHandleObject public HandleType ObjectType => HandleType.FileHandle; } + public sealed class PortReply + { + public byte[] Data; + public List Handles; + + public void AttachHandle(ulong Handle) + { + Handles ??= new List(); + Handles.Add(Handle); + } + } + /// /// Per-port ALPC/LPC message handler. - /// Receives the raw send-payload bytes (everything after the PORT_MESSAGE header), - /// may mutate them, and writes the server reply into . - /// Returns the NTSTATUS to surface to the caller. /// - public delegate NTSTATUS PortAlpcHandler(WinPort Port, byte[] SendData, out byte[] ReplyData, BinaryEmulator Instance); + public delegate NTSTATUS PortAlpcHandler(WinPort Port, byte[] SendData, PortReply Reply, BinaryEmulator Instance); public sealed class WinPort : IHandleObject { @@ -1708,6 +1719,19 @@ public sealed class WinPort : IHandleObject /// Optional per-port message handler invoked by NtAlpcSendWaitReceivePort. /// public PortAlpcHandler Handler; + + /// + /// Handles delivered with the reply the client last received, waiting to be claimed through + /// NtAlpcQueryInformationMessage(AlpcMessageHandleInformation). + /// + public List DeliveredHandles; + + /// + /// Handles the client attached to the message being dispatched, in the order the handle + /// attribute listed them. + /// + public List ReceivedHandles; + public string ObjectId => Name; public HandleType ObjectType => HandleType.PortHandle; } diff --git a/Brovan/Core/Emulation/OS/Windows/WinInternalHelper.cs b/Brovan/Core/Emulation/OS/Windows/WinInternalHelper.cs index eac2811..8e90c37 100644 --- a/Brovan/Core/Emulation/OS/Windows/WinInternalHelper.cs +++ b/Brovan/Core/Emulation/OS/Windows/WinInternalHelper.cs @@ -170,32 +170,44 @@ private static bool IsReservedHandleValue(ulong Handle) Handle == 0; } + private bool TryGetEntry(ulong Handle, out ulong Key, out HandleEntry Entry) + { + if (HandleTable.TryGetValue(Handle, out Entry)) + { + Key = Handle; + return true; + } + + Key = Handle & ~3UL; + return Key != Handle && HandleTable.TryGetValue(Key, out Entry); + } + public ObjectHandleFlags GetHandleFlags(ulong Handle) { - if (HandleTable.TryGetValue(Handle, out HandleEntry Entry)) + if (TryGetEntry(Handle, out _, out HandleEntry Entry)) return Entry.Flags; return ObjectHandleFlags.None; } public bool SetHandleFlags(ulong Handle, ObjectHandleFlags Flags) { - if (!HandleTable.TryGetValue(Handle, out HandleEntry Entry)) + if (!TryGetEntry(Handle, out ulong Key, out HandleEntry Entry)) return false; Entry.Flags = Flags; - HandleTable[Handle] = Entry; + HandleTable[Key] = Entry; return true; } public T? GetObjectByHandle(ulong Handle) where T : class, IHandleObject { - if (HandleTable.TryGetValue(Handle, out HandleEntry Entry) && Entry.Object is T typedObj) + if (TryGetEntry(Handle, out _, out HandleEntry Entry) && Entry.Object is T typedObj) return typedObj; return null; } public IHandleObject? GetObjectByHandle(ulong Handle) { - if (HandleTable.TryGetValue(Handle, out HandleEntry Entry)) + if (TryGetEntry(Handle, out _, out HandleEntry Entry)) return Entry.Object; return null; } @@ -223,25 +235,25 @@ public List GetHandlesByObjectId(string ObjectId) public AccessMask GetPermissionsByHandle(ulong Handle) { - if (HandleTable.TryGetValue(Handle, out HandleEntry Entry)) + if (TryGetEntry(Handle, out _, out HandleEntry Entry)) return Entry.Permissions; return AccessMask.None; } public bool TryGetHandle(ulong Handle, out HandleEntry Entry) { - return HandleTable.TryGetValue(Handle, out Entry); + return TryGetEntry(Handle, out _, out Entry); } public bool TryRemoveHandle(ulong Handle, out HandleEntry Entry) { - if (!HandleTable.TryGetValue(Handle, out Entry)) + if (!TryGetEntry(Handle, out ulong Key, out Entry)) return false; - HandleTable.Remove(Handle); + HandleTable.Remove(Key); IHandleObject obj = Entry.Object; if (ObjectIdToHandles.TryGetValue(obj.ObjectId, out List Handles)) { - Handles.Remove(Handle); + Handles.Remove(Key); if (Handles.Count == 0) ObjectIdToHandles.Remove(obj.ObjectId); } @@ -250,14 +262,14 @@ public bool TryRemoveHandle(ulong Handle, out HandleEntry Entry) public bool RemoveHandle(ulong Handle) { - if (!HandleTable.TryGetValue(Handle, out HandleEntry Entry)) + if (!TryGetEntry(Handle, out ulong Key, out HandleEntry Entry)) return false; IHandleObject obj = Entry.Object; - HandleTable.Remove(Handle); + HandleTable.Remove(Key); if (ObjectIdToHandles.TryGetValue(obj.ObjectId, out List Handles)) { - Handles.Remove(Handle); + Handles.Remove(Key); if (Handles.Count == 0) ObjectIdToHandles.Remove(obj.ObjectId); @@ -284,19 +296,19 @@ public void SnapshotHandles(List> Destination public bool HandleExists(ulong Handle) { - return HandleTable.ContainsKey(Handle); + return TryGetEntry(Handle, out _, out _); } public bool HandleExists(ulong Handle, HandleType type) { - if (!HandleTable.TryGetValue(Handle, out HandleEntry Entry)) + if (!TryGetEntry(Handle, out _, out HandleEntry Entry)) return false; return Entry.Object != null && Entry.Object.ObjectType == type; } public bool CheckAccess(ulong Handle, AccessMask RequiredAccess) { - if (!HandleTable.TryGetValue(Handle, out HandleEntry Entry)) + if (!TryGetEntry(Handle, out _, out HandleEntry Entry)) return false; AccessMask GrantedAccess = Entry.Permissions; @@ -340,6 +352,7 @@ public sealed class KuserSharedDataManager private readonly BinaryEmulator Emulator; private MemoryHookCallback ReadHook; private bool Installed; + private bool RefreshedOnRead; private long LastUpdateTimestamp; @@ -360,7 +373,8 @@ public void Initialize() BaseInterruptTime = ReadKsystemTimeFromBuffer(Page, OffsetInterruptTime); LastUpdateTimestamp = 0; - if (!Emulator._emulator.MapMmio(Emulator.KUSER_SHARED_DATA, PageSize, FillTimeFields, IgnoreWrite)) + RefreshedOnRead = Emulator._emulator.MapMmio(Emulator.KUSER_SHARED_DATA, PageSize, FillTimeFields, IgnoreWrite); + if (!RefreshedOnRead) { if (!Emulator.IsRegionMapped(Emulator.KUSER_SHARED_DATA, PageSize) && Emulator.MapMemoryRegion(Emulator.KUSER_SHARED_DATA, PageSize, MemoryProtection.Read) == 0) @@ -369,11 +383,8 @@ public void Initialize() } ReadHook = OnRead; - if (Emulator._emulator.AddMemoryHook(Emulator.KUSER_SHARED_DATA, - Emulator.KUSER_SHARED_DATA + (PageSize - 1), BackendHookType.MemoryRead, ReadHook) == IntPtr.Zero) - { - Utils.LogError($"[KUSER_MANAGER] No way to keep KUSER_SHARED_DATA current: {Emulator.GetLastError()}"); - } + RefreshedOnRead = Emulator._emulator.AddMemoryHook(Emulator.KUSER_SHARED_DATA, + Emulator.KUSER_SHARED_DATA + (PageSize - 1), BackendHookType.MemoryRead, ReadHook) != IntPtr.Zero; } if (!Emulator._emulator.WriteMemory(Emulator.KUSER_SHARED_DATA, Page)) @@ -393,6 +404,17 @@ private bool OnRead(BackendMemoryAccessType Type, ulong Address, uint Size, ulon return true; } + /// + /// Brings the clock fields forward when no hooks/MMIO are installed. + /// + public void RefreshIfUnhooked() + { + if (RefreshedOnRead) + return; + + UpdateDynamicFields(false); + } + private void FillTimeFields(ulong Offset, Span Destination) { if (Offset != 0 || Destination.Length < OffsetTickCountQuad + 12) @@ -1049,6 +1071,9 @@ private uint SafeReadUInt(ulong address) internal static class Win32kMessageOnlyParent { public const ulong HwndMessage = 0xFFFFFFFFFFFFFFFDUL; + private const ulong HwndMessage32 = 0xFFFFFFFDUL; + + public static bool IsHwndMessage(ulong Hwnd) => Hwnd == HwndMessage || Hwnd == HwndMessage32; public static WinWindow Ensure(BinaryEmulator Instance) { diff --git a/Brovan/Core/Emulation/OS/Windows/WinSyscallsHelper.cs b/Brovan/Core/Emulation/OS/Windows/WinSyscallsHelper.cs index f27332f..6a1cc60 100644 --- a/Brovan/Core/Emulation/OS/Windows/WinSyscallsHelper.cs +++ b/Brovan/Core/Emulation/OS/Windows/WinSyscallsHelper.cs @@ -1102,19 +1102,6 @@ private static long SaturatingMillisecondsToFileTimeDuration(long Milliseconds) public List MappedImageViews = new List(); private readonly Dictionary ImageViewCountsByPath = new Dictionary(StringComparer.OrdinalIgnoreCase); internal KuserSharedDataManager KuserSharedData; - - /// - /// Current performance counter, in the units claims. - /// - internal static ulong QueryPerformanceCounterValue() - { - long Ticks = System.Diagnostics.Stopwatch.GetTimestamp(); - long HostFrequency = System.Diagnostics.Stopwatch.Frequency; - if (HostFrequency == KuserSharedDataManager.QpcFrequency) - return (ulong)Ticks; - - return (ulong)((decimal)Ticks * KuserSharedDataManager.QpcFrequency / HostFrequency); - } internal HandleManager HandleManager = new HandleManager(); private static string WinRegPath = Path.Combine(AppContext.BaseDirectory, "WinReg"); public RegistryManager RegManager = new RegistryManager(WinRegPath); @@ -1435,6 +1422,38 @@ public WinSysHelper(BinaryEmulator Emulator) Handler = RPC.Ports.CsrssPortHandler.Handle }); } + + foreach (string AudioPort in RPC.Ports.AudioSrvPortHandler.PortNames) + { + WinPorts.Add(new WinPort + { + Name = AudioPort, + Handler = RPC.Ports.AudioSrvPortHandler.Handle + }); + } + + // The kernel publishes these under \KernelObjects on every system. ntdll's commit-condition + // path and CreateMemoryResourceNotification open them by name and expect them to exist. + foreach (string ConditionName in new[] + { + "\\KernelObjects\\MaximumCommitCondition", + "\\KernelObjects\\LowCommitCondition", + "\\KernelObjects\\HighCommitCondition", + "\\KernelObjects\\LowMemoryCondition", + "\\KernelObjects\\HighMemoryCondition", + "\\KernelObjects\\LowNonPagedPoolCondition", + "\\KernelObjects\\HighNonPagedPoolCondition", + "\\KernelObjects\\LowPagedPoolCondition", + "\\KernelObjects\\HighPagedPoolCondition", + "\\KernelObjects\\MemoryErrors", + }) + { + CreateEventHandle(ConditionName, 0, false, AccessMask.StandardRightsAll); + } + + // services.exe signals this once the SCM is up. A caller that does not find it creates its own + // and waits for a service controller that never arrives. winmm's audio path fast fails on that + CreateEventHandle("\\Sessions\\1\\BaseNamedObjects\\Global\\SvcctrlStartEvent_A3752DX", 0, true, AccessMask.EventAllAccess); } public enum ExceptionType @@ -1514,12 +1533,47 @@ public bool UnmapViewOfSection(ulong BaseAddress) return UnmappedAny; } + WinSection? ViewSection = FindSectionByBackingAddress(BaseAddress); + if (ViewSection != null) + { + if (ViewSection.MappedViewCount > 0) + ViewSection.MappedViewCount--; + + ReleaseSectionIfUnreferenced(ViewSection); + return true; + } + if (!Emulator.TryFindMemoryRegion(BaseAddress, out MemoryRegion ViewRegion)) return false; return Emulator.UnmapMemoryRegion(ViewRegion.BaseAddress); } + private WinSection? FindSectionByBackingAddress(ulong Address) + { + for (int Index = 0; Index < WinSections.Count; Index++) + { + WinSection Section = WinSections[Index]; + if (Section.BackingAddress != 0 && Address >= Section.BackingAddress && Address - Section.BackingAddress < Section.Size) + return Section; + } + + return null; + } + + private void ReleaseSectionIfUnreferenced(WinSection Section) + { + if (Section.BackingAddress == 0 || Section.MappedViewCount != 0) + return; + + if (HandleManager.GetHandlesByObjectId(Section.ObjectId).Count != 0) + return; + + Emulator.UnmapMemoryRegion(Section.BackingAddress); + Section.BackingAddress = 0; + WinSections.Remove(Section); + } + /// /// Invoke KiUserExceptionDispatcher with the specified exception. /// @@ -3035,7 +3089,10 @@ private void EnsureGdiHandleTable() if (Peb == 0) return; - GdiHandleTableAddress = Emulator.ReadMemoryULong(Peb + 0xF8); + ulong Table = Emulator.ReadMemoryULong(Peb + 0xF8); + GdiHandleTableAddress = Table != 0 && Emulator.IsRegionMapped(Table, GdiHandleEntryCount * GdiHandleEntrySize) + ? Table + : 0; } public bool EnsureUserSharedInfo(out ulong ServerInfo, out ulong HandleTable, out uint EntrySize) @@ -3911,8 +3968,8 @@ private void RefreshUserWindowObject(WinWindow Window) Emulator._emulator.WriteMemory(Window.ClientWindowAddress + 0x00, Window.Hwnd, 8); Emulator._emulator.WriteMemory(Window.ClientWindowAddress + 0x08, Window.ClientWindowAddress, 8); - int OuterLeft = Window.X == unchecked((int)0x80000000) ? 0 : Window.X; - int OuterTop = Window.Y == unchecked((int)0x80000000) ? 0 : Window.Y; + int OuterLeft = Window.X; + int OuterTop = Window.Y; int OuterWidth = (int)Window.Width; int OuterHeight = (int)Window.Height; int OuterRight = OuterLeft + OuterWidth; @@ -4247,8 +4304,15 @@ public void HideDesktopWindow() } /// - /// Presents the current foreground Win32k window through the host window manager. + /// Moves the host pointer to a point in the desktop window's client area, for guests that re-centre + /// the cursor themselves. /// + public void WarpHostCursor(int ClientX, int ClientY) + { + if (DesktopDisplay is GuiThreadManager GuiManager) + GuiManager.EnqueueWarpCursor(ClientX, ClientY); + } + public void PresentDesktop() { try @@ -4733,6 +4797,9 @@ private string FixupNtRegistryPath(string NtPath) if (First.Equals(".DEFAULT", StringComparison.OrdinalIgnoreCase)) return "\\Registry\\User\\.DEFAULT" + Tail; + if (First.EndsWith("_Classes", StringComparison.OrdinalIgnoreCase)) + return "\\Registry\\Machine\\SOFTWARE\\Classes" + Tail; + if (First.StartsWith("S-1-", StringComparison.OrdinalIgnoreCase)) return $"\\Registry\\User\\{CurrentUserSid}{Tail}"; @@ -5139,11 +5206,19 @@ private void AddSyntheticKnownFolder(string Root, string Guid, string Name, stri SetSyntheticRegistryStringTrusted(PropertyBag, "ThisPCPolicy", 1, "Show", KeyCache, DefaultHive); } + /// + /// We don't support CoreMessagingRegistrar right now, so this is a workaround. + /// + private const string UnclaimedActivatableClassNamespace = "\\WindowsRuntime\\ActivatableClassId\\Windows.Gaming.Input."; + private bool IsRegistryPathDeleted(string NtPath) { if (string.IsNullOrEmpty(NtPath)) return true; + if (NtPath.Contains(UnclaimedActivatableClassNamespace, StringComparison.OrdinalIgnoreCase)) + return true; + NtPath = NormalizeKeyPath(NtPath); foreach (string DeletedKey in DeletedRegistryKeys) @@ -5323,27 +5398,34 @@ private bool TryGetRegistryValues(WinRegKey RegKey, out List Values) return true; } - public bool TryEnumerateRegistrySubKey(WinRegKey RegKey, int Index, out string Name) + public bool TryCollectRegistrySubKeyNames(WinRegKey RegKey, out List Names) { - Name = null; + Names = null; - if (RegKey == null || Index < 0) + if (RegKey == null) return false; string NtPath = NormalizeNtRegistryPath(RegKey.FullPath); if (string.IsNullOrEmpty(NtPath) || IsRegistryPathDeleted(NtPath)) return false; - List Names = new List(); + Names = new List(); AddVirtualRegistrySubKeys(NtPath, Names); if (RegKey.Hive != null && RegKey.Hive.Reader != null && RegKey.HasParsedKey) { - for (int i = 0; RegKey.Hive.Reader.TryEnumerateSubKey(RegKey.ParsedKey, i, out string SubKeyName); i++) + RegistryHiveReader.HiveKey Parsed = RegKey.ParsedKey; + bool Enumerated = RegKey.Hive.Reader.TryGetSubKeyNames(ref Parsed, out Dictionary SubKeyNames); + RegKey.ParsedKey = Parsed; + + if (Enumerated) { - string ChildFullPath = NormalizeKeyPath(NtPath + "\\" + SubKeyName); - if (!DeletedRegistryKeys.Contains(ChildFullPath)) - Names.Add(SubKeyName); + foreach (string SubKeyName in SubKeyNames.Keys) + { + string ChildFullPath = NormalizeKeyPath(NtPath + "\\" + SubKeyName); + if (!DeletedRegistryKeys.Contains(ChildFullPath)) + Names.Add(SubKeyName); + } } } @@ -5356,10 +5438,17 @@ public bool TryEnumerateRegistrySubKey(WinRegKey RegKey, int Index, out string N Names.Add(ChildName); } - if (Index >= Names.Count) + Names.Sort(StringComparer.OrdinalIgnoreCase); + return true; + } + + public bool TryEnumerateRegistrySubKey(WinRegKey RegKey, int Index, out string Name) + { + Name = null; + + if (Index < 0 || !TryCollectRegistrySubKeyNames(RegKey, out List Names) || Index >= Names.Count) return false; - Names.Sort(StringComparer.OrdinalIgnoreCase); Name = Names[Index]; return true; } @@ -5415,10 +5504,13 @@ public bool TryQueryRegistryKeyFullInfo(WinRegKey RegKey, out int SubKeyCount, o if (!TryQueryRegistryKeyHeader(RegKey, out SubKeyCount, out ValueCount, out Name)) return false; - for (int i = 0; TryEnumerateRegistrySubKey(RegKey, i, out string SubKeyName); i++) + if (TryCollectRegistrySubKeyNames(RegKey, out List SubKeyNames)) { - if (!string.IsNullOrEmpty(SubKeyName) && SubKeyName.Length > MaxSubKeyNameChars) - MaxSubKeyNameChars = SubKeyName.Length; + foreach (string SubKeyName in SubKeyNames) + { + if (!string.IsNullOrEmpty(SubKeyName) && SubKeyName.Length > MaxSubKeyNameChars) + MaxSubKeyNameChars = SubKeyName.Length; + } } if (TryGetRegistryValues(RegKey, out List Values)) @@ -5483,8 +5575,8 @@ public bool TryQueryRegistryKeyHeader(WinRegKey RegKey, out int SubKeyCount, out if (string.IsNullOrEmpty(Name) && RegKey.Hive != null && RegKey.Hive.Reader != null && RegKey.HasParsedKey) RegKey.Hive.Reader.TryQueryKeyHeader(RegKey.ParsedKey, out _, out _, out Name); - for (int i = 0; TryEnumerateRegistrySubKey(RegKey, i, out _); i++) - SubKeyCount++; + if (TryCollectRegistrySubKeyNames(RegKey, out List SubKeyNames)) + SubKeyCount = SubKeyNames.Count; if (TryGetRegistryValues(RegKey, out List Values)) ValueCount = Values.Count; @@ -5917,6 +6009,9 @@ public WinHandle CreateSectionHandle(string Name, ulong Size, uint Protection, u public void CloseHandle(ulong Handle) { + WinSection? ClosingSection = null; + IHandleObject? ClosingSyncObject = null; + if (HandleManager.TryGetHandle(Handle, out HandleEntry Entry)) { if (Entry.Object != null && Entry.Object.ObjectType == HandleType.FileHandle) @@ -5930,12 +6025,45 @@ public void CloseHandle(ulong Handle) ApplyDeleteOnClose(Closing); } } + else if (Entry.Object != null && Entry.Object.ObjectType == HandleType.SectionHandle) + { + ClosingSection = Entry.Object as WinSection; + } + else + { + ClosingSyncObject = Entry.Object; + } } if (HandleManager.TryRemoveHandle(Handle, out _)) { RemoveWinHandle(Handle); } + + ForgetNamedSyncObjectIfUnreferenced(ClosingSyncObject); + + if (ClosingSection != null) + ReleaseSectionIfUnreferenced(ClosingSection); + } + + /// + /// Removes an object if it is not referenced anymore. + /// + private void ForgetNamedSyncObjectIfUnreferenced(IHandleObject? Object) + { + if (Object == null || HandleManager.GetHandlesByObjectId(Object.ObjectId).Count != 0) + return; + + switch (Object) + { + case WinSemaphore Semaphore: + WinSemaphores.Remove(Semaphore); + break; + + case WinMutex Mutex: + WinMutexes.Remove(Mutex); + break; + } } private void ApplyDeleteOnClose(WinFile Target) diff --git a/Brovan/Core/Helpers/RegistryManager.cs b/Brovan/Core/Helpers/RegistryManager.cs index 72f7ffe..b7aa3d4 100644 --- a/Brovan/Core/Helpers/RegistryManager.cs +++ b/Brovan/Core/Helpers/RegistryManager.cs @@ -244,24 +244,9 @@ public bool TryEnumerateSubKey(HiveKey Key, int Index, out string Name) if (Key.SubKeyBlockOffset <= 0) return false; - int ItemAbs = MainRootOffset + Key.SubKeyBlockOffset; - EnsureInBounds(ItemAbs, 0x0C); - - string BlockType = ReadAscii(ItemAbs + 4, 2); - - if (BlockType.Length != 2 || (BlockType[1] != 'f' && BlockType[1] != 'h')) + if (!TryFindSubKeyOffset(Key.SubKeyBlockOffset, ref Index, 0, out int Offset)) return false; - short Count = ReadI16(ItemAbs + 0x06); - if (Index >= Count) - return false; - - int EntriesAbs = ItemAbs + 0x08; - EnsureInBounds(EntriesAbs, Count * 8); - - int EntryAbs = EntriesAbs + (Index * 8); - - int Offset = ReadI32(EntryAbs); int SubKeyAbs = MainRootOffset + Offset; EnsureInBounds(SubKeyAbs, 0x60); @@ -279,6 +264,67 @@ public bool TryEnumerateSubKey(HiveKey Key, int Index, out string Name) return true; } + /// + /// Resolves the Index'th "nk" offset in a subkey list, descending through "ri" index roots and + /// decrementing Index by the size of each sublist it skips. + /// + private bool TryFindSubKeyOffset(int BlockOffset, ref int Index, int Depth, out int Offset) + { + const int MaxIndexDepth = 4; + + Offset = 0; + + if (BlockOffset <= 0 || Depth > MaxIndexDepth) + return false; + + int ItemAbs = MainRootOffset + BlockOffset; + EnsureInBounds(ItemAbs, 0x0C); + + string BlockType = ReadAscii(ItemAbs + 4, 2); + if (BlockType.Length != 2) + return false; + + bool Hashed = BlockType[0] == 'l' && (BlockType[1] == 'f' || BlockType[1] == 'h'); + bool IndexRoot = BlockType == "ri"; + + if (!Hashed && !IndexRoot && BlockType != "li") + return false; + + int EntrySize = Hashed ? 8 : 4; + int Count = (ushort)ReadI16(ItemAbs + 0x06); + int EntriesAbs = ItemAbs + 0x08; + + EnsureInBounds(EntriesAbs, Count * EntrySize); + + if (!IndexRoot) + { + if (Index >= Count) + { + Index -= Count; + return false; + } + + Offset = ReadI32(EntriesAbs + (Index * EntrySize)); + return true; + } + + for (int i = 0; i < Count; i++) + { + if (TryFindSubKeyOffset(ReadI32(EntriesAbs + (i * EntrySize)), ref Index, Depth + 1, out Offset)) + return true; + } + + return false; + } + + public bool TryGetSubKeyNames(ref HiveKey Key, out Dictionary Names) + { + EnsureSubKeysParsed(ref Key); + + Names = Key.SubKeys; + return Names != null; + } + public bool TryQueryKeyHeader(HiveKey Key, out int SubKeyCount, out int ValueCount, out string Name) { SubKeyCount = 0; @@ -610,33 +656,47 @@ private void EnsureSubKeysParsed(ref HiveKey Key) Dictionary SubKeys = new(StringComparer.OrdinalIgnoreCase); - if (Key.SubKeyBlockOffset <= 0) - { - Key.SubKeys = SubKeys; + CollectSubKeys(Key.SubKeyBlockOffset, SubKeys, 0); + + Key.SubKeys = SubKeys; + } + + private void CollectSubKeys(int BlockOffset, Dictionary SubKeys, int Depth) + { + const int MaxIndexDepth = 4; + + if (BlockOffset <= 0 || Depth > MaxIndexDepth) return; - } - int ItemAbs = MainRootOffset + Key.SubKeyBlockOffset; + int ItemAbs = MainRootOffset + BlockOffset; EnsureInBounds(ItemAbs, 0x0C); string BlockType = ReadAscii(ItemAbs + 4, 2); + if (BlockType.Length != 2) + return; - if (BlockType.Length != 2 || (BlockType[1] != 'f' && BlockType[1] != 'h')) - { - Key.SubKeys = SubKeys; + bool Hashed = BlockType[0] == 'l' && (BlockType[1] == 'f' || BlockType[1] == 'h'); + bool IndexRoot = BlockType == "ri"; + + if (!Hashed && !IndexRoot && BlockType != "li") return; - } - short Count = ReadI16(ItemAbs + 0x06); + int EntrySize = Hashed ? 8 : 4; + int Count = (ushort)ReadI16(ItemAbs + 0x06); int EntriesAbs = ItemAbs + 0x08; - EnsureInBounds(EntriesAbs, Count * 8); + EnsureInBounds(EntriesAbs, Count * EntrySize); for (int i = 0; i < Count; i++) { - int EntryAbs = EntriesAbs + (i * 8); + int Offset = ReadI32(EntriesAbs + (i * EntrySize)); + + if (IndexRoot) + { + CollectSubKeys(Offset, SubKeys, Depth + 1); + continue; + } - int Offset = ReadI32(EntryAbs); int SubKeyAbs = MainRootOffset + Offset; EnsureInBounds(SubKeyAbs, 0x60); @@ -655,8 +715,6 @@ private void EnsureSubKeysParsed(ref HiveKey Key) if (!SubKeys.ContainsKey(SubKeyName)) SubKeys.Add(SubKeyName, Offset); } - - Key.SubKeys = SubKeys; } private byte[] ReadValueData(RawHiveValue Raw) diff --git a/Brovan/Program.cs b/Brovan/Program.cs index 3977869..70b0584 100644 --- a/Brovan/Program.cs +++ b/Brovan/Program.cs @@ -142,6 +142,10 @@ static void ShowHelp() Console.WriteLine(" --install-runtimes"); Console.WriteLine(" Download only the Visual C++ runtimes into WindowsLibs, then exit. Useful when"); Console.WriteLine(" the Windows system files are already in place."); + Console.WriteLine(" --install-dxvk[=]"); + Console.WriteLine(" Download a DXVK release from GitHub into the emulated System32 and SysWOW64,"); + Console.WriteLine(" then exit. Direct3D 8 to 11 programs are translated to Vulkan through it."); + Console.WriteLine(" Defaults to the newest release; pass a release tag such as v2.7.1 to pin one."); Console.WriteLine(" --windows-iso

Use a local ISO/WIM/ESD or a direct URL instead of asking Microsoft for a link."); Console.WriteLine(" --windows-image "); Console.WriteLine(" Edition index inside the installation image. Defaults to 1."); @@ -323,6 +327,8 @@ private static bool TryInstallWindowsSystemFiles(string[] args) { bool Requested = false; bool RuntimesOnly = false; + bool DxvkRequested = false; + string DxvkVersion = null; WindowsSetupOptions Options = new WindowsSetupOptions(); for (int i = 0; i < args.Length; i++) @@ -333,6 +339,13 @@ private static bool TryInstallWindowsSystemFiles(string[] args) Requested = true; else if (Arg == "--install-runtimes") RuntimesOnly = true; + else if (Arg == "--install-dxvk") + DxvkRequested = true; + else if (Arg.StartsWith("--install-dxvk=", StringComparison.Ordinal)) + { + DxvkRequested = true; + DxvkVersion = Arg.Substring("--install-dxvk=".Length); + } else if (Arg == "--accept-windows-license") Options.LicenseAccepted = true; else if (Arg == "--windows-iso" && i + 1 < args.Length) @@ -345,12 +358,19 @@ private static bool TryInstallWindowsSystemFiles(string[] args) int.TryParse(Arg.Substring("--windows-image=".Length), out Options.ImageIndex); } - if (!Requested && !RuntimesOnly) + if (!Requested && !RuntimesOnly && !DxvkRequested) return false; - bool Installed = Requested - ? WindowsSetup.Install(AppContext.BaseDirectory, Options, Message => PrintHighlight(Message), ConfirmWindowsLicense) - : WindowsSetup.InstallRuntimes(AppContext.BaseDirectory, Options.LicenseAccepted, Message => PrintHighlight(Message), ConfirmWindowsLicense); + bool Installed = true; + + if (Requested) + Installed = WindowsSetup.Install(AppContext.BaseDirectory, Options, Message => PrintHighlight(Message), ConfirmWindowsLicense); + else if (RuntimesOnly) + Installed = WindowsSetup.InstallRuntimes(AppContext.BaseDirectory, Options.LicenseAccepted, Message => PrintHighlight(Message), ConfirmWindowsLicense); + + if (DxvkRequested && Installed) + Installed = DxvkImporter.Import(AppContext.BaseDirectory, DxvkVersion, Message => PrintHighlight(Message)); + Environment.Exit(Installed ? 0 : 1); return true; } From 8d6c2772e338fe76a8c4703be48ec1fcf9d3034e Mon Sep 17 00:00:00 2001 From: AdvDebug <90452585+AdvDebug@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:12:00 +0300 Subject: [PATCH 2/2] Fix audio stream lifetime and RPC bounds checks --- .../OS/Windows/Misc/NtCreateEvent.cs | 41 ++++----- Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs | 5 +- .../OS/Windows/RPC/Ports/AudioSrvPort.cs | 43 ++++++++- .../OS/Windows/RPC/Ports/AudioStreamEngine.cs | 87 +++++++++++-------- .../Emulation/OS/Windows/WinInternalHelper.cs | 3 + 5 files changed, 115 insertions(+), 64 deletions(-) diff --git a/Brovan/Core/Emulation/OS/Windows/Misc/NtCreateEvent.cs b/Brovan/Core/Emulation/OS/Windows/Misc/NtCreateEvent.cs index 1553ba1..e63f08f 100644 --- a/Brovan/Core/Emulation/OS/Windows/Misc/NtCreateEvent.cs +++ b/Brovan/Core/Emulation/OS/Windows/Misc/NtCreateEvent.cs @@ -6,34 +6,31 @@ internal class NtCreateEvent : IWinSyscall { public NTSTATUS Handle(BinaryEmulator Instance) { - { - ulong EventHandlePtr = Instance.WinHelper.GetArg(0); - ulong DesiredAccess = (uint)Instance.WinHelper.GetArg(1); - ulong ObjectAttributes = Instance.WinHelper.GetArg(2); - uint EventType = (uint)Instance.WinHelper.GetArg(3); - bool InitialState = (byte)Instance.WinHelper.GetArg(4) != 0; + ulong EventHandlePtr = Instance.WinHelper.GetArg(0); + ulong DesiredAccess = (uint)Instance.WinHelper.GetArg(1); + ulong ObjectAttributes = Instance.WinHelper.GetArg(2); + uint EventType = (uint)Instance.WinHelper.GetArg(3); + bool InitialState = (byte)Instance.WinHelper.GetArg(4) != 0; - if (EventHandlePtr == 0) - return NTSTATUS.STATUS_INVALID_PARAMETER; + if (EventHandlePtr == 0) + return NTSTATUS.STATUS_INVALID_PARAMETER; - if (!Instance.IsRegionMapped(EventHandlePtr, (uint)Instance.WinHelper.PointerSize)) - return NTSTATUS.STATUS_ACCESS_VIOLATION; + if (!Instance.IsRegionMapped(EventHandlePtr, (uint)Instance.WinHelper.PointerSize)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; - if (EventType > 1) - return NTSTATUS.STATUS_INVALID_PARAMETER; + if (EventType > 1) + return NTSTATUS.STATUS_INVALID_PARAMETER; - string Name = null; - if (ObjectAttributes != 0) - Instance.WinHelper.TryReadObjectAttributesName(ObjectAttributes, out _, out _, out Name, out _); + string Name = null; + if (ObjectAttributes != 0) + Instance.WinHelper.TryReadObjectAttributesName(ObjectAttributes, out _, out _, out Name, out _); - AccessMask Permissions = (AccessMask)(uint)DesiredAccess; - WinHandle Handle = Instance.WinHelper.CreateEventHandle(Name, EventType, InitialState, Permissions); - if (!Instance.WinHelper.WritePointer(EventHandlePtr, Handle.Handle)) - return NTSTATUS.STATUS_ACCESS_VIOLATION; + AccessMask Permissions = (AccessMask)(uint)DesiredAccess; + WinHandle Handle = Instance.WinHelper.CreateEventHandle(Name, EventType, InitialState, Permissions); + if (!Instance.WinHelper.WritePointer(EventHandlePtr, Handle.Handle)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; - return NTSTATUS.STATUS_SUCCESS; - } - return Instance.WinUnimplemented; + return NTSTATUS.STATUS_SUCCESS; } } } diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs b/Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs index 5a8d3d5..5a5085d 100644 --- a/Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs +++ b/Brovan/Core/Emulation/OS/Windows/RPC/Ndr20.cs @@ -214,10 +214,11 @@ public bool TryReadConformantWideString(out string Value) if (ActualCount > MaxCount) return false; - int Bytes = checked((int)ActualCount * 2); - if (Position + Bytes > Data.Length) + if (ActualCount > (uint)(Data.Length - Position) / 2) return false; + int Bytes = (int)ActualCount * 2; + Value = Encoding.Unicode.GetString(Data.Slice(Position, Bytes)).TrimEnd('\0'); Position += (Bytes + 3) & ~3; return true; diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioSrvPort.cs b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioSrvPort.cs index 0f55f77..b76f487 100644 --- a/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioSrvPort.cs +++ b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioSrvPort.cs @@ -40,6 +40,7 @@ private sealed class AudioStream public uint StreamFlags; public ulong ControlBlock; public ulong SectionHandle; + public ulong ServerSectionHandle; public uint RingBytes; public uint PeriodFrames; public string HandlePortName; @@ -315,7 +316,12 @@ private static byte[] ReleaseStream(in LrpcMessage Message, BinaryEmulator Insta if (TryGetStream(Cookie, out AudioStream Stream)) { Log(Instance, $"stream {Stream.Id} released after {Stream.Engine?.RenderedBytes ?? 0} bytes rendered."); - Stream.Engine?.Dispose(); + + bool EngineStopped = Stream.Engine?.Stop() ?? true; + if (EngineStopped && Stream.ServerSectionHandle != 0) + Instance.WinHelper.CloseHandle(Stream.ServerSectionHandle); + + RemoveHandlePort(Instance, Stream); Streams.Remove(Stream.Id); } @@ -420,6 +426,8 @@ private static bool TryCreateSharedBuffer(BinaryEmulator Instance, AudioStream S Stream.SectionHandle = Instance.WinHelper.CreateSectionHandle( null, SectionSize, PageReadWrite, 0, null, Backing, AccessMask.StandardRightsAll).Handle; + HoldServerSectionReference(Instance, Stream); + // The engine walks the ring from a host thread, so it needs a host pointer into the section // rather than the emulator's memory accessors, which are only safe from the guest thread. IntPtr Host = Instance.GetHostPointer(Backing, SectionSize); @@ -437,6 +445,17 @@ private static bool TryCreateSharedBuffer(BinaryEmulator Instance, AudioStream S return true; } + private static void HoldServerSectionReference(BinaryEmulator Instance, AudioStream Stream) + { + WinSection Section = Instance.WinHelper.GetSectionByHandle(Stream.SectionHandle, AccessMask.StandardRightsAll); + if (Section == null) + return; + + WinHandle ServerHandle = Instance.WinHelper.HandleManager.AddHandle(Section, AccessMask.StandardRightsAll); + Instance.WinHelper.AddWinHandle(ServerHandle); + Stream.ServerSectionHandle = ServerHandle.Handle; + } + private static void PublishHandlePort(BinaryEmulator Instance, AudioStream Stream) { Stream.HandlePortName = HandlePortPrefix + Stream.Id; @@ -451,6 +470,19 @@ private static void PublishHandlePort(BinaryEmulator Instance, AudioStream Strea Encoding.Unicode.GetBytes(Stream.HandlePortName + "\0")); } + private static void RemoveHandlePort(BinaryEmulator Instance, AudioStream Stream) + { + if (Stream.HandlePortName == null) + return; + + List Ports = Instance.WinHelper.WinPorts; + for (int Index = Ports.Count - 1; Index >= 0; Index--) + { + if (string.Equals(Ports[Index].Name, Stream.HandlePortName, StringComparison.OrdinalIgnoreCase)) + Ports.RemoveAt(Index); + } + } + private static NTSTATUS HandleEventPortMessage(WinPort Port, byte[] SendData, PortReply Reply, BinaryEmulator Instance) { ulong EventHandle = Port.ReceivedHandles is { Count: > 0 } ? Port.ReceivedHandles[0] : 0; @@ -487,9 +519,14 @@ private static bool TryGetStreamByHandlePort(string PortName, out AudioStream St private static uint FramesForDuration(ulong DurationHns) { - ulong Frames = DurationHns * MixSampleRate / HundredNanosecondsPerSecond; uint MaxFrames = MaxRingBytes / MixBlockAlign; - return Frames == 0 ? 1 : (uint)Math.Min(Frames, MaxFrames); + ulong MaxDurationHns = (ulong)MaxFrames * HundredNanosecondsPerSecond / MixSampleRate; + + if (DurationHns >= MaxDurationHns) + return MaxFrames; + + ulong Frames = DurationHns * MixSampleRate / HundredNanosecondsPerSecond; + return Frames == 0 ? 1 : (uint)Frames; } private static uint RingBytesForDuration(ulong DurationHns) diff --git a/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioStreamEngine.cs b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioStreamEngine.cs index 53885d3..182af44 100644 --- a/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioStreamEngine.cs +++ b/Brovan/Core/Emulation/OS/Windows/RPC/Ports/AudioStreamEngine.cs @@ -4,7 +4,7 @@ namespace Brovan.Core.Emulation.OS.Windows.RPC.Ports { - internal sealed class AudioStreamEngine : IDisposable + internal sealed class AudioStreamEngine { private const int IdleSleepMilliseconds = 5; private const int ChunkMilliseconds = 10; @@ -18,6 +18,7 @@ internal sealed class AudioStreamEngine : IDisposable private readonly IntPtr Block; private readonly uint BufferStart; private readonly uint RingBytes; + private readonly int BlockAlign; private readonly IAudioSink Sink; private readonly byte[] Chunk; private readonly byte[] Silence; @@ -37,6 +38,7 @@ public AudioStreamEngine(IntPtr Block, uint BufferStart, uint RingBytes, AudioSi this.Block = Block; this.BufferStart = BufferStart; this.RingBytes = RingBytes; + BlockAlign = Format.BlockAlign; Sink = AudioSinkFactory.Create(Format, out string SinkBackend); Backend = SinkBackend; @@ -59,40 +61,51 @@ private unsafe void Run() { byte* Base = (byte*)Block; - while (!Stopping) + // The sink is torn down here rather than in Dispose so that no host device buffer is freed + // while this thread is still inside Sink.Write. + try { - if ((*(uint*)(Base + OffVolatileFlags) & FlagRunning) == 0) + while (!Stopping) { - Thread.Sleep(IdleSleepMilliseconds); - continue; - } - - long Written = Interlocked.CompareExchange(ref *(long*)(Base + OffClientCursor), 0, 0); - long Read = *(long*)(Base + OffServerCursor); - long Available = Written - Read; - - // Feed the device anyway when the guest is behind, both to keep it from underrunning and - // because the write is what advances real time for this loop. - if (Available <= 0) - { - Sink.Write(Silence); + if ((*(uint*)(Base + OffVolatileFlags) & FlagRunning) == 0) + { + Thread.Sleep(IdleSleepMilliseconds); + continue; + } + + long Written = Interlocked.CompareExchange(ref *(long*)(Base + OffClientCursor), 0, 0); + long Read = *(long*)(Base + OffServerCursor); + long Available = Written - Read; + + int Take = (int)Math.Min(Available, Chunk.Length); + Take -= Take % BlockAlign; + + // Feed the device anyway when the guest is behind, both to keep it from underrunning and + // because the write is what advances real time for this loop. + if (Take <= 0) + { + Sink.Write(Silence); + SignalPeriod(); + continue; + } + + int Offset = (int)(Read % RingBytes); + int Contiguous = Math.Min(Take, (int)RingBytes - Offset); + + new ReadOnlySpan(Base + BufferStart + Offset, Contiguous).CopyTo(Chunk); + if (Contiguous < Take) + new ReadOnlySpan(Base + BufferStart, Take - Contiguous).CopyTo(Chunk.AsSpan(Contiguous)); + + Sink.Write(Chunk.AsSpan(0, Take)); + + Interlocked.Exchange(ref *(long*)(Base + OffServerCursor), Read + Take); + RenderedBytes += Take; SignalPeriod(); - continue; } - - int Take = (int)Math.Min(Available, Chunk.Length); - int Offset = (int)(Read % RingBytes); - int Contiguous = Math.Min(Take, (int)RingBytes - Offset); - - new ReadOnlySpan(Base + BufferStart + Offset, Contiguous).CopyTo(Chunk); - if (Contiguous < Take) - new ReadOnlySpan(Base + BufferStart, Take - Contiguous).CopyTo(Chunk.AsSpan(Contiguous)); - - Sink.Write(Chunk.AsSpan(0, Take)); - - Interlocked.Exchange(ref *(long*)(Base + OffServerCursor), Read + Take); - RenderedBytes += Take; - SignalPeriod(); + } + finally + { + Sink.Dispose(); } } @@ -103,14 +116,14 @@ private void SignalPeriod() Event.Signaled = true; } - public void Dispose() + ///

+ /// Returns false when the worker is still running, which means the guest memory it renders from + /// must stay mapped. + /// + public bool Stop() { - if (Stopping) - return; - Stopping = true; - Worker.Join(StopTimeoutMilliseconds); - Sink.Dispose(); + return Worker.Join(StopTimeoutMilliseconds); } } } diff --git a/Brovan/Core/Emulation/OS/Windows/WinInternalHelper.cs b/Brovan/Core/Emulation/OS/Windows/WinInternalHelper.cs index 8e90c37..a0f2835 100644 --- a/Brovan/Core/Emulation/OS/Windows/WinInternalHelper.cs +++ b/Brovan/Core/Emulation/OS/Windows/WinInternalHelper.cs @@ -178,6 +178,9 @@ private bool TryGetEntry(ulong Handle, out ulong Key, out HandleEntry Entry) return true; } + // NT ignores the low two bits of a handle (OBJ_HANDLE_TAGBITS). + // user mode stores flags there, ntdll's loader lock and RPC among others. Handle values are allocated four apart, so masking + // cannot alias two live handles. Key = Handle & ~3UL; return Key != Handle && HandleTable.TryGetValue(Key, out Entry); }