Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 167 additions & 16 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,187 @@
name: Build SynchronousAudioRouter
name: build-and-package

# Verifiable WDK build pipeline.
#
# Produces, for every push/PR, an UNSIGNED but submission-ready driver package
# (.sys/.inf/.cat + a .cab) together with SHA-256 hashes and - on master/tags -
# a cryptographic build-provenance attestation linking each binary to its exact
# source commit. That provenance + reproducibility is what OSSign / Microsoft
# attestation signing require before they will sign the driver. See BUILDING.md.

on:
workflow_dispatch:
push:
branches: [master]
tags: ["v*"]
pull_request:
branches: [master]

permissions:
contents: read
pull-requests: read

concurrency:
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true
jobs:
build:
runs-on: windows-latest

env:
# Keep in lockstep with <WindowsTargetPlatformVersion> in the driver .vcxproj
# and with the defaults in tools/Install-WdkBuildEnv.ps1.
SDK_VERSION: "10.0.22621.0"

jobs:
# ---------------------------------------------------------------------------
# Kernel driver: needs the WDK + its VS extension installed on the runner.
# ARM64 is intentionally omitted for now: the INF has no [Standard.NTARM64]
# model yet, so Inf2Cat cannot package it. Add that section first (see
# BUILDING.md) before enabling an ARM64 matrix entry.
# ---------------------------------------------------------------------------
driver:
name: driver (${{ matrix.platform }})
runs-on: windows-2022
permissions:
contents: read
id-token: write # required by attest-build-provenance
attestations: write
strategy:
fail-fast: false
matrix:
platform: [x64]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # tags needed for version stamping

- name: Setup MSBuild
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v2
with:
vs-version: "[17.0,18.0)"
msbuild-architecture: x64
- name: Build 32
working-directory: ${{env.GITHUB_WORKSPACE}}
run: msbuild SynchronousAudioRouter.sln /t:Build /p:Configuration=Release /p:Platform=x86 /m /verbosity:minimal
- name: Build 64
working-directory: ${{env.GITHUB_WORKSPACE}}
run: msbuild SynchronousAudioRouter.sln /t:Build /p:Configuration=Release /p:Platform=x64 /m /verbosity:minimal
- name: Artifact

- name: Install WDK build environment
shell: pwsh
run: ./tools/Install-WdkBuildEnv.ps1 -SdkVersion "${{ env.SDK_VERSION }}"

- name: Compute version
id: ver
shell: pwsh
run: |
$desc = (& git describe --tags --always --dirty 2>$null)
if (-not $desc) { $desc = "0.0.0-$(& git rev-parse --short HEAD)" }
$tag = (& git describe --tags --abbrev=0 2>$null)
if ($tag -match '(\d+)\.(\d+)\.(\d+)') {
$numeric = "$($Matches[1]).$($Matches[2]).$($Matches[3]).$env:GITHUB_RUN_NUMBER"
} else {
$numeric = "0.0.0.$env:GITHUB_RUN_NUMBER"
}
"describe=$desc" | Out-File $env:GITHUB_OUTPUT -Append
"numeric=$numeric" | Out-File $env:GITHUB_OUTPUT -Append
Write-Host "version: $desc (DriverVer numeric: $numeric)"

- name: Build driver
shell: pwsh
run: |
# Pin to whatever 10.0.22621.x SDK the runner actually has installed,
# so the build doesn't fail if the image carries a different revision
# than the .0 hard-coded in the .vcxproj.
$inc = 'C:\Program Files (x86)\Windows Kits\10\Include'
# Pick the highest SDK version that ALSO ships the WDK kernel headers
# (km\ntifs.h). The image's plain SDK and its matching WDK can live under
# different version folders, so requiring km\ntifs.h guarantees both the
# headers and the matching kernel import libs are present for $sdk.
$sdk = (Get-ChildItem $inc -Directory |
Where-Object { Test-Path (Join-Path $_.FullName 'km\ntifs.h') } |
Sort-Object { [version]$_.Name } -Descending |
Select-Object -First 1).Name
if (-not $sdk) { throw "No SDK carrying WDK kernel headers (km\ntifs.h) under $inc" }
Write-Host "Using Windows SDK+WDK: $sdk"

