Repository navigation
Mock Driver - #15
Open
vlabo wants to merge 24 commits into
Open
Mock Driver#15vlabo wants to merge 24 commits into
vlabo wants to merge 24 commits into
Conversation
Make the device API shared-reference based. `get_device` now hands out `&'static Device` instead of `&'static mut`, so callout threads and the read/write paths can share one device soundly. Every field is interior-mutable: `FilterEngineCell` wraps the filter engine (it cannot live behind the IRQL-raising spin lock, since WFP transactions must run at PASSIVE_LEVEL), `IdCache::next_id` becomes an `AtomicU64` assigned under the values lock, and `clean_ended_connections` returns owned vectors instead of handing out `&mut` to reused buffers. Bounds-check command parsing. `parse_type` no longer panics on an empty buffer, and `as_type` rejects buffers shorter than the target struct instead of reading out of bounds; the parse functions now return `Option` and the device logs and drops truncated commands. Replace the `MaybeUninit::zeroed().assume_init()` log statics with const-initialized ones, drop the unneeded `unsafe` blocks, and fix the `crit!` macro, which called a stale two-argument `add_line`. Return `STATUS_FAILED_DRIVER_ENTRY` rather than -1 when device initialization fails. Add deterministic Rust <-> Go differential tests of the wire format: both sides derive field values from the record index with the same splitmix64-style hash, so any divergence in endianness, field order, type width, or struct size shows up as a mismatch. Wire the new tests and fixtures into the `Justfile` targets.
Make the device API shared-reference based. `get_device` now hands out `&'static Device` instead of `&'static mut`, so callout threads and the read/write paths can share one device soundly. Every field is interior-mutable: `FilterEngineCell` wraps the filter engine (it cannot live behind the IRQL-raising spin lock, since WFP transactions must run at PASSIVE_LEVEL), `IdCache::next_id` becomes an `AtomicU64` assigned under the values lock, and `clean_ended_connections` returns owned vectors instead of handing out `&mut` to reused buffers. Bounds-check command parsing. `parse_type` no longer panics on an empty buffer, and `as_type` rejects buffers shorter than the target struct instead of reading out of bounds; the parse functions now return `Option` and the device logs and drops truncated commands. Replace the `MaybeUninit::zeroed().assume_init()` log statics with const-initialized ones, drop the unneeded `unsafe` blocks, and fix the `crit!` macro, which called a stale two-argument `add_line`. Return `STATUS_FAILED_DRIVER_ENTRY` rather than -1 when device initialization fails. Add deterministic Rust <-> Go differential tests of the wire format: both sides derive field values from the record index with the same splitmix64-style hash, so any divergence in endianness, field order, type width, or struct size shows up as a mismatch. Wire the new tests and fixtures into the `Justfile` targets.
The `driver` crate was both the driver logic and the kernel image build root: it carried crate-type = "staticlib", the global allocator, the panic handler, the CRT stub and the exported `DriverEntry`. That glue only makes sense for a kernel build, and it blocks building the same logic as a plain library for host-side fuzz/simulation harnesses. Move it to a new `driver/kernel` crate, the only crate that produces the shipped driver.lib (built as driver_kernel.lib). It depends on `driver` with the `real_wdk` feature, so nothing in that graph can reach a mock wdk. Keep it free of logic so the code tested on the host is the code that ships. `driver` becomes a plain library whose `wdk` and `windows-sys` dependencies are optional behind the default `real_wdk` feature. `entry.rs` now names kernel types through the new `wdk::kernel_types` re-export instead of `windows_sys` directly, so a mock can supply API-compatible stand-ins under the same path. Update the Justfile, CI workflow and release packaging for the moved build root and the new lib name.
Make the callout entry points, device, caches and packet helpers public, add IdCache::pending_ids and Default impls for the public caches, and drop the real_wdk feature (the mock now lives in its own crate). No mock code and no behaviour change in the driver.
mock/driver compiles the unmodified driver/src against mock/wdk (a host mock of the Windows kernel API) through a Cargo dependency rename, so tests run the production code with only the Windows API swapped out. mock/harness loads it through the real driver_entry/DriverUnload and holds the fuzzing API, the in-process simulation and the property tests; mock/fuzz holds the cargo-fuzz targets. Nothing in driver/ or wdk/ refers to mock/.
zf-sim loads the real driver through mock/harness and exposes it on a named pipe (Unix socket on Linux/macOS); kext_interface gains the matching pipe transport (OpenPipe, MockKextService, SimConn) and kext_tester can spawn and drive it. Adds the sim-* Justfile recipes and the sim-linux CI job.
GetConnectionsUpdate was comparing last_accessed_timestamp against the request timestamp with the wrong sign, so it reported the connections that had *not* seen traffic since the last update. Invert the check and take the cursor from the driver's clock before the walk, sending it back in ConnectionUpdateEnd so user space can pass it to the next request without interpreting it. The cursor is inclusive, so a connection touched during the walk is reported again rather than missed. Ended connections are now skipped: their end event already carried the final counters. This gives ConnectionUpdateEnd a payload, so parseEmptyInfo goes away and the Go side parses it like any other event. The diff-test corpus gains a seventh record type to cover it. Also fix the payload layer for inbound ALE: it is cloned after retreating over the IP and transport headers, so the payload starts at the network layer, not the transport layer. Only outbound ALE hands over the packet from its transport header.
# Conflicts: # driver/src/device.rs # driver/src/entry.rs # driver/src/id_cache.rs # kext_interface/diff_test.go # protocol/src/difftest.rs
The zf-sim daemon hard-coded its scenarios in Rust: random traffic from a producer, authored runs as CBOR files emitted by a sample builder, and a Go agent that spawned the daemon to drive either. Adding a case meant a new Rust builder and a new fixture, and asserting on the driver's state from the agent side was not possible at all. Replace it with `zfsim`, a Python extension module (PyO3, abi3) that loads the same driver through mock/harness in-process. The Rust side is now only an interface -- callouts, the user-space channel, driver state -- and scenarios, traffic, seeds and assertions live in Python. A script plays the OS and the agent at once, or serves the driver on a named pipe (a Unix socket on Linux/macOS) to the real Go agent. sim/tests holds the scenario tests, sim/scripts/serve.py the slow readable traffic for a real agent, and sim/docker runs the Linux agent against it in a container. Build with plain cargo plus a copy (build.py), no maturin; the cdylib is just a renamed extension module. Drop the CBOR scenario format, the producer, the sample emitter and their sim-* recipes; `just sim-test` now runs the Python tests and CI calls it in place of the emit/replay pair. kext_tester only attaches: the script owns the driver's lifecycle, so MockKextService and the spawn path go away. Supporting fixes, needed for scripts to parse what the driver emits: the structured packet builders now produce well-formed packets (real TCP/UDP headers, IP and transport checksums, correct lengths) and hand ALE the data offset WFP would; Event::decode in mock/harness parses an event payload per its info_type, round-trip tested against the protocol serializers; and the mock clock returns system time in the kernel's 1601 epoch rather than the Unix one.
Package `zfsim` as a wheel so colleagues can `pip install` the driver-over-mock-kernel simulator instead of building it from source. - sim/docker/wheels.Dockerfile cross-builds the Windows, Linux and macOS x86_64 abi3 wheels on one Linux machine with maturin, zig and cargo-xwin, so a wheel build needs nothing but Docker. - `just sim-wheels [version]` runs that build into sim/dist/; the version defaults to the driver's (kext_interface/version.txt), which build.py now prints with --version. - sim/pyproject.toml gains the maturin build-system and project metadata; the dev extension built by build.py is excluded so it cannot ride along in a wheel for another platform. - pyo3 gets generate-import-lib, so the Windows wheel links against python3.dll without a Windows Python install. - .github/workflows/zfsim-wheels.yml builds the same container on every published release, installs the Windows and Linux wheels into a clean Python, runs the scenario tests against them, and attaches all three wheels to the release. Day-to-day development is unchanged: `just sim-build` still does a plain cargo build and copy, with no maturin involved.
Build the wheels on every PR push and publish them as the pre-release `pr-<number>`, versioned `<driver>.dev<run>` so each push installs as an upgrade of the last. A sticky PR comment carries the `pip install` command per OS; both the release and the comment are replaced on every push and removed when the PR closes. PRs from forks get no write token, so they keep only the run artifact. Runs for a PR are cancelled by the next push, while release runs always finish.
zfsim 2.0.2.dev2 (32affab)Install the build of this PR for your OS (any CPython 3.9+): macOS (Intel) Linux Windows Updated on every push; removed when the PR closes. |
zfsim is the mock driver with a Python face: it loads the real driver over mock/wdk through mock/harness and serves it to an agent. It lived in a workspace of its own next to mock/, which duplicated the lockfile (sim/Cargo.lock) and the build of the very crates it depends on. It is now mock/sim, a fourth member of the mock/ workspace alongside wdk, driver and harness, sharing mock/Cargo.lock and mock/target. build.py copies the cdylib out of mock/target, and pyo3 stays out of the driver build as before: driver/kernel depends on nothing under mock/. Paths updated throughout: Justfile sim-* recipes, .gitignore, go.work, the CI workflows (the Swatinem cache now keys on the mock workspace), the wheels Dockerfile and its build context, and the doc comments that pointed at sim/. rustdoc-gen moves mock/sim into RUSTDOC-mock.md, leaving RUSTDOC-tools.md to release/ alone; the stale mock/fuzz entry goes with it (those targets were removed earlier). Also drop protocol-test.yml. Its two jobs were a subset of what `just test` already runs in pr-checks.yml, and build.yml no longer triggers on every branch push: a push to a PR branch ran both the push and pull_request copies of every job.
Build on `release: published` as well, selecting the release profile for those runs (PRs and main keep building debug, like `just link`). A new release-only `publish` job zips the linked driver.sys with its PDB, names the zip after the tag and uploads it to the release. Also pin the workflow's default permissions to `contents: read`, with `contents: write` granted only to the publishing job, and skip the kext_interface compile check on release runs.
The driver only ever needed a device object to talk to user space, but it pulled in KMDF to get one: `pm_InitDriverObject` in `c_helper` wrapped WdfDriverCreate/WdfDeviceCreate, which bound the image to WdfLdr and the FxDriverEntry stub, and the context-space plumbing around it was unused (the device lives in a global). Create the device directly instead. `init_driver_object` now calls WdmlibIoCreateDeviceSecure with SDDL_DEVOBJ_SYS_ALL_ADM_ALL, so only the system and administrators can open `\\.\ZenithFence`, and creates the symbolic link itself. `delete_device` undoes both, and is called from `driver_unload` after the callouts are gone as well as from the DriverEntry failure path, which the unload routine never reaches. With no framework dispatching for us, the driver has to answer the requests it previously inherited: add create/cleanup/close handlers that complete the IRP successfully, via a new `irp_helpers::complete_empty`, and set DO_BUFFERED_IO so read/write requests arrive in SystemBuffer the way `irp_helpers` already reads them. The link entry point moves from FxDriverEntry to GsDriverEntry in every place that invokes link.exe. Delete `c_helper/` and the WdfDriverEntry/WdfLdr import libraries. Also switch the driver's clock from system time to interrupt time. `get_system_timestamp_ms` returned wall-clock time, so an NTP step or a user changing the clock shifted every cached timestamp: connection eviction could wipe the cache or never run, and bandwidth windows could go backwards. `get_monotonic_ms` reads KUSER_SHARED_DATA.InterruptTime, which only counts forward from boot. Cache cutoffs now saturate, since the clock starts near zero when the driver loads with the system.
Replace `ArrayHolder` with `PendingEvent`. The old type was a `RefCell` behind a hand-written `Sync` impl, so two reads racing on the leftover buffer could each take it and free it twice, and a failed `try_borrow` silently dropped the rest of an event. The new type keeps the serialized event and how many bytes were already sent under the spin-lock mutex, and `Info::into_bytes` hands the buffer over instead of copying the tail. Back `FilterEngineCell` with a kernel mutex. It was an `UnsafeCell` with a fake `Sync`, relying on the filter reset queue and the teardown never overlapping; a KMUTEX enforces it, and unlike a spin lock or a FAST_MUTEX it leaves the IRQL where it is, so the holder can still call the PASSIVE_LEVEL filter engine routines and wait. Delete the unused `fast_mutex.rs`, which was never a module and did not compile. Never leave a pended packet uninjected. An out-of-range verdict byte made `FromPrimitive` return None and the match fell through without injecting, so the connection waited forever; so did a redirect verdict whose connection had been cleared from the cache in the meantime. Both now block and inject. Add a `Drop` to `ClassifyDefer` that completes an operation resolved nowhere else. WFP then classifies the connection again and the verdict comes from the cache, rather than the thread hanging in the kernel and the callout refusing to unregister. Check `can_set_action` in the ALE and packet callouts before touching the packet. Once an earlier filter has made a final decision the classify-out right is gone, and pending or absorbing there is both ignored and, for a pend, unanswerable. Add the connection to the cache before queueing its event, in both ALE auth paths. The verdict can come back as soon as user space reads the event, and it is stored on the cached connection, so an insert afterwards overwrote it with no verdict at all. Skip non-first IP fragments in the packet layer. `get_key_from_nb_v4` and `_v6` read the two bytes after the IP header as ports, which in a later fragment are payload, producing a key for a connection that does not exist. They now return `Ok(None)` for a non-zero fragment offset (following the IPv6 Fragment header chain to find it) and the packet layer permits: the first fragment carries the decision. Fix the `IOQueue` pop status check. `NtStatus::try_from(ptr as u32)` compared only the low 32 bits of the returned pointer, so an entry whose address happened to end in one of the three status values was reported as an error and leaked. Compare the whole value. Make `wait_and_pop` wait in one-second steps so a reader that entered just as `rundown` ran notices the queue is gone, and run the queue down once more from `Drop` to free an entry pushed in that same window. Stop building slices from the IRP system buffer unconditionally: a zero-length read or write has no system buffer, and `from_raw_parts_mut` on null is UB even for length zero. `DeviceControlRequest::complete` now reports the bytes actually written rather than the whole output buffer, and returns the status directly, since the IRP belongs to the I/O manager once completed and must not be read afterwards. Correct the NDIS and WFP struct layouts. `NET_BUFFER_LIST` and `NET_BUFFER` were missing the `DECLSPEC_ALIGN(16)` on their reserved arrays and `NET_BUFFER` was missing the padding of the `NET_BUFFER_HEADER` union, which shifted `DataPhysicalAddress` and `SharedMemoryInfo`. In `FwpsIncomingMetadataValues`, `local_redirect_target_pid` was `u64` and the vSwitch fields were guessed as padding, shifting everything after `current_l2_metadata_values`. Pin all of it down with compile-time offset assertions against the WDK 10.0.26100 headers. Do not leak a callout when its filter cannot be added: the callout registration is not part of the WFP transaction, so an abort does not undo it. `register_callout` unregisters it, `commit` keeps every callout it managed to register so the teardown can find them, and the three transaction paths restore the recorded filter ids when the transaction aborts and WFP keeps the filters it already had. Drop the ShutdownRequest IOCTL from the driver and from `kext_interface`. Shutdown is the write command; the IOCTL was a second entry point into the same teardown.
Cap the packet cache and the event queue. Both grew without bound, so a user-space agent that stopped reading could make the driver hold every pended packet and every event until the machine ran out of pool. The packet cache now holds at most 32768 packets and the event queue 16384 events, and the callouts check `Device::is_backlogged` before capturing anything: when either is full they fail closed (block, without copying the packet) instead of pending one more. Move the two pushes into `Device::queue_packet`, which all three capture sites now call. If the event queue filled up between the backlog check and the push, a packet-layer copy is taken back out of the cache and dropped; a pended ALE operation is left to expire instead, since completing it inside the classify that pended it would reenter the classify. Add `wdk::worker::PeriodicWorker`: a system thread that calls a routine at a fixed interval at PASSIVE_LEVEL and can be stopped and waited for, with `PsCreateSystemThread` and `ZwClose` declared in `ffi` (windows-sys types the start routine without its context argument and links `ZwClose` against ntdll). The device starts one for maintenance, runs it every second, and stops it first thing in `shutdown`, from where on the teardown resolves the remaining packets. Maintenance blocks the packets that have waited 30 seconds for a verdict, so a connect does not hang forever on an agent that stopped answering. A pended connection that is still undecided is moved to `Failed` first, with a compare-exchange (`Connection::replace_verdict`) rather than a store, so a verdict that arrived in the meantime still wins; the reinjection then classifies the connection again and the cached verdict decides it. Split `inject_packet(packet, blocked)` into `inject_permitted_packet` and `inject_blocked_packet` over a shared `reinject_ale`. The boolean read backwards at every call site, and the old body's early `Ok(())` for a blocked packet-layer copy looked like it also covered the ALE case, which must always be reinjected. Stop calling `handle_alloc_error` from the global allocator: it bugchecks on every failed allocation, including the fallible ones. `alloc` returns null as `GlobalAlloc` requires and lets the caller decide. It also honours the requested alignment now, asking for a cache-aligned allocation above the pool's 16 bytes and refusing anything over 64, rather than returning memory the caller cannot use. `PrintMemoryStats` reports the new counters: packets expired without a verdict, events queued, and events dropped because the queue was full. Also drop the unused `CalloutData::can_set_action` wrapper, and record a TODO in `clone_packet` about large send offload, where the copy loses `TcpLargeSendNetBufferListInfo` and the recomputed TCP checksum is not the pseudo-header seed LSO expects.
Replace the RCU port design with a reader-writer spin lock per port. Every read used to clone an `Arc`, and every write copied the whole port array, published the copy and pushed the old one onto an `MpscQueue` to be freed later, once its reader count had been zero for a second. `PortConnections<T>` now holds a `Vec<Entry<T>>` that readers share and a writer changes in place, so an add or a removal costs one lock instead of a copy of the port plus deferred-free bookkeeping. Each port has its own lock, so traffic on one port never waits for another. `rcu_port.rs` and `mpsc_queue.rs` are gone, as are the unlinked-array counters in the memory stats. Because nothing may outlive the lock, the cache hands connections to a closure rather than returning them: `end_v4(key, f)`, `with_connection_v4`, `end_all_on_port_v4(port, f)` and `clean_ended_connections(on_stale_v4, on_stale_v6)`. Every caller now builds its event inside the closure, while the connection is still alive, which is what lets a removed connection be freed right away instead of on a later pass. The closures must not reach into the cache again — a writer waiting on the same port would deadlock — and `read` and `write` say so. Keep each connection's identity inline next to the pointer to it: `Entry` stores a `Connection::CompactKey` (local and remote address and remote port; the cache slot already fixes the protocol and the local port), so a lookup scans contiguous memory and only dereferences the match. `Connection::equals` and `get_key` are replaced by `compact_key` and `compact_key_of`, which also reject a key of the wrong IP version instead of silently failing to match. The redirect check still has to consider every connection on the port, so it is now only reached for packets whose remote port is the DNS or the tunnel port, the only two places a redirect goes. Bound stale eviction with `MAX_STALE_EVICTIONS` instead of a `Vec` capacity. The old code evicted while `capacity() > len()` on a vector built `with_capacity(100)`, so the limit lived in an allocation; it is now a constant of 100 per IP version. Check `alloc_zeroed` for null in `alloc_port_array`. Each of the four port arrays is a single large allocation and the old code wrapped whatever came back in a `Box`, so a failure became a null `self.tcp_v4` and a crash on the first packet. `ConnectionCache::new` returns a `Result` and `Device::new` propagates it. Also move the `dbg!` in `ale_resource_monitor` ahead of the cache call, so a discarded or released port is logged even when no connection was open on it.
Complete the IRP from `ReadRequest::complete` and `WriteRequest::complete`, which consume the request and return the status for the dispatch routine, instead of every caller setting an outcome, reading it back with `get_status` and completing the IRP itself. The outcome setters are renamed `set_success`, `set_end_of_file` and `set_timeout`; a read starts out successful with zero bytes, so a dispatch that finds no device still completes, and a timeout reports no bytes rather than whatever `fill_index` had reached. A write clears `Information` on construction. Read `Value::value_type` as the raw `FWP_DATA_TYPE` that WFP writes there: a type outside the `ValueType` enum made the value an invalid discriminant. `ValueType::from_raw` maps it and `CalloutData::get_value_type` returns an `Option`, which the two endpoint closure callouts match on. Return the transport header size only when the layer provides it. `get_transport_header_size` now checks FWPS_METADATA_FIELD_TRANSPORT_HEADER_SIZE and `create_packet_list` gives up when the field is missing. Unlike the IP header there is no safe fallback, since TCP options make the size vary, and retreating by a wrong amount clones a shifted packet. Check the injection handle the packet actually goes through. `inject_packet_list` tested `packet_inject_handle_v4` even for an IPv6 packet, so an invalid v6 handle got through the guard and into the inject call. Free only the net buffer list that was passed in. `free_net_buffer` walked the `Next` chain, but the lists it frees come from `wrap_packet_in_nbl`, which builds exactly one; anything chained behind it belongs to whoever chained it. Delete `spin_lock.rs`. Nothing used it, and `KSpinLock::create` passed its own null pointer to `KeInitializeSpinLock` and then locked on that. Tighten the two hand-written `Sync` impls. `IOQueue<T>` now requires `T: Send`, since an entry is pushed on one thread and popped on another, and `rw_spin_lock::Mutex<T>` requires `T: Sync` as well, because its readers hold `&T` on several threads at once. Build the connection events on the connection. `end_info` and `update_info` on the `Connection` trait replace eight copies of the same twelve-argument call across the endpoint closures, the resource monitor, `GetConnectionsUpdate` and `CleanEndedConnections`, and one generic `log_connection` replaces the two near-identical walk closures in `PrintMemoryStats`. Report the attempt the reauthorized batch actually went out on: the counter was zeroed before the message read it, so it always said zero. Also note in the packet layer that the loop sets the classify action per net buffer and the last one wins, which is correct because everything in one classify belongs to one flow. Record what is still open. The device is not opened exclusive, so two agents would split the event stream between them, and closing a handle leaves that agent's pended packets to the 30 s timeout and keeps the cached verdicts. The read also blocks its thread in `device.read` until an event arrives, so it cannot be cancelled; it wants a cancel-safe queue.
# Conflicts: # CLAUDE.md # Justfile # driver/src/connection_cache.rs # driver/src/device.rs # driver/src/entry.rs # driver/src/id_cache.rs # driver/src/lib.rs
The scenario tests in mock/sim/tests ran twice: once from source in pr-checks.yml (sim-linux) and once against the installed wheels in zfsim-wheels.yml. Drop the source run and keep the wheel run as the only CI execution of mock/sim/tests, and extend it to pushes to main so the wheels and scenarios are also checked outside pull requests. Build the Go test agent in the wheels job and point ZF_KEXT_TESTER at it, since the installed package cannot locate kext_tester in the source tree. Drop the separate check-kext-interface job from build.yml: the Windows test job in pr-checks.yml already compiles kext_interface via `go test`, so only the full `go vet` is left to add there.
Keep queued events as plain values rather than as serialized buffers. Every event used to be built into an `Info`, a `Vec` allocated in the callout that produced it, pushed as a boxed queue entry, and copied again into the read buffer. The queue now holds an `Event` enum of fixed-size structs -- the connection, end and update events, and the update-end marker -- and they are serialized straight into the read buffer when user space reads them. Log lines stay an `Info`: they are rare and their length varies. A packet event no longer carries the packet. It records the payload length and `Event::write` copies the payload out of the packet cache at read time, under its read lock. A packet that expired in the meantime is written as zeros of the announced length, so the stream keeps its framing. Serialize through a `Sink`: either a `Vec` that grows, as the log lines and the tests use, or a `Writer` over the read buffer, which leaves out the first `sent` bytes of the event and stops at the end of the buffer. That is what lets an event larger than one read be sent across several, so `PendingEvent` now keeps the event itself and how many of its bytes went out, instead of the serialized rest. A test checks that writing an event in chunks of every size produces the same bytes as writing it in one go. `write_event` drops an event that stops short of its own `serialized_size` with room left in the buffer, rather than stashing it and stalling every read after it. Replace the KQUEUE behind `IOQueue` with a `VecDeque` reserved to `max_len` and a synchronization event. The old queue boxed a `LIST_ENTRY` per entry, so a push allocated at DISPATCH_LEVEL on every packet sent to user space, and a failed allocation was not expressible. Room for all entries is taken once, in `IOQueue::new`, which returns a `Result` that `Device::new` propagates before it registers anything; push and pop never allocate after that. A reader waits on the event in one-second steps and passes it on when more entries are left, since it is auto-reset and only the push into an empty queue signals it. With the per-entry box gone, `MAX_EVENTS` goes from 16384 to 32768. Hand connections to the closures without collecting their results. `end_all_on_port_v4`/`_v6` and `clean_ended_connections` returned a `Vec` of whatever the closure built, which the caller then pushed; the closures now push into the event queue themselves, under the port's lock. Pushing never waits, and a full queue drops the event.
Transport injection needed three separately allocated pieces to stay alive until its completion routine ran: the packet copy inside the NET_BUFFER_LIST, the control data blob and the remote address. They were kept alive by boxing a TransportInjectContext and handing that to WFP as the completion context, which means an extra allocation (plus the Vec behind the packet copy) per injected packet. Make the packet copy a single PacketBuffer allocation that can carry a prefix in front of the packet bytes, and put the remote address, the control data length and the control data itself into that prefix. The start of the allocation is then the completion context: no context is allocated for an injection, and freeing the list frees everything the send referenced. Network injections, which only need the list, hand over the same allocation with an empty prefix instead of boxing the NetBufferList. from_injected rebuilds the buffer from the completion context, taking the packet length from the MDL, and refuses to free a list whose MDL does not start right after the prefix, since freeing with a wrong layout would corrupt the pool.
# Conflicts: # driver/src/device.rs # driver/src/lib.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.