From f672db60d50431966a50364b36123af00499855e Mon Sep 17 00:00:00 2001 From: Mack Straight Date: Sat, 3 Oct 2026 11:22:25 -0400 Subject: [PATCH 1/2] test: run a loopback with application routing on Adds an app-routing scenario to the issues group: SarAsio's COM classes registered and a loopback run whose configuration turns application routing on. That starts the driver's registry filter, which then sees every registry value query on the machine, and sends the harness's own WASAPI client through SarAsio's device enumerator wrapper. SarTest gains --app-routing for the configuration it writes. Co-Authored-By: Claude Opus 5.5 --- SarTest/SarTest.cpp | 5 +++-- SarTest/install.cpp | 5 +++-- SarTest/install.h | 6 ++++-- tools/test/Invoke-SarTest.ps1 | 20 +++++++++++++++++++- tools/test/README.md | 4 +++- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/SarTest/SarTest.cpp b/SarTest/SarTest.cpp index 7639651..fbb19e4 100644 --- a/SarTest/SarTest.cpp +++ b/SarTest/SarTest.cpp @@ -68,6 +68,7 @@ int usage() " --id-prefix (endpoint IDs; default: the name prefix)\n" "Other options: --results --phase-timeout S --no-sarasio-log\n" " --keep-config (host/run: do not rewrite default.json)\n" + " --app-routing (turn on application routing in default.json)\n" " --registered (use the COM-registered SarAsio instead of a path)\n" " --wait S --wait-gone S --restart-delay MS --rate HZ\n" " --min-valid RATIO --max-discontinuities N\n" @@ -145,7 +146,7 @@ bool prepareConfig(const Args& args, const EndpointLayout& layout, } if (!writeDriverConfig(layout, driverClsid, - args.getInt(L"wavert-min-frames", 0), path)) { + args.getInt(L"wavert-min-frames", 0), path, args.has(L"app-routing"))) { return false; } @@ -211,7 +212,7 @@ int cmdConfig(const Args& args) std::wstring out = args.get(L"out", configurationPath().c_str()); if (!writeDriverConfig(layout, SAR_TEST_CLOCK_CLSID_STR, - args.getInt(L"wavert-min-frames", 0), out)) { + args.getInt(L"wavert-min-frames", 0), out, args.has(L"app-routing"))) { return 1; } diff --git a/SarTest/install.cpp b/SarTest/install.cpp index 18e1957..0d3986b 100644 --- a/SarTest/install.cpp +++ b/SarTest/install.cpp @@ -58,7 +58,8 @@ std::wstring loggingPath() bool writeDriverConfig( const EndpointLayout& layout, const std::wstring& driverClsid, - int waveRtMinimumFrames, const std::wstring& path) + int waveRtMinimumFrames, const std::wstring& path, + bool applicationRouting) { std::vector endpoints; @@ -80,7 +81,7 @@ bool writeDriverConfig( JsonObject root; root.setString("driverClsid", narrow(driverClsid)) - .setBool("enableApplicationRouting", false) + .setBool("enableApplicationRouting", applicationRouting) .setInt("waveRtMinimumFrames", waveRtMinimumFrames) .setRaw("endpoints", jsonArray(endpoints)) .setRaw("applications", "[]"); diff --git a/SarTest/install.h b/SarTest/install.h index a029223..748dd86 100644 --- a/SarTest/install.h +++ b/SarTest/install.h @@ -28,10 +28,12 @@ std::wstring configurationPath(); std::wstring loggingPath(); // Writes a SarAsio default.json describing the layout, with the given ASIO -// driver CLSID as the physical interface and application routing disabled. +// driver CLSID as the physical interface. Application routing (no rules, so +// only the redirection of the audio device enumerator) is off unless asked. bool writeDriverConfig( const EndpointLayout& layout, const std::wstring& driverClsid, - int waveRtMinimumFrames, const std::wstring& path); + int waveRtMinimumFrames, const std::wstring& path, + bool applicationRouting = false); // Software device node for the SAR driver, matching the INF's hardware ID. bool deviceNodeExists(); diff --git a/tools/test/Invoke-SarTest.ps1 b/tools/test/Invoke-SarTest.ps1 index 45d598a..1cf8f9c 100644 --- a/tools/test/Invoke-SarTest.ps1 +++ b/tools/test/Invoke-SarTest.ps1 @@ -109,6 +109,16 @@ function Invoke-Scenario { return $record } +# Registers or unregisters SarAsio's COM classes. The driver's application +# routing only redirects to SarAsio when they are registered. +function Set-SarAsioRegistered { + param([bool]$Registered) + + $regArgs = @('/s') + $(if ($Registered) { @() } else { @('/u') }) + @("`"$sarAsio`"") + $proc = Start-Process regsvr32.exe -ArgumentList $regArgs -Wait -PassThru + if ($proc.ExitCode -ne 0) { Write-Warning "regsvr32 $($regArgs -join ' ') exited with $($proc.ExitCode)" } +} + # Windows Server images ship with the audio stack disabled. foreach ($service in 'AudioEndpointBuilder', 'Audiosrv') { try { @@ -146,7 +156,7 @@ if (-not $SkipInstall) { $common = @('--sarasio', "`"$sarAsio`"", '--channels', $Channels) if ($Scenarios -contains 'issues') { - # Regression checks for specific GitHub issues, one short host each. + # Regression checks, one short host each, most for a GitHub issue. $summary.scenarios += Invoke-Scenario 'issue-133-control-panel' ( @('control-panel', '--endpoints', 1) + $common) -Timeout 120 $summary.scenarios += Invoke-Scenario 'issue-54-no-interface' ( @@ -158,6 +168,14 @@ if ($Scenarios -contains 'issues') { $summary.scenarios += Invoke-Scenario 'long-endpoint-name' ( @('host', '--endpoints', 1, '--duration', 2, '--id-prefix', 'SarTest-long', '--prefix', ('SarTestLongEndpointName' * 4)) + $common) -Timeout 120 + # Application routing on: from then on the driver's registry filter sees + # every registry value query on the machine. With SarAsio's COM classes + # registered, the harness's own WASAPI client also goes through SarAsio's + # device enumerator wrapper. + Set-SarAsioRegistered $true + $summary.scenarios += Invoke-Scenario 'app-routing' ( + @('run', '--endpoints', 1, '--iterations', 1, '--duration', $Duration, '--app-routing') + $common) + Set-SarAsioRegistered $false } if ($Scenarios -contains 'matrix') { diff --git a/tools/test/README.md b/tools/test/README.md index e677faf..d6d668b 100644 --- a/tools/test/README.md +++ b/tools/test/README.md @@ -92,7 +92,9 @@ configured and checks that it still offers a stereo input and output. then gives it a new ID, and checks that the new ID shows the new name; the results record what Windows shows for the reused ID. `long-endpoint-name` runs a host whose endpoint names are longer than the driver's 63-character -limit. +limit. `app-routing` runs a loopback with application routing on and +SarAsio's COM classes registered, which turns on the driver's registry +filter. `Invoke-SarTest.ps1 -Scenarios` selects any of `issues`, `matrix`, `race` and `kill`. From 705ce6732d678b7d887230a47faf9cff40178a8e Mon Sep 17 00:00:00 2001 From: Mack Straight Date: Sat, 3 Oct 2026 11:22:51 -0400 Subject: [PATCH 2/2] driver: look up registry redirects at PASSIVE_LEVEL With application routing on, the driver's registry callback runs for every registry value query on the machine. It looked the key up in the redirect tables under a spin lock, at DISPATCH_LEVEL, but the table's comparison routine calls RtlCompareUnicodeString, which is pageable (and the key was copied to nonpaged memory first only because of the lock). When that code was paged out, the lookup bugchecked with IRQL_NOT_LESS_OR_EQUAL. Under Driver Verifier it happens on the first registry query after a host turns application routing on: IRQL_NOT_LESS_OR_EQUAL (a), process taskhostw.exe nt!RtlCompareUnicodeString SynchronousAudioRouter!SarCompareStringTableEntry+0x16 nt!RtlLookupElementGenericTableAvl+0xec SynchronousAudioRouter!SarGetStringTableEntry+0x21 SynchronousAudioRouter!SarFilterMatchesPath+0x7a SynchronousAudioRouter!SarRegistryCallback+0x6d nt!CmpCallCallBacksEx+0x283 Guard the tables with an ERESOURCE instead, as the old TODO suggested, and look up the configuration manager's path directly. Co-Authored-By: Claude Opus 5.5 --- SynchronousAudioRouter/entry.cpp | 34 ++++++++++++++------------------ SynchronousAudioRouter/sar.h | 9 +++++---- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/SynchronousAudioRouter/entry.cpp b/SynchronousAudioRouter/entry.cpp index 91d4049..62f9936 100644 --- a/SynchronousAudioRouter/entry.cpp +++ b/SynchronousAudioRouter/entry.cpp @@ -718,7 +718,6 @@ NTSTATUS SarIrpDeviceControl(PDEVICE_OBJECT deviceObject, PIRP irp) VOID SarUnload(PDRIVER_OBJECT driverObject) { SAR_INFO("SAR is unloading"); - KIRQL irql; SarDriverExtension *extension = (SarDriverExtension *)IoGetDriverObjectExtension( driverObject, DriverEntry); @@ -731,12 +730,15 @@ VOID SarUnload(PDRIVER_OBJECT driverObject) CmUnRegisterCallback(extension->filterCookie); } - irql = ExAcquireSpinLockExclusive(&extension->registryRedirectLock); + KeEnterCriticalRegion(); + ExAcquireResourceExclusiveLite(&extension->registryRedirectLock, TRUE); SarClearStringTable( &extension->registryRedirectTableWow64, SarDeleteRegistryRedirect); SarClearStringTable( &extension->registryRedirectTable, SarDeleteRegistryRedirect); - ExReleaseSpinLockExclusive(&extension->registryRedirectLock, irql); + ExReleaseResourceLite(&extension->registryRedirectLock); + KeLeaveCriticalRegion(); + ExDeleteResourceLite(&extension->registryRedirectLock); if (extension->filterUser) { // Allocated by SeQueryInformationToken @@ -768,9 +770,6 @@ BOOL SarFilterMatchesPath( { PRTL_AVL_TABLE table = &extension->registryRedirectTable; PVOID entry = nullptr; - KIRQL irql; - UNICODE_STRING nonpagedPath; - NTSTATUS status = STATUS_SUCCESS; if (!path) { return FALSE; @@ -782,24 +781,20 @@ BOOL SarFilterMatchesPath( } #endif - // TODO: we get a path that's in paged memory from the configuration manager - // so we can't access it with IRQL raised to dispatch level by the spinlock. - // Probably better to just use an ERESOURCE or something here instead. - status = SarStringDuplicate(&nonpagedPath, path); - - if (!NT_SUCCESS(status)) { - return FALSE; - } - - irql = ExAcquireSpinLockShared(&extension->registryRedirectLock); - entry = SarGetStringTableEntry(table, &nonpagedPath); + // Stay at PASSIVE_LEVEL: the path from the configuration manager is in + // paged memory and the table's string comparison is pageable code. Under + // a spin lock this bugchecked (IRQL_NOT_LESS_OR_EQUAL) whenever that code + // was paged out. + KeEnterCriticalRegion(); + ExAcquireResourceSharedLite(&extension->registryRedirectLock, TRUE); + entry = SarGetStringTableEntry(table, path); if (entry) { *redirectPath = *(PCUNICODE_STRING)entry; } - ExReleaseSpinLockShared(&extension->registryRedirectLock, irql); - SarStringFree(&nonpagedPath); + ExReleaseResourceLite(&extension->registryRedirectLock); + KeLeaveCriticalRegion(); return entry != nullptr; } @@ -1129,6 +1124,7 @@ extern "C" NTSTATUS DriverEntry( RtlZeroMemory(extension, sizeof(SarDriverExtension)); ExInitializeFastMutex(&extension->mutex); + ExInitializeResourceLite(&extension->registryRedirectLock); SarInitializeTable(&extension->controlContextTable); SarInitializeStringTable(&extension->registryRedirectTableWow64); SarInitializeStringTable(&extension->registryRedirectTable); diff --git a/SynchronousAudioRouter/sar.h b/SynchronousAudioRouter/sar.h index d0733d6..f24fc92 100644 --- a/SynchronousAudioRouter/sar.h +++ b/SynchronousAudioRouter/sar.h @@ -210,11 +210,12 @@ typedef struct SarDriverExtension // This shouldn't really be needed as long as CmUnRegisterCallback can't // complete while one of our callback routines is still running, but there // is no explicit documentation of that, so we protect the registry redirect - // tables with a reader writer spinlock "just in case." Note that + // tables with a reader writer lock "just in case." Note that // RTL_AVL_TABLE is used here instead of RTL_GENERIC_TABLE to avoid - // splaying which would be unsafe with a read-only lock. All related - // structures must be in nonpaged memory. - EX_SPIN_LOCK registryRedirectLock; + // splaying which would be unsafe with a read-only lock. It is a resource, + // not a spin lock: lookups compare strings with RtlCompareUnicodeString, + // which is pageable, and the registry callback runs at PASSIVE_LEVEL. + ERESOURCE registryRedirectLock; RTL_AVL_TABLE registryRedirectTableWow64; RTL_AVL_TABLE registryRedirectTable; LARGE_INTEGER filterCookie;