# SignMode=Off: leave the binary unsigned for attestation/OSSign to sign.
# SpectreMitigation=false: keep iteration-1 green without the Spectre libs;
# flip to Spectre + install the libs for production security (BUILDING.md).
& msbuild SynchronousAudioRouter.sln `
/t:SynchronousAudioRouter `
/p:Configuration=Release `
/p:Platform=${{ matrix.platform }} `
/p:WindowsTargetPlatformVersion=$sdk `
/p:SignMode=Off `
/p:SpectreMitigation=false `
/m /verbosity:minimal /nologo
if ($LASTEXITCODE -ne 0) { throw "msbuild failed (exit $LASTEXITCODE)" }

- name: Package + hash
shell: pwsh
run: |
./tools/Package-SarDriver.ps1 `
-Platform ${{ matrix.platform }} `
-Configuration Release `
-DriverVersion ${{ steps.ver.outputs.numeric }} `
-Describe "${{ steps.ver.outputs.describe }}"

- name: Attest build provenance
if: github.event_name == 'push' # OIDC token isn't writable on fork PRs
uses: actions/attest-build-provenance@v1
with:
subject-path: artifacts/${{ matrix.platform }}/SynchronousAudioRouter.sys

- name: Upload driver package
uses: actions/upload-artifact@v4
with:
name: sar-driver-${{ matrix.platform }}
path: artifacts/${{ matrix.platform }}/
if-no-files-found: error

# ---------------------------------------------------------------------------
# User-mode ASIO plugin: plain MSVC, no WDK. 32-bit (Win32) is still built
# because 32-bit DAWs need a 32-bit ASIO DLL.
# ---------------------------------------------------------------------------
usermode:
name: usermode (${{ matrix.platform }})
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
platform: [x64, x86]
steps:
- uses: actions/checkout@v4

- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v2
with:
vs-version: "[17.0,18.0)"

- name: Build SarAsio
shell: pwsh
run: |
& msbuild SynchronousAudioRouter.sln `
/t:SarAsio `
/p:Configuration=Release `
/p:Platform=${{ matrix.platform }} `
/m /verbosity:minimal /nologo
if ($LASTEXITCODE -ne 0) { throw "msbuild failed (exit $LASTEXITCODE)" }

- name: Collect + hash
shell: pwsh
run: |
$plat = "${{ matrix.platform }}"
$stage = "artifacts/$plat"
New-Item -ItemType Directory -Force -Path $stage | Out-Null
$dll = Get-ChildItem -Recurse -Filter 'SarAsio.dll' |
Where-Object { $_.FullName -match "\\Release\\" } |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $dll) { throw "SarAsio.dll not found - did the build succeed?" }
Copy-Item $dll.FullName $stage -Force
# Materialize the hash lines into a variable BEFORE opening the output
# file, and skip the sums file itself - otherwise the same pipeline both
# reads and writes SHA256SUMS.txt and Get-FileHash hits a file lock.
$lines = Get-ChildItem $stage -File |
Where-Object { $_.Name -ne 'SHA256SUMS.txt' } |
Get-FileHash -Algorithm SHA256 |
ForEach-Object { '{0} {1}' -f $_.Hash, (Split-Path $_.Path -Leaf) }
$lines | Out-File "$stage/SHA256SUMS.txt" -Encoding ascii

- name: Upload SarAsio
uses: actions/upload-artifact@v4
with:
name: SynchronousAudioRouter
path: SarInstaller/bin/x64/Release/SynchronousAudioRouter.msi
name: sar-asio-${{ matrix.platform }}
path: artifacts/${{ matrix.platform }}/
if-no-files-found: error
89 changes: 89 additions & 0 deletions BUILDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Building SynchronousAudioRouter

This repo ships a verifiable WDK build pipeline (`.github/workflows/build.yml`)
plus two helper scripts under `tools/`. The goal is a **reproducible, unsigned,
submission-ready driver package** that can then be signed for free via
[OSSign](https://ossign.org/) or Microsoft attestation signing. The build itself
never signs anything — signing is a separate, later step.

## What the pipeline produces

For each run, the `driver` job emits an artifact `sar-driver-x64/` containing:

| file | purpose |
|------|---------|
| `SynchronousAudioRouter.sys` | the unsigned kernel driver |
| `SynchronousAudioRouter.inf` | INF with a **valid** `DriverVer` stamped at build time |
| `SynchronousAudioRouter.cat` | security catalog generated by `Inf2Cat` |
| `SynchronousAudioRouter_x64.cab` | the package you submit for attestation signing |
| `SHA256SUMS.txt` | SHA-256 of every payload file |
| `BUILD-INFO.txt` | commit, version, platform, build time |

On pushes to `master`/tags the job also produces a GitHub **build-provenance
attestation** for the `.sys`, cryptographically linking the binary to the exact
workflow run and source commit. Hashes + provenance are precisely the
"source ↔ binary is verifiable" property OSSign asks for.

The `usermode` job builds the `SarAsio.dll` ASIO plugin for x64 and x86 (32-bit
DAWs need the 32-bit DLL).

## Local build

```powershell
# One-time: install the WDK + its VS extension (needs VS2022 already present)
./tools/Install-WdkBuildEnv.ps1

