From 9e8acb2d258814d6e45f15d6016888aede67ce7c Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Mon, 12 Jan 2026 17:01:27 +0200 Subject: [PATCH 1/9] chore(windows_kext): reduce compiler warnings for cleaner build output --- windows_kext/wdk/src/ffi.rs | 2 ++ windows_kext/wdk/src/irp_helpers.rs | 6 +++--- windows_kext/wdk/src/rw_spin_lock.rs | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/windows_kext/wdk/src/ffi.rs b/windows_kext/wdk/src/ffi.rs index c250499e0..282885d76 100644 --- a/windows_kext/wdk/src/ffi.rs +++ b/windows_kext/wdk/src/ffi.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + use core::ffi::c_void; use windows_sys::{ diff --git a/windows_kext/wdk/src/irp_helpers.rs b/windows_kext/wdk/src/irp_helpers.rs index 821c3b135..e15652d84 100644 --- a/windows_kext/wdk/src/irp_helpers.rs +++ b/windows_kext/wdk/src/irp_helpers.rs @@ -16,7 +16,7 @@ pub struct ReadRequest<'a> { } impl ReadRequest<'_> { - pub fn new(irp: &mut IRP) -> ReadRequest { + pub fn new(irp: &mut IRP) -> ReadRequest<'_> { unsafe { let irp_sp = irp.Tail.Overlay.Anonymous2.Anonymous.CurrentStackLocation; let device_io = (*irp_sp).Parameters.Read; @@ -79,7 +79,7 @@ pub struct WriteRequest<'a> { } impl WriteRequest<'_> { - pub fn new(irp: &mut IRP) -> WriteRequest { + pub fn new(irp: &mut IRP) -> WriteRequest<'_> { unsafe { let irp_sp = irp.Tail.Overlay.Anonymous2.Anonymous.CurrentStackLocation; let device_io = (*irp_sp).Parameters.Write; @@ -131,7 +131,7 @@ struct DeviceIOControlParams { } impl DeviceControlRequest<'_> { - pub fn new(irp: &mut IRP) -> DeviceControlRequest { + pub fn new(irp: &mut IRP) -> DeviceControlRequest<'_> { unsafe { let irp_sp = irp.Tail.Overlay.Anonymous2.Anonymous.CurrentStackLocation; // Use the struct directly when replaced with proper version. diff --git a/windows_kext/wdk/src/rw_spin_lock.rs b/windows_kext/wdk/src/rw_spin_lock.rs index 625b81320..0548c3618 100644 --- a/windows_kext/wdk/src/rw_spin_lock.rs +++ b/windows_kext/wdk/src/rw_spin_lock.rs @@ -26,7 +26,7 @@ impl RwSpinLock { /// /// This method blocks until a read lock can be acquired. /// Returns a `RwLockGuard` that represents the acquired read lock. - pub fn read_lock(&self) -> RwLockGuard { + pub fn read_lock(&self) -> RwLockGuard<'_> { let irq = unsafe { ExAcquireSpinLockShared(self.data.get()) }; RwLockGuard { data: &self.data, @@ -39,7 +39,7 @@ impl RwSpinLock { /// /// This method blocks until a write lock can be acquired. /// Returns a `RwLockGuard` that represents the acquired write lock. - pub fn write_lock(&self) -> RwLockGuard { + pub fn write_lock(&self) -> RwLockGuard<'_> { let irq = unsafe { ExAcquireSpinLockExclusive(self.data.get()) }; RwLockGuard { data: &self.data, From def9d3407ebeb69bdb6e4815a4ba77e903d20fe6 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Mon, 2 Mar 2026 17:08:47 +0200 Subject: [PATCH 2/9] feat(tests): add documentation and build script for Windows kernel driver testing --- windows_kext/test/BUILD_DEBUG.md | 215 +++++++++++++++++++++++++++++++ windows_kext/test/README.md | 13 ++ windows_kext/test/build_test.ps1 | 145 +++++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 windows_kext/test/BUILD_DEBUG.md create mode 100644 windows_kext/test/README.md create mode 100644 windows_kext/test/build_test.ps1 diff --git a/windows_kext/test/BUILD_DEBUG.md b/windows_kext/test/BUILD_DEBUG.md new file mode 100644 index 000000000..23f53103e --- /dev/null +++ b/windows_kext/test/BUILD_DEBUG.md @@ -0,0 +1,215 @@ +# Building and Running Driver with Debug Logging + +## Driver Signing Requirement + +Windows requires **all kernel drivers to be signed**. Test signing provides a free alternative to expensive production code signing certificates for development and testing purposes. + +## Important: Debug Builds Are Disabled + +⚠️ **The driver cannot be compiled in debug mode.** The code contains a compile-time check (`compile_error!`) that prevents debug builds due to potential optimization-related issues and inconsistent compiler behavior between debug and release modes. + +However, you can still enable verbose logging in release builds by changing the log level. + +## Prerequisites + +Already documented in [main README](../README.md), but quick recap: + +1. **Visual Studio 2022** with C++ and Windows SDK +2. **Windows Driver Kit (WDK)** installed +3. **Rust toolchain** installed +4. **Test signing enabled** (see below) + +## Step 1: Enable Test Signing (One-time Setup) + +⚠️ **SECURITY WARNING**: Test signing reduces system security by allowing any locally-generated test certificate to load kernel drivers. **Strongly recommended to use a VM or dedicated test machine**. See "Disabling Test Signing" section below to restore security when done testing. + +### Create Test Certificate + +Open **PowerShell as Administrator**: + +```powershell +# Create a self-signed certificate for driver testing +MakeCert -r -pe -ss PrivateCertStore -n "CN=DriverTestCert" DriverTestCert.cer + +# Install the certificate to Trusted Root +CertMgr /add DriverTestCert.cer /s /r localMachine root + +# Install to Trusted Publishers (needed for driver installation) +CertMgr /add DriverTestCert.cer /s /r localMachine trustedpublisher +``` + +### Enable Test Signing Mode + +```powershell +# Enable test signing +Bcdedit.exe -set TESTSIGNING ON + +# Restart required! +Restart-Computer +``` + +After restart, you should see **"Test Mode"** watermark in the corner of your screen. + +### Verify Test Signing is Enabled + +```powershell +bcdedit /enum | Select-String testsigning +# Should show: testsigning Yes +``` + +## Step 2: Enable Debug Logging in Driver + +To see verbose logs from the driver, edit the log level before building. + +**Edit `driver/src/logger.rs`:** + +```rust +// Change line 8 from: +pub const LOG_LEVEL: u8 = Severity::Warning as u8; + +// To one of: +pub const LOG_LEVEL: u8 = Severity::Debug as u8; // Recommended for testing +// pub const LOG_LEVEL: u8 = Severity::Info as u8; // Less verbose +// pub const LOG_LEVEL: u8 = Severity::Trace as u8; // Most verbose +``` + +For testing, `Debug` level is recommended. + +## Step 3: Build Driver in Release Mode + +Navigate to the driver directory: + +```powershell +cd D:\Projects\Portmaster\portmaster\windows_kext\driver + +# Build in release mode (only mode supported) +cargo build --release --target x86_64-pc-windows-msvc + +# Output: driver/target/x86_64-pc-windows-msvc/release/driver.lib +``` + +**Note:** Debug builds (`cargo build` without `--release`) will fail with a compile error by design. + +## Step 4: Link the Driver + +Copy the `.lib` file to the root directory: + +```powershell +cd D:\Projects\Portmaster\portmaster\windows_kext + +Copy-Item driver/target/x86_64-pc-windows-msvc/release/driver.lib . -Force +``` + +Run the linker script: + +```powershell +.\link-dev.ps1 +``` + +This creates `driver.sys` in the current directory. + +## Step 5: Sign the Driver + +## Step 5: Sign the Driver + +```powershell +cd D:\Projects\Portmaster\portmaster\windows_kext + +# Sign the driver +SignTool sign /v /s PrivateCertStore /n DriverTestCert driver.sys +``` + +Verify signature: + +```powershell +SignTool verify /v /pa driver.sys +``` + +You should see: **"Successfully verified: driver.sys"** + +## Step 6: View Driver Logs + +### Ring Buffer Logs (Recommended) + +These logs come through the `GetLogs` command. + +### Kernel Debugger Output (Not Available in Release) + +The `wdk::dbg!()`, `wdk::info!()`, and `wdk::err!()` macros only work in debug builds, which are disabled for this driver. These would output to tools like DebugView via `DbgPrint`, but since debug builds are not allowed, this logging path is not available. + +**Use the ring buffer logs** (captured by `dbg!`, `info!`, `warn!`, `err!` macros) for all debugging. + +## Common Issues + +### "The hash for the file is not present in the specified catalog file" + +**Solution**: Your driver isn't signed or the certificate isn't trusted. +```powershell +# Re-sign the driver +SignTool sign /v /s PrivateCertStore /n DriverTestCert driver.sys +``` + +### "Windows cannot verify the digital signature" + +**Solution**: Test signing not enabled or certificate not in Trusted Root. +```powershell +# Check test signing +bcdedit /enum | Select-String testsigning + +# Reinstall certificate if needed +CertMgr /add DriverTestCert.cer /s /r localMachine root +``` + +### "Service marked for deletion" + +**Solution**: Manually clean up: +```powershell +sc stop PortmasterKext +sc delete PortmasterKext +# Wait a few seconds +# Then try starting again +``` + +### "Access is denied" when creating service + +**Solution**: Run as Administrator. + +### No debug output (`GetLogs` command) + +**Solution**: +1. Make sure you edited `driver/src/logger.rs` to set `LOG_LEVEL = Severity::Debug` +2. Rebuild the driver in **release mode** (`cargo build --release`) +3. The driver must be actively running and processing connections to generate logs +4. Default log level (`Warning`) only shows errors, not normal operations + +## Quick Build & Test Cycle + +```powershell +# 1. (Optional) Enable debug logging - edit driver/src/logger.rs first + +# 2. Build driver in release mode +cd D:\Projects\Portmaster\portmaster\windows_kext\driver +cargo build --release + +# 3. Link and sign +cd .. +Copy-Item driver/target/x86_64-pc-windows-msvc/release/driver.lib . -Force +.\link-dev.ps1 +SignTool sign /v /s PrivateCertStore /n DriverTestCert driver.sys + +# 4. Test (in playground, as Administrator) +``` + +## Disabling Test Signing (When Done Testing) + +⚠️ **IMPORTANT**: When finished testing, disable test signing to restore system security. + +```powershell +# Run as Administrator +Bcdedit.exe -set TESTSIGNING OFF + +# Restart required for changes to take effect +Restart-Computer +``` + +After restart, the "Test Mode" watermark will disappear and the system will no longer accept test-signed drivers. This restores normal kernel driver security enforcement.Production vs Test Signing \ No newline at end of file diff --git a/windows_kext/test/README.md b/windows_kext/test/README.md new file mode 100644 index 000000000..e8ad85c6e --- /dev/null +++ b/windows_kext/test/README.md @@ -0,0 +1,13 @@ +# Test Directory + +> ⚠️ **Notice**: This folder and its contents were primarily generated with the assistance of AI and may contain errors or inaccuracies. They are intended solely for local testing and development and must not be used in production. + +## Contents + +- `build_test.ps1` - Script to build the test-signed driver +- `_out/` - Output directory for built test driver +- `_testcert/` - Test certificates for driver signing + +## Purpose + +This directory contains tools and utilities for testing the Portmaster Windows kernel driver during development. These are developer tools only and are not part of the production build or release process. diff --git a/windows_kext/test/build_test.ps1 b/windows_kext/test/build_test.ps1 new file mode 100644 index 000000000..1be104753 --- /dev/null +++ b/windows_kext/test/build_test.ps1 @@ -0,0 +1,145 @@ +# Build and Sign Test Driver Script +# Must be run from Developer PowerShell for Visual Studio + +$ErrorActionPreference = "Stop" + +# Get script directory and set paths +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$rootDir = Split-Path -Parent $scriptDir +$driverDir = Join-Path $rootDir "driver" +$certPath = Join-Path $rootDir "test\_testcert\DriverTestCert.cer" +$outDir = Join-Path $scriptDir "_out" + +# Create output directory if it doesn't exist +if (-not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +Write-Host "=================================================" -ForegroundColor Cyan +Write-Host " Building and Signing Test Driver" -ForegroundColor Cyan +Write-Host "=================================================" -ForegroundColor Cyan +Write-Host "" + +# Verify we are in the correct directory +if (-not (Test-Path $driverDir)) { + Write-Host "ERROR: Driver directory not found at: $driverDir" -ForegroundColor Red + Write-Host "Please run this script from the windows_kext root directory or ensure paths are correct." -ForegroundColor Red + exit 1 +} + +# Verify certificate exists +if (-not (Test-Path $certPath)) { + Write-Host "ERROR: Certificate not found at: $certPath" -ForegroundColor Red + Write-Host "Please create the test certificate first." -ForegroundColor Red + exit 1 +} + +# +# Step 1: Build Driver in Release Mode +# +Write-Host "[1/3] Building driver in release mode..." -ForegroundColor Yellow +Push-Location $driverDir +try { + cargo build --release --target x86_64-pc-windows-msvc + + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Cargo build failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit $LASTEXITCODE + } + + Write-Host " Driver built successfully" -ForegroundColor Green +} finally { + Pop-Location +} + +# +# Step 2: Link the Driver +# +Write-Host "[2/3] Linking driver..." -ForegroundColor Yellow +Push-Location $outDir +try { + # Copy the .lib file to output directory + $libSource = Join-Path $driverDir "target\x86_64-pc-windows-msvc\release\driver.lib" + $libDest = Join-Path $outDir "driver.lib" + + if (-not (Test-Path $libSource)) { + Write-Host "ERROR: Built driver.lib not found at: $libSource" -ForegroundColor Red + exit 1 + } + + Copy-Item $libSource $libDest -Force + Write-Host " Copied driver.lib" -ForegroundColor Green + + # Run linker script (from output directory so files are created here) + $linkScript = Join-Path $rootDir "link-dev.ps1" + if (-not (Test-Path $linkScript)) { + Write-Host "ERROR: link-dev.ps1 not found at: $linkScript" -ForegroundColor Red + exit 1 + } + + & $linkScript + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Linking failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit $LASTEXITCODE + } + + # Rename driver.sys to test name + $sysFile = Join-Path $outDir "driver.sys" + if (-not (Test-Path $sysFile)) { + Write-Host "ERROR: driver.sys was not created" -ForegroundColor Red + exit 1 + } + + $testSysFile = Join-Path $outDir "PortmasterKext_test.sys" + Move-Item $sysFile $testSysFile -Force + + Write-Host " Driver linked successfully (PortmasterKext_test.sys)" -ForegroundColor Green +} finally { + Pop-Location +} + +# +# Step 3: Sign the Driver +# +Write-Host "[3/3] Signing driver..." -ForegroundColor Yellow +Push-Location $outDir +try { + $sysFile = Join-Path $outDir "PortmasterKext_test.sys" + + # Sign the driver + SignTool sign /v /fd SHA256 /s PrivateCertStore /n DriverTestCert $sysFile + + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Signing failed with exit code $LASTEXITCODE" -ForegroundColor Red + Write-Host "Make sure the certificate is installed in PrivateCertStore" -ForegroundColor Yellow + exit $LASTEXITCODE + } + + Write-Host " Driver signed successfully" -ForegroundColor Green + + # Verify signature + Write-Host "" + Write-Host "Verifying signature..." -ForegroundColor Yellow + SignTool verify /v /pa $sysFile + + if ($LASTEXITCODE -ne 0) { + Write-Host "WARNING: Signature verification failed" -ForegroundColor Yellow + } else { + Write-Host " Signature verified" -ForegroundColor Green + } +} finally { + Pop-Location +} + +Write-Host "" +Write-Host "=================================================" -ForegroundColor Cyan +Write-Host " Build Complete!" -ForegroundColor Green +Write-Host "=================================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Output directory: $outDir" -ForegroundColor White +Write-Host "Driver file: PortmasterKext_test.sys" -ForegroundColor White +Write-Host "" +Write-Host "Next steps:" -ForegroundColor Yellow +Write-Host " 1. Run playground as Administrator" -ForegroundColor White +Write-Host " 2. Use start command to load the driver" -ForegroundColor White +Write-Host "" From 674ff3f4dc9fcedb73ea1b1a12e9629de581be74 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Mon, 2 Mar 2026 18:42:31 +0200 Subject: [PATCH 3/9] fix(kext): potentially fix BSOD by heap-allocating WFP transport send params FwpsInjectTransportSendAsync1 dereferences FWPS_TRANSPORT_SEND_PARAMS1 (and remote_address within it) asynchronously after the callsite returns. The params were stack-allocated, so WFP may have accessed freed stack memory at DISPATCH_LEVEL, potentially causing PAGE_FAULT_IN_NONPAGED_AREA or DRIVER_IRQL_NOT_LESS_OR_EQUAL BSODs. Candidate fix: embed send_params in TransportPacketList, box the whole struct before calling the inject API, and populate send_params (remote_address points into boxed remote_ip) only after boxing so all pointers are into stable non-paged heap memory. Introduce free_transport_packet as the WFP completion callback that drops Box once injection is complete. https://github.com/safing/portmaster-shadow/issues/38 --- windows_kext/wdk/src/filter_engine/packet.rs | 72 ++++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/windows_kext/wdk/src/filter_engine/packet.rs b/windows_kext/wdk/src/filter_engine/packet.rs index afdcb0217..7f74c4ed2 100644 --- a/windows_kext/wdk/src/filter_engine/packet.rs +++ b/windows_kext/wdk/src/filter_engine/packet.rs @@ -32,6 +32,11 @@ pub struct TransportPacketList { inbound: bool, interface_index: u32, sub_interface_index: u32, + // send_params and remote_ip must outlive inject_packet_list_transport + // because FwpsInjectTransportSendAsync1 may read them after the function returns. + // Storing send_params here ensures it lives on the heap inside Box + // until the WFP completion callback (free_transport_packet) drops it. + send_params: FWPS_TRANSPORT_SEND_PARAMS1, } pub struct InjectInfo { @@ -100,7 +105,7 @@ impl Injector { sub_interface_index: u32, ) -> TransportPacketList { let mut control_data = None; - if let Some(cd) = callout_data.get_control_data() { + if let Some(cd) = callout_data.get_control_data() { control_data = Some(cd); } let mut remote_ip: [u8; 16] = [0; 16]; @@ -122,6 +127,8 @@ impl Injector { inbound, interface_index, sub_interface_index, + // Populated with valid pointers in inject_packet_list_transport after boxing. + send_params: unsafe { MaybeUninit::zeroed().assume_init() }, } } @@ -133,34 +140,37 @@ impl Injector { if self.transport_inject_handle == INVALID_HANDLE_VALUE { return Err("failed to inject packet: invalid handle value".to_string()); } + // Box the entire packet_list so that remote_ip and send_params + // are heap-allocated. Their addresses remain stable until free_transport_packet + // drops the Box after WFP calls the completion callback. + let mut boxed = Box::new(packet_list); + let raw_nbl = boxed.net_buffer_list.nbl; + unsafe { + // Populate send_params with pointers into the boxed struct. + // These addresses are stable because the Box will not move until freed. let mut control_data_length = 0; - let control_data = match &packet_list.control_data { + let control_data: *mut c_void = match &boxed.control_data { Some(cd) => { control_data_length = cd.len(); - cd.as_ptr().cast() + cd.as_ptr() as *mut c_void } None => core::ptr::null_mut(), }; - - let mut send_params = FWPS_TRANSPORT_SEND_PARAMS1 { - remote_address: &packet_list.remote_ip as _, - remote_scope_id: packet_list.remote_scope_id, - control_data: control_data as _, + boxed.send_params = FWPS_TRANSPORT_SEND_PARAMS1 { + remote_address: boxed.remote_ip.as_ptr(), + remote_scope_id: boxed.remote_scope_id, + control_data, control_data_length: control_data_length as u32, header_include_header: core::ptr::null_mut(), header_include_header_length: 0, }; - let address_family = if packet_list.ipv6 { AF_INET6 } else { AF_INET }; - let net_buffer_list = packet_list.net_buffer_list; - // Escape the stack. Packet buffer should be valid until the packet is injected. - let boxed_nbl = Box::new(net_buffer_list); - let raw_nbl = boxed_nbl.nbl; - let raw_ptr = Box::into_raw(boxed_nbl); + let address_family = if boxed.ipv6 { AF_INET6 } else { AF_INET }; + let raw_ptr = Box::into_raw(boxed); - // Inject - let status = if packet_list.inbound { + // Inject. Context is *mut TransportPacketList; freed by free_transport_packet. + let status = if (*raw_ptr).inbound { FwpsInjectTransportReceiveAsync0( self.transport_inject_handle, core::ptr::null_mut(), @@ -168,23 +178,23 @@ impl Injector { 0, address_family, UNSPECIFIED_COMPARTMENT_ID, - packet_list.interface_index, - packet_list.sub_interface_index, + (*raw_ptr).interface_index, + (*raw_ptr).sub_interface_index, raw_nbl, - free_packet, + free_transport_packet, raw_ptr as _, ) } else { FwpsInjectTransportSendAsync1( self.transport_inject_handle, core::ptr::null_mut(), - packet_list.endpoint_handle, + (*raw_ptr).endpoint_handle, 0, - &mut send_params, + &mut (*raw_ptr).send_params, address_family, UNSPECIFIED_COMPARTMENT_ID, raw_nbl, - free_packet, + free_transport_packet, raw_ptr as _, ) }; @@ -344,3 +354,21 @@ unsafe extern "C" fn free_packet( } _ = Box::from_raw(context as *mut NetBufferList); } + +/// Completion callback for transport inject paths (both inbound and outbound). +/// The context is a `Box` cast to `*mut c_void`. +/// Dropping it also correctly drops the inner `NetBufferList`. +unsafe extern "C" fn free_transport_packet( + context: *mut c_void, + net_buffer_list: *mut NET_BUFFER_LIST, + _dispatch_level: bool, +) { + if let Some(nbl) = net_buffer_list.as_ref() { + if let Err(err) = check_ntstatus(nbl.Status) { + crate::err!("inject status: {}", err); + } else { + crate::dbg!("inject status: Ok"); + } + } + _ = Box::from_raw(context as *mut TransportPacketList); +} From 2d6fd0ad09b08c0fc6352d998153c9b06573e0da Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Mon, 2 Mar 2026 19:06:26 +0200 Subject: [PATCH 4/9] fix(kext): potentially fix BSOD by copying WFP control data before callout returns FwpsInjectTransportSendAsync1 reads control_data (WSACMSGHDR) asynchronously after the ALE classify callout returns. The pointer was into WFP-managed metadata memory that WFP frees immediately when the callout returns, leaving a dangling pointer used during deferred injection. This could cause PAGE_FAULT_IN_NONPAGED_AREA or DRIVER_IRQL_NOT_LESS_OR_EQUAL BSODs. Candidate fix: change TransportPacketList.control_data from Option> to Option> and copy the bytes inside from_ale_callout while the WFP pointer is still valid. The owned copy lives on the heap as part of the boxed TransportPacketList context and is freed by free_transport_packet after WFP calls the completion callback. https://github.com/safing/portmaster-shadow/issues/38 --- windows_kext/wdk/src/filter_engine/packet.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/windows_kext/wdk/src/filter_engine/packet.rs b/windows_kext/wdk/src/filter_engine/packet.rs index 7f74c4ed2..29a604037 100644 --- a/windows_kext/wdk/src/filter_engine/packet.rs +++ b/windows_kext/wdk/src/filter_engine/packet.rs @@ -2,7 +2,7 @@ use alloc::{ boxed::Box, string::{String, ToString}, }; -use core::{ffi::c_void, mem::MaybeUninit, ptr::NonNull}; +use core::{ffi::c_void, mem::MaybeUninit}; use windows_sys::Win32::{ Foundation::{HANDLE, INVALID_HANDLE_VALUE}, Networking::WinSock::{AF_INET, AF_INET6, AF_UNSPEC, SCOPE_ID}, @@ -28,7 +28,9 @@ pub struct TransportPacketList { remote_ip: [u8; 16], endpoint_handle: u64, remote_scope_id: SCOPE_ID, - control_data: Option>, + // Owned copy of the WFP control data. The original WFP pointer is only + // valid during the ALE classify callback; the bytes are copied here so they outlive it. + control_data: Option>, inbound: bool, interface_index: u32, sub_interface_index: u32, @@ -104,9 +106,10 @@ impl Injector { interface_index: u32, sub_interface_index: u32, ) -> TransportPacketList { - let mut control_data = None; - if let Some(cd) = callout_data.get_control_data() { - control_data = Some(cd); + let mut control_data: Option> = None; + if let Some(cd) = callout_data.get_control_data() { + // Copy the bytes while the WFP pointer is still valid (we are inside the callout). + control_data = Some(unsafe { cd.as_ref() }.to_vec().into_boxed_slice()); } let mut remote_ip: [u8; 16] = [0; 16]; if ipv6 { From 7b4d3a93c328cc2e7a9715b0e59d102952128fa5 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Mon, 2 Mar 2026 19:21:30 +0200 Subject: [PATCH 5/9] fix(kext): remove unused spin_lock module to clean up codebase --- windows_kext/wdk/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/windows_kext/wdk/src/lib.rs b/windows_kext/wdk/src/lib.rs index ba1daf76a..66b200969 100644 --- a/windows_kext/wdk/src/lib.rs +++ b/windows_kext/wdk/src/lib.rs @@ -13,7 +13,6 @@ pub mod interface; pub mod ioqueue; pub mod irp_helpers; pub mod rw_spin_lock; -pub mod spin_lock; pub mod utils; #[allow(dead_code)] From 489b9c0442caa0194ddb4d23844e76564337aede Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 3 Mar 2026 13:33:04 +0200 Subject: [PATCH 6/9] fix(kext): replace global device pointer with AtomicPtr for thread safety --- windows_kext/driver/src/entry.rs | 50 ++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/windows_kext/driver/src/entry.rs b/windows_kext/driver/src/entry.rs index 513c004b8..9b1ab0039 100644 --- a/windows_kext/driver/src/entry.rs +++ b/windows_kext/driver/src/entry.rs @@ -1,6 +1,7 @@ use crate::common::ControlCode; use crate::device; use alloc::boxed::Box; +use core::sync::atomic::{AtomicPtr, Ordering}; use num_traits::FromPrimitive; use wdk::irp_helpers::{DeviceControlRequest, ReadRequest, WriteRequest}; use wdk::{err, info, interface}; @@ -9,9 +10,18 @@ use windows_sys::Win32::Foundation::{NTSTATUS, STATUS_SUCCESS}; static VERSION: [u8; 4] = include!("../../kextinterface/version.txt"); -static mut DEVICE: *mut device::Device = core::ptr::null_mut(); +/// Global device pointer. +/// +/// We use `AtomicPtr` to ensure thread safety. +/// - **Safety**: Prevents data races and acts as a compiler barrier against dangerous optimizations +/// (e.g., load hoisting), ensuring concurrent callouts see a valid, up-to-date pointer. +/// - **Performance**: Negligible overhead. On x64, `Acquire` is free (same as a normal load). +/// On ARM64, it uses efficient hardware-supported load-acquire instructions. +static DEVICE: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + pub fn get_device() -> Option<&'static mut device::Device> { - return unsafe { DEVICE.as_mut() }; + // Acquire pairs with the Release store in driver_entry and the AcqRel swap in driver_unload. + unsafe { DEVICE.load(Ordering::Acquire).as_mut() } } // DriverEntry is the entry point of the driver (main function). Will be called when driver is loaded. @@ -44,16 +54,16 @@ pub extern "system" fn driver_entry( driver.set_device_control_fn(Some(device_control)); // Initialize device. - unsafe { - let device = match device::Device::new(&driver) { - Ok(device) => Box::new(device), - Err(err) => { - wdk::err!("filed to initialize device: {}", err); - return -1; - } - }; - DEVICE = Box::into_raw(device); - } + let device = match device::Device::new(&driver) { + Ok(device) => Box::new(device), + Err(err) => { + wdk::err!("filed to initialize device: {}", err); + return -1; + } + }; + // Release: makes the fully-constructed Device visible to all cores that subsequently + // perform an Acquire load. + DEVICE.store(Box::into_raw(device), Ordering::Release); STATUS_SUCCESS } @@ -61,10 +71,18 @@ pub extern "system" fn driver_entry( // driver_unload function is called when service delete is called from user-space. unsafe extern "system" fn driver_unload(_object: *const DRIVER_OBJECT) { info!("Unloading complete"); - unsafe { - if !DEVICE.is_null() { - _ = Box::from_raw(DEVICE); - } + // Atomically null the pointer before freeing. Any core that performs an Acquire load + // *after* this swap will see null and bail out safely. Any core that already loaded a + // non-null pointer before this swap is protected by the OS-level serialisation: + // - WFP callouts: FilterEngine::drop() (field declared first in Device) calls the WFP + // unregister APIs which block until every in-flight classify callback has returned, + // so no callout thread holds a live reference by the time the memory is freed. + // - IRP dispatch (read/write/ioctl): the I/O Manager guarantees no dispatch routine + // is executing when driver_unload is called. + // The swap is executed exactly once, on the unload path. + let ptr = DEVICE.swap(core::ptr::null_mut(), Ordering::AcqRel); + if !ptr.is_null() { + unsafe { drop(Box::from_raw(ptr)); } } } From f586f23a3ae429d60698aa3824ff2a518850ed55 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 3 Mar 2026 13:39:20 +0200 Subject: [PATCH 7/9] fix(kext): change advance method to accept mutable reference for buffer modification --- windows_kext/wdk/src/filter_engine/net_buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows_kext/wdk/src/filter_engine/net_buffer.rs b/windows_kext/wdk/src/filter_engine/net_buffer.rs index 08f828d82..90363b77a 100644 --- a/windows_kext/wdk/src/filter_engine/net_buffer.rs +++ b/windows_kext/wdk/src/filter_engine/net_buffer.rs @@ -166,7 +166,7 @@ impl NetBufferList { } /// Advances the MDL of the buffer. - pub fn advance(&self, size: u32) { + pub fn advance(&mut self, size: u32) { unsafe { if let Some(nbl) = self.nbl.as_mut() { if let Some(nb) = nbl.Header.first_net_buffer.as_mut() { From 72c048cb07b8a506ac750317ab084426bd37da00 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 3 Mar 2026 18:07:55 +0200 Subject: [PATCH 8/9] fix(kext): ignore STATUS_FWP_TXN_IN_PROGRESS in reset_all_filters When completing a Reauthorization classify defer, reset_all_filters() would fail with STATUS_FWP_TXN_IN_PROGRESS if another WFP transaction was already running (e.g. from a concurrent ClearCache command). This caused the packet to be silently dropped instead of injected. This error is safe to ignore: the concurrent transaction will trigger WFP reauthorization for all connections anyway, and the verdict for the current connection is already written to the connection_cache before complete() is called, so the callout will apply the correct verdict when the injected packet passes through. All other errors from reset_all_filters() are still propagated. --- .../wdk/src/filter_engine/callout_data.rs | 16 +++++++++++++++- .../wdk/src/filter_engine/transaction.rs | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/windows_kext/wdk/src/filter_engine/callout_data.rs b/windows_kext/wdk/src/filter_engine/callout_data.rs index 6efaac1f8..4e17cf454 100644 --- a/windows_kext/wdk/src/filter_engine/callout_data.rs +++ b/windows_kext/wdk/src/filter_engine/callout_data.rs @@ -37,7 +37,21 @@ impl ClassifyDefer { } ClassifyDefer::Reauthorization(_callout_id, packet_list) => { // There is no way to reset single filter. If another request for filter reset is trigger at the same time it will fail. - filter_engine.reset_all_filters()?; + // + // Resetting all filters forces WFP to re-evaluate (reauthorize) all existing connections + // using the updated verdict cache. + // If STATUS_FWP_TXN_IN_PROGRESS is returned, another reset_all_filters() call is + // already running concurrently, which will trigger the same WFP reauthorization. + // It is safe to ignore this specific error and proceed with injecting the packet: + // the verdict for this connection is already in the connection_cache, so the callout + // will apply the correct verdict when the injected packet passes through. + match filter_engine.reset_all_filters() { + Ok(_) => {} + Err(err) if err.contains("STATUS_FWP_TXN_IN_PROGRESS") => { + // Another transaction is already in progress and will handle reauthorization. + } + Err(err) => return Err(err), + } return Ok(packet_list); } } diff --git a/windows_kext/wdk/src/filter_engine/transaction.rs b/windows_kext/wdk/src/filter_engine/transaction.rs index e80728b3e..162cbcf12 100644 --- a/windows_kext/wdk/src/filter_engine/transaction.rs +++ b/windows_kext/wdk/src/filter_engine/transaction.rs @@ -32,6 +32,7 @@ impl<'a> Transaction<'a> { } /// Creates a read/write guard for filter engine transaction. + /// Note! If another transaction is already in progress, it will return an error STATUS_FWP_TXN_IN_PROGRESS. pub(super) fn begin_write(filter_engine: &'a mut FilterEngine) -> Result { return Self::begin(filter_engine, 0); } From 35ecee89d5736a726f74c334f41ee1aa333a246f Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Tue, 3 Mar 2026 18:15:47 +0200 Subject: [PATCH 9/9] fix(kext): prevent block verdicts from being overridden by downstream WFP filters Rename action_block() to action_block_hard(), which additionally calls clear_write_flag(). This prevents lower-weight filters in the same WFP sublayer from overwriting a block action set by Portmaster's callout. Updated call sites: - ALE layer: PermanentBlock, Undeterminable, Failed, and inbound Block - Packet layer: PermanentBlock --- windows_kext/driver/src/ale_callouts.rs | 4 ++-- windows_kext/driver/src/packet_callouts.rs | 2 +- windows_kext/wdk/src/filter_engine/callout_data.rs | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/windows_kext/driver/src/ale_callouts.rs b/windows_kext/driver/src/ale_callouts.rs index 51c5cc303..857e7256d 100644 --- a/windows_kext/driver/src/ale_callouts.rs +++ b/windows_kext/driver/src/ale_callouts.rs @@ -190,7 +190,7 @@ fn ale_layer_auth(mut data: CalloutData, ale_data: AleLayerData) { Verdict::PermanentBlock | Verdict::Undeterminable | Verdict::Failed => { // Packet layer will not see this connection. crate::dbg!("permanent block {}", key); - data.action_block(); + data.action_block_hard(); } Verdict::PermanentDrop => { // Packet layer will not see this connection. @@ -203,7 +203,7 @@ fn ale_layer_auth(mut data: CalloutData, ale_data: AleLayerData) { data.action_permit(); } else { // packet layer will still see the packets. - data.action_block(); + data.action_block_hard(); } } Verdict::Drop => { diff --git a/windows_kext/driver/src/packet_callouts.rs b/windows_kext/driver/src/packet_callouts.rs index 1e8c28f17..a5652b038 100644 --- a/windows_kext/driver/src/packet_callouts.rs +++ b/windows_kext/driver/src/packet_callouts.rs @@ -180,7 +180,7 @@ fn ip_packet_layer( } Verdict::PermanentBlock => { send_request_to_portmaster = false; - data.action_block(); + data.action_block_hard(); } Verdict::Undeterminable | Verdict::PermanentDrop | Verdict::Failed => { send_request_to_portmaster = false; diff --git a/windows_kext/wdk/src/filter_engine/callout_data.rs b/windows_kext/wdk/src/filter_engine/callout_data.rs index 4e17cf454..60c18825f 100644 --- a/windows_kext/wdk/src/filter_engine/callout_data.rs +++ b/windows_kext/wdk/src/filter_engine/callout_data.rs @@ -186,10 +186,14 @@ impl<'a> CalloutData<'a> { } } - pub fn action_block(&mut self) { + // Block action and clear the write flag. + // This will block the packet and prevent next filter in the chain to change the action. + pub fn action_block_hard(&mut self) { unsafe { (*self.classify_out).action_block(); (*self.classify_out).clear_absorb_flag(); + // Next filter in the chain will not change the action. + (*self.classify_out).clear_write_flag(); } }