Skip to content

Commit 0d9a85b

Browse files
committed
ci: sync bundled winredirect drivers
1 parent 9b306c0 commit 0d9a85b

6 files changed

Lines changed: 275 additions & 1 deletion

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import hashlib
7+
import sys
8+
from pathlib import Path
9+
10+
11+
class PEFormatError(ValueError):
12+
pass
13+
14+
15+
def read_u16(data: bytes, offset: int) -> int:
16+
return int.from_bytes(data[offset:offset + 2], "little")
17+
18+
19+
def read_u32(data: bytes, offset: int) -> int:
20+
return int.from_bytes(data[offset:offset + 4], "little")
21+
22+
23+
def canonicalize_signed_pe(path: Path) -> bytes:
24+
data = bytearray(path.read_bytes())
25+
if len(data) < 0x40 or data[:2] != b"MZ":
26+
raise PEFormatError(f"{path}: missing DOS header")
27+
28+
pe_offset = read_u32(data, 0x3C)
29+
if pe_offset + 24 > len(data) or data[pe_offset:pe_offset + 4] != b"PE\x00\x00":
30+
raise PEFormatError(f"{path}: missing PE header")
31+
32+
optional_offset = pe_offset + 24
33+
magic = read_u16(data, optional_offset)
34+
if magic == 0x10B:
35+
data_directory_offset = optional_offset + 96
36+
elif magic == 0x20B:
37+
data_directory_offset = optional_offset + 112
38+
else:
39+
raise PEFormatError(f"{path}: unsupported optional header magic 0x{magic:04x}")
40+
41+
checksum_offset = optional_offset + 64
42+
if checksum_offset + 4 > len(data):
43+
raise PEFormatError(f"{path}: truncated optional header")
44+
data[checksum_offset:checksum_offset + 4] = b"\x00" * 4
45+
46+
security_directory_offset = data_directory_offset + 8 * 4
47+
if security_directory_offset + 8 > len(data):
48+
raise PEFormatError(f"{path}: truncated data directories")
49+
50+
certificate_offset = read_u32(data, security_directory_offset)
51+
certificate_size = read_u32(data, security_directory_offset + 4)
52+
data[security_directory_offset:security_directory_offset + 8] = b"\x00" * 8
53+
54+
if certificate_offset == 0 or certificate_size == 0:
55+
return bytes(data)
56+
57+
certificate_end = certificate_offset + certificate_size
58+
if certificate_end > len(data):
59+
raise PEFormatError(f"{path}: certificate table exceeds file size")
60+
61+
return bytes(data[:certificate_offset] + data[certificate_end:])
62+
63+
64+
def canonical_sha256(path: Path) -> str:
65+
return hashlib.sha256(canonicalize_signed_pe(path)).hexdigest()
66+
67+
68+
def main() -> int:
69+
parser = argparse.ArgumentParser(
70+
description="Compare two PE files after stripping Authenticode-only differences.",
71+
)
72+
parser.add_argument("left", type=Path)
73+
parser.add_argument("right", type=Path)
74+
args = parser.parse_args()
75+
76+
try:
77+
left_hash = canonical_sha256(args.left)
78+
right_hash = canonical_sha256(args.right)
79+
except (OSError, PEFormatError) as exc:
80+
print(exc, file=sys.stderr)
81+
return 2
82+
83+
print(f"{args.left}: {left_hash}")
84+
print(f"{args.right}: {right_hash}")
85+
return 0 if left_hash == right_hash else 1
86+
87+
88+
if __name__ == "__main__":
89+
raise SystemExit(main())
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
name: sync winredirect driver
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
- dev
8+
paths:
9+
- '.github/scripts/compare_signed_pe.py'
10+
- '.github/workflows/winredirect-driver-sync.yml'
11+
- 'internal/winredirect/driver/**'
12+
13+
permissions:
14+
contents: write
15+
16+
concurrency:
17+
group: winredirect-driver-${{ github.event.pull_request.head.repo.full_name }}-${{ github.event.pull_request.head.ref }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
sync:
22+
name: Refresh bundled drivers
23+
if: github.event.pull_request.head.repo.full_name == github.repository
24+
runs-on: windows-2025
25+
env:
26+
REPO_DIR: C:\Users\sekai\Projects\sing-tun
27+
steps:
28+
- name: Checkout PR head
29+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
30+
with:
31+
fetch-depth: 0
32+
ref: ${{ github.event.pull_request.head.ref }}
33+
34+
- name: Install Windows SDK and WDK
35+
shell: pwsh
36+
run: |
37+
$sdkHeader = 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\Windows.h'
38+
$wdkHeader = 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\km\ntddk.h'
39+
40+
if (-not (Test-Path $sdkHeader)) {
41+
winget install --source winget --exact --id Microsoft.WindowsSDK.10.0.26100 --accept-source-agreements --accept-package-agreements --disable-interactivity --log "$env:RUNNER_TEMP\sdk-install.log"
42+
Write-Host "Windows SDK install exit code: $LASTEXITCODE"
43+
}
44+
45+
if (-not (Test-Path $wdkHeader)) {
46+
winget install --source winget --exact --id Microsoft.WindowsWDK.10.0.26100 --accept-source-agreements --accept-package-agreements --disable-interactivity --log "$env:RUNNER_TEMP\wdk-install.log"
47+
Write-Host "WDK install exit code: $LASTEXITCODE"
48+
}
49+
50+
if (-not (Test-Path $sdkHeader)) {
51+
throw "Windows SDK header not found after installation: $sdkHeader"
52+
}
53+
if (-not (Test-Path $wdkHeader)) {
54+
throw "WDK header not found after installation: $wdkHeader"
55+
}
56+
57+
- name: Mirror repository to stable Windows path
58+
shell: pwsh
59+
run: |
60+
if (Test-Path $env:REPO_DIR) {
61+
Remove-Item -LiteralPath $env:REPO_DIR -Recurse -Force
62+
}
63+
New-Item -ItemType Directory -Path (Split-Path -Parent $env:REPO_DIR) -Force | Out-Null
64+
robocopy $env:GITHUB_WORKSPACE $env:REPO_DIR /MIR /NFL /NDL /NJH /NJS /NP
65+
$robocopyExitCode = $LASTEXITCODE
66+
if ($robocopyExitCode -gt 7) {
67+
throw "robocopy failed with exit code $robocopyExitCode"
68+
}
69+
Write-Host "robocopy completed with exit code $robocopyExitCode"
70+
exit 0
71+
72+
- name: Build, verify reproducibility, and refresh tracked drivers
73+
id: sync
74+
shell: pwsh
75+
working-directory: ${{ env.REPO_DIR }}
76+
run: |
77+
git config --global --add safe.directory $env:REPO_DIR
78+
79+
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
80+
$msbuild = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1
81+
if (-not $msbuild) {
82+
throw 'MSBuild.exe not found'
83+
}
84+
85+
$env:CL = '/Brepro'
86+
$env:LINK = '/Brepro'
87+
$artifactDir = Join-Path $env:RUNNER_TEMP 'winredirect-driver-sync'
88+
$failureDir = Join-Path $artifactDir 'failure'
89+
Remove-Item -LiteralPath $artifactDir -Recurse -Force -ErrorAction SilentlyContinue
90+
New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null
91+
92+
function Invoke-DriverBuild([string] $platform) {
93+
Remove-Item -LiteralPath "internal/winredirect/driver/build/$platform" -Recurse -Force -ErrorAction SilentlyContinue
94+
Remove-Item -LiteralPath "internal/winredirect/driver/intermediate/$platform" -Recurse -Force -ErrorAction SilentlyContinue
95+
& $msbuild 'internal/winredirect/driver/winredirect.vcxproj' '/t:Rebuild' '/p:Configuration=Release' "/p:Platform=$platform" '/p:SpectreMitigation=false' '/v:minimal'
96+
if ($LASTEXITCODE -ne 0) {
97+
throw "MSBuild failed for $platform with exit code $LASTEXITCODE"
98+
}
99+
}
100+
101+
$targets = @(
102+
@{ platform = 'x64'; repo = 'internal/winredirect/amd64/winredirect.sys' },
103+
@{ platform = 'ARM64'; repo = 'internal/winredirect/arm64/winredirect.sys' }
104+
)
105+
106+
Write-Host 'ARM and x86 are intentionally skipped here: WDK 10.0.26100 on GitHub-hosted CI does not support them.'
107+
108+
$changed = $false
109+
foreach ($target in $targets) {
110+
$platform = $target.platform
111+
$tracked = $target.repo
112+
$buildOutput = "internal/winredirect/driver/build/$platform/Release/winredirect.sys"
113+
$run1 = Join-Path $artifactDir "$platform-run1.sys"
114+
$run2 = Join-Path $artifactDir "$platform-run2.sys"
115+
116+
Invoke-DriverBuild $platform
117+
Copy-Item -LiteralPath $buildOutput -Destination $run1 -Force
118+
119+
Invoke-DriverBuild $platform
120+
Copy-Item -LiteralPath $buildOutput -Destination $run2 -Force
121+
122+
& python '.github/scripts/compare_signed_pe.py' $run1 $run2
123+
switch ($LASTEXITCODE) {
124+
0 {
125+
Write-Host "$platform reproduced after stripping Authenticode data."
126+
}
127+
1 {
128+
New-Item -ItemType Directory -Path $failureDir -Force | Out-Null
129+
Copy-Item -LiteralPath $run1 -Destination (Join-Path $failureDir "$platform-run1.sys") -Force
130+
Copy-Item -LiteralPath $run2 -Destination (Join-Path $failureDir "$platform-run2.sys") -Force
131+
throw "$platform build is not reproducible beyond signing metadata."
132+
}
133+
default {
134+
throw "reproducibility comparison failed for $platform with exit code $LASTEXITCODE"
135+
}
136+
}
137+
138+
& python '.github/scripts/compare_signed_pe.py' $run2 $tracked
139+
switch ($LASTEXITCODE) {
140+
0 {
141+
Write-Host "$platform matches the tracked driver after stripping Authenticode data."
142+
}
143+
1 {
144+
Write-Host "$platform differs beyond signing metadata; replacing tracked driver."
145+
Copy-Item -LiteralPath $run2 -Destination $tracked -Force
146+
git add -- $tracked
147+
$changed = $true
148+
}
149+
default {
150+
throw "comparison failed for $platform with exit code $LASTEXITCODE"
151+
}
152+
}
153+
}
154+
155+
if ($changed) {
156+
"changed=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
157+
} else {
158+
"changed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
159+
}
160+
161+
- name: Upload failed reproducibility artifacts
162+
if: failure()
163+
uses: actions/upload-artifact@v4
164+
with:
165+
name: winredirect-repro-failure-${{ github.run_id }}-${{ github.run_attempt }}
166+
path: ${{ runner.temp }}\winredirect-driver-sync\failure\*.sys
167+
if-no-files-found: warn
168+
169+
- name: Commit and push refreshed drivers
170+
if: steps.sync.outputs.changed == 'true'
171+
shell: pwsh
172+
working-directory: ${{ env.REPO_DIR }}
173+
run: |
174+
git config user.name 'github-actions[bot]'
175+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
176+
177+
git diff --cached --quiet
178+
if ($LASTEXITCODE -eq 0) {
179+
exit 0
180+
}
181+
182+
git commit -m 'winredirect: update bundled drivers'
183+
git push origin "HEAD:${{ github.event.pull_request.head.ref }}"
-8 Bytes
Binary file not shown.
-4.57 KB
Binary file not shown.

internal/winredirect/driver/winredirect.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,9 @@ void EvtIoDeviceControl(
439439
if (NT_SUCCESS(status)) {
440440
WINREDIRECT_CONFIG* config = (WINREDIRECT_CONFIG*)inBuf;
441441
NET_LUID tunLuid = {0};
442+
const GUID nullGuid = {0};
442443
KIRQL oldIrql;
443-
if (config->RedirectPort == 0 || config->ProxyPID == 0 || InlineIsEqualGUID(&GUID_NULL, &config->TunGuid)) {
444+
if (config->RedirectPort == 0 || config->ProxyPID == 0 || InlineIsEqualGUID(&nullGuid, &config->TunGuid)) {
444445
status = STATUS_INVALID_PARAMETER;
445446
} else {
446447
status = ConvertInterfaceGuidToLuid(&config->TunGuid, &tunLuid);

internal/winredirect/driver/winredirect.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
</ClCompile>
8080
<Link>
8181
<AdditionalDependencies>Fwpkclnt.lib;$(DDK_LIB_PATH)netio.lib;$(DDK_LIB_PATH)wdmsec.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies)</AdditionalDependencies>
82+
<GenerateDebugInformation>false</GenerateDebugInformation>
8283
</Link>
8384
<Inf>
8485
<Inf2CatUseLocalTime>true</Inf2CatUseLocalTime>

0 commit comments

Comments
 (0)