# Build the driver (unsigned)
msbuild SynchronousAudioRouter.sln /t:SynchronousAudioRouter `
/p:Configuration=Release /p:Platform=x64 /p:SignMode=Off /p:SpectreMitigation=false

# Stage, version-stamp, catalog, cab and hash it
./tools/Package-SarDriver.ps1 -Platform x64 -DriverVersion 0.13.2.0 -Describe local
```

## Signing (the part the build deliberately skips)

A modified kernel driver does **not** inherit anyone else's signature; the
package above must be signed before stock Windows will load it without test mode.

1. **For your own machine, right now (free):** enable test signing
(`bcdedit /set testsigning on`, reboot) and test-sign the `.sys`/`.cat`. Note
the desktop watermark and that this breaks most anti-cheat games and requires
Secure Boot off on Windows 11.
2. **For a clean, redistributable driver (free, the real goal):** submit
`SynchronousAudioRouter_x64.cab` to OSSign or, if you hold an EV certificate,
to Microsoft Partner Center for **attestation signing**. The returned
signature loads on stock Windows 11 with no test mode.

## Known follow-ups (intentionally not done in iteration 1)

These are tracked here so nothing is silently missing:

- **ARM64.** The INF has only `[Standard.NTamd64]` / `[Standard.NTx86]` models,
so `Inf2Cat` cannot package ARM64. Add `[Standard.NTARM64]` (and the matching
install/service sections) before enabling an ARM64 matrix entry. This is the
step that matters for the Windows-on-Arm direction.
- **Spectre mitigation.** The build passes `/p:SpectreMitigation=false` to stay
green without the Spectre-mitigated runtime libs. For a production/attestation
driver, install those libs (VS component
`Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre`) and drop the
flag so it builds with `Spectre`.
- **`DriverVer` in source.** The committed INF still has the placeholder
`DriverVer=0.1`; the pipeline stamps a valid value at build time but the source
value should eventually be fixed too.
- **Installer (MSI).** The WiX `SarInstaller` project is not built by this
pipeline yet (needs the WiX 3.x toolset on the runner). The driver package is
sufficient for signing; the MSI is a packaging convenience to add back later.
- **EWDK alternative.** For maximum reproducibility you can swap the
`Install-WdkBuildEnv.ps1` step for a pinned **Enterprise WDK** ISO (a fully
self-contained, version-locked build environment). That removes the dependency
on the mutable runner image at the cost of a large ISO download per run.

> Note on WDK/SDK version: the CI build does **not** hard-pin a version. The
> `Build driver` step auto-selects the highest SDK under `Windows Kits\10\Include`
> that actually carries the WDK kernel headers (`km\ntifs.h`) and passes it as
> `WindowsTargetPlatformVersion`. On today's `windows-2022` image that resolves to
> `10.0.26100` (the SDK pinned in the `.vcxproj`, `10.0.22621.0`, ships without the
> matching kernel headers, which is why pinning it broke the build). `SDK_VERSION`
> in the workflow and the param in `tools/Install-WdkBuildEnv.ps1` are only a
> fallback-download hint for machines that have no WDK at all.
1 change: 1 addition & 0 deletions SynchronousAudioRouter/SynchronousAudioRouter.inf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ClassGuid={4d36e96c-e325-11ce-bfc1-08002be10318}
Provider=%Provider%
DriverVer=0.1
CatalogFile=SynchronousAudioRouter.cat
PnpLockdown=1

