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<TransportPacketList> once injection is complete.

https://github.com/safing/portmaster-shadow/issues/38
This commit is contained in:
Alexandr Stelnykovych
2026-03-02 18:42:31 +02:00
parent def9d3407e
commit 674ff3f4dc
+50 -22
View File
@@ -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<TransportPacketList>
// 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<TransportPacketList>` 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);
}