[ControlFlags]
ExcludeFromSelect=*
Expand Down
94 changes: 94 additions & 0 deletions tools/Install-WdkBuildEnv.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<#
.SYNOPSIS
Ensure the WDK kernel-mode platform toolset is available to MSBuild.

.DESCRIPTION
GitHub's windows-2022 image ships the WDK (22H2) + VS2022, but a known
runner-images bug (actions/runner-images#5970) can leave the
"WindowsKernelModeDriver10.0" VS platform toolset *unregistered*, so the
driver project fails to load. This script is defensive and fast:

1. if the kernel toolset is already registered -> do nothing,
2. else install the WDK.vsix that already ships on the image,
3. only if no WDK is present at all -> download + install it (fallback).

Keep $SdkVersion / $WdkFwlink in lockstep with the driver .vcxproj and the
workflow's SDK_VERSION when you bump the target WDK.

.NOTES
Idempotent. Works under Windows PowerShell 5.1 and pwsh 7+.
#>
#Requires -Version 5.1
[CmdletBinding()]
param(
[string]$SdkVersion = '10.0.22621.0',
# WDK for Windows 11, version 22H2 (fallback download only).
[string]$WdkFwlink = 'https://go.microsoft.com/fwlink/?linkid=2196230',
[string]$WorkDir = $(if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP })
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

$VsixSearchRoots = @(
'C:\Program Files (x86)\Windows Kits\10\Vsix',
'C:\Program Files\Windows Kits\10\Vsix'
)

function Find-VsInstall {
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (-not (Test-Path $vswhere)) { throw "vswhere.exe not found: $vswhere" }
$p = (& $vswhere -latest -products * -property installationPath | Select-Object -First 1)
if (-not $p) { throw "No Visual Studio installation found by vswhere." }
return $p.Trim()
}

function Test-KernelToolset([string]$vs) {
# The WDK VS extension registers the kernel-mode toolset here.
$tp = Join-Path $vs 'MSBuild\Microsoft\VC\v170\Platforms\x64\PlatformToolsets\WindowsKernelModeDriver10.0'
return (Test-Path $tp)
}

function Find-WdkVsix {
Get-ChildItem -Path $VsixSearchRoots -Recurse -Filter 'WDK.vsix' -ErrorAction SilentlyContinue |
Sort-Object FullName -Descending | Select-Object -First 1
}

function Install-Vsix([string]$vs, [string]$vsixPath) {
$vsixInstaller = Join-Path $vs 'Common7\IDE\VSIXInstaller.exe'
if (-not (Test-Path $vsixInstaller)) { throw "VSIXInstaller.exe not found: $vsixInstaller" }
Write-Host "==> Installing extension: $vsixPath"
$proc = Start-Process -FilePath $vsixInstaller `
-ArgumentList '/admin', '/quiet', "`"$vsixPath`"" -PassThru -Wait
# 0 = installed, 1001 = already installed.
if ($proc.ExitCode -notin 0, 1001) { throw "VSIXInstaller failed (exit $($proc.ExitCode))." }
}

$vs = Find-VsInstall
Write-Host "==> Visual Studio: $vs"

if (Test-KernelToolset $vs) {
Write-Host "==> WindowsKernelModeDriver10.0 toolset already registered - nothing to do."
return
}

Write-Host "==> Kernel toolset missing; looking for the WDK extension on the image..."
$vsix = Find-WdkVsix

if (-not $vsix) {
Write-Host "==> No WDK found on image; downloading (fallback) from $WdkFwlink ..."
$wdkSetup = Join-Path $WorkDir 'wdksetup.exe'
Invoke-WebRequest -Uri $WdkFwlink -OutFile $wdkSetup -UseBasicParsing
$proc = Start-Process -FilePath $wdkSetup `
-ArgumentList '/q', '/norestart', '/ceip', 'off' -PassThru -Wait
if ($proc.ExitCode -notin 0, 3010) { throw "WDK install failed (exit $($proc.ExitCode))." }
$vsix = Find-WdkVsix
if (-not $vsix) { throw "WDK.vsix not found even after install." }
}

Install-Vsix $vs $vsix.FullName

if (-not (Test-KernelToolset $vs)) {
throw "WindowsKernelModeDriver10.0 still not registered after installing the WDK extension."
}
Write-Host "==> Kernel toolset registered. WDK build environment ready."
Loading