Repository navigation
[DO NOT REVIEW] microVM: refuse virtio device buffers outside the e820 RAM ranges - #4794
Merged
Pedro Henrique Penna (ppenna) merged 5 commits intoOct 29, 2026
Conversation
|
This PR modifies files containing For more on why we check whole files, instead of just diffs, check out the Rustonomicon |
A device reaches guest memory through the GuestMemory that its transport gives it, and nothing narrows what that GuestMemory reaches. A microVM virtio device gets the whole guest memory, so it reads or writes any range that the guest names in a descriptor as long as memory backs it, including the ranges that the e820 map reserves inside RAM, such as the shared virtio interrupt-status page at 0x30000 and the ISA hole (microsoft/nvx#554). An emulated IOMMU narrows a device's view by translating each access through the fallback methods, but that gives up the mapping, and with it the zero-copy page locks on which the block path relies. Add GuestMemory::restrict, which returns a view of a memory that admits only the accesses that lie wholly inside a set of page-aligned ranges. The view shares the regions, and so the mappings, of the memory that it restricts, and keeps that memory alive, so an admitted access takes the same path as through the memory itself. GuestMemoryInner::region, which every access takes before it reaches a mapping or a fallback, refuses any other access with an out-of-range error before touching memory, and calls the view's refusal hook, if any, with the refused address and length. Reads, writes, fills, compare-exchanges, probes, page locks, IOVA lookups, and subranges all pass through it, and a subrange of a view stays inside a range that the view admitted when it was created. full_mapping and sharing return None for a view, since either would expose all of the memory. A view admits the union of its ranges, merging overlapping and abutting ones. The ranges must be page-aligned, because a page lock admits a page by its first byte. An empty access touches nothing and is admitted anywhere. A view of a view is rejected rather than intersected, as nothing needs one. Add GuestMemory::check_admitted, which checks a range as an access would, refusal hook included, without touching memory, so that a caller can refuse a request as a whole before acting on any part of it. Memory that is not a view admits every range. Memory that is not a view pays one more None check per access. An access through a view also searches its ranges, of which a microVM has four. The tests check that a view admits accesses inside its ranges, even across abutting ones; that it refuses, through every access path and without writing even the part inside its ranges, an access that lies in or crosses into a page outside them; that its hook sees each refusal; that the memory it restricts still admits everything; and that unaligned ranges and a view of a view are rejected. Part of microsoft/nvx#554. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dmit A virtio queue hands a device each descriptor's address and length without checking them, and the device reads and writes the buffer later, often in pieces: a header before the data, the data page by page, or just a prefix when the buffer is longer than what the device needs. Through a restricted GuestMemory each of those accesses fails on its own, so the device could still read or write the part of a buffer that lies inside the ranges that the memory admits before reaching the part outside them. Check each descriptor's whole buffer, and each indirect table, with GuestMemory::check_admitted before handing the chain to the device, and refuse the chain with the new QueueError::BufferNotAdmitted when the memory does not admit it. As with any other malformed chain, the queue does not consume it and retires its fetch side, so the device reads and writes none of the chain and sees the error once. The queue already places each ring in a subrange that it creates when it starts, which a restricted memory refuses as a whole. Memory that is not restricted admits every range, so nothing changes for a device that gets one. The tests restrict a queue's memory to the RAM ranges of the e820 map of a microVM with 4 GiB of RAM. They check that the queue refuses buffers in the shared interrupt-status page, in the ISA hole, and in the MMIO aperture, and buffers that cross into or out of them, reporting each refusal once and staying retired afterward; that it hands over buffers that end or begin right beside them; that it refuses an indirect table in the ISA hole and a table in RAM whose buffer is the interrupt-status word; and that it fails to start when a ring lies in or crosses into one of them. Part of microsoft/nvx#554. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With guest memory views and the queue's admission check, a device can be kept to a set of ranges of guest memory, but no transport gives a device such a view, and nothing tells the guest when the device refuses a ring or a buffer. Add VirtioMmioDevice::restrict_dma, which a VMM calls before the guest starts the device. It replaces the memory that the transport hands the device in each queue's QueueResources with a view that admits only the given ranges. The transport keeps the unrestricted memory for itself, so a shared interrupt-status word outside the ranges still works. Report a refusal by marking the device as needing reset, as virtio 1.2 section 2.1.2 provides for a device that enters an error state from which only a reset recovers it. The view's hook latches the refusal and wakes the transport, which sets DEVICE_NEEDS_RESET in the device status and, if the driver has set DRIVER_OK, sends a configuration-change notification, once until the next reset. A refused ring fails the queue's start and so DRIVER_OK; the transport resets the device as for any failed start, but leaves DEVICE_NEEDS_RESET set, without a notification, since DRIVER_OK is then clear. A device reset clears the mark and any refusal latched before it, as does a restore, which also checks the saved rings against the view. An error completion was the alternative, but no single form serves every device: the refused buffer may be the one that would carry the error, such as a virtio-blk status byte or a virtio-fs reply header, and virtio-net and virtio-console have no error completion at all. DEVICE_NEEDS_RESET reports a refusal the same way for every device, and the queue has already stopped serving the refused chain. The tests restrict an MMIO device's DMA to the RAM ranges of the e820 map of a microVM with 4 GiB of RAM, with its shared interrupt-status word in the status page, as the microVM places it. They check that the device refuses a writable buffer in, or crossing into, the interrupt-status page, the ISA hole, or the aperture: it writes none of the buffer, the transport marks it as needing reset and notifies the driver through the interrupt-status word, which the device leaves otherwise untouched, and a reset clears both. They check that the device writes buffers right beside those ranges without being marked; and that a ring in or crossing into them fails DRIVER_OK, leaving only DEVICE_NEEDS_RESET set and no notification, until a reset. Part of microsoft/nvx#554. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The microVM MP-table loader builds the e820 map from the memory layout: it reserves the ISA hole and the ranges that the VMM passes, such as the shared virtio interrupt-status page, and leaves the MMIO aperture out of RAM. Only the loader knows the resulting RAM ranges, so a VMM that must keep device DMA inside the RAM that the guest sees would have to repeat the loader's reservations. Add mptable_ram_ranges, which returns the RAM entries of the map that load_x86_mptable builds for a layout and its reserved ranges. It derives them from the same build_mptable_e820, so the two cannot diverge, and validates the layout and reservations as loading does. The test checks the ranges for 8 GiB of RAM, which the aperture splits, and for 256 MiB, which ends below it: both leave out the status page and the ISA hole, and neither reaches into the aperture. Part of microsoft/nvx#554. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The microVM layout requires that DMA ranges lie wholly inside a RAM range of the e820 map and cross neither the MMIO aperture nor a reserved boot structure. Each microVM virtio device received the whole guest memory, though, so a descriptor failed only when its buffer left guest memory, as in the aperture, and then only for the part that did. A guest could point a buffer at a range that the e820 map reserves inside RAM, such as the shared virtio interrupt-status page at 0x30000, which the devices themselves update, or the ISA hole, and the device would read or write it (microsoft/nvx#554). The guest can write those pages directly, so this did not weaken isolation, but the devices enforced less than the layout states. Restrict the DMA of every microVM virtio device to the RAM ranges of the e820 map, which mptable_ram_ranges derives from the memory layout and the same reservations that the loader uses: the status page, and the ISA hole, which the loader adds. The devices thus refuse a queue whose rings, or a descriptor whose buffer, lies even partly outside RAM: they read and write none of it, and each refusal marks the device as needing reset. The transports keep the whole guest memory for the shared interrupt-status words that they update. The ranges come from the layout at device construction, which on a snapshot restore is the restored layout, so they include the RAM that a restore activates beyond the captured base, which the guest adds and onlines. They are computed for the first microVM device, as the e820 map applies only to the microVM profile. The Linux direct-boot page of the Guide describes the restriction. Part of microsoft/nvx#554. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
copilot/microvm-virtio-ram-only-dma
branch
from
October 29, 2026 00:09
d647d4d to
bd2b27a
Compare
Pedro Henrique Penna (ppenna)
added a commit
to microsoft/nvx
that referenced
this pull request
Oct 29, 2026
Pin microsoft/openvmm#4794 at bd2b27a2ef9339bd16692c93facb8d84952dc9f8, five commits on 832d5bb48, the previous pin, which is the head of feature/microvm/dev. The microVM memory layout requires that DMA ranges lie wholly inside a RAM range of the e820 map and cross neither the MMIO aperture nor a reserved boot structure, but each microVM virtio device received the whole guest memory. A descriptor failed only when its buffer left guest memory, as in the aperture, and then only for the part that did, so a guest could point a device buffer at the shared virtio interrupt-status page at 0x30000, which the devices themselves update, or at the ISA hole. The guest can write those pages directly, so this did not weaken isolation, but the devices enforced less than the layout states. The first commit adds guest memory views, which admit only the accesses that lie wholly inside a set of page-aligned ranges and refuse any other before touching memory, on every access path, page locks included, while sharing the mappings of the memory that they restrict, so that admitted accesses keep their zero-copy paths. The second makes a virtio queue check each descriptor's whole buffer, and each indirect table, against the device's memory before handing the chain to the device, and refuse the chain as malformed when the memory does not admit it, so that a device reads and writes none of a buffer that lies even partly outside the admitted ranges. Rings already lie in subranges that the memory refuses as a whole when a queue starts. The third lets an MMIO transport restrict its device to a set of ranges and report each refusal by setting DEVICE_NEEDS_RESET, with a configuration-change notification once the driver has set DRIVER_OK, as virtio 1.2 provides for a device in an error state. A refused ring fails DRIVER_OK and leaves DEVICE_NEEDS_RESET alone set. A reset clears the mark. The transport keeps the whole memory for the shared interrupt-status word. The fourth returns the RAM ranges of the microVM e820 map from the loader that builds it, and the fifth restricts every microVM virtio device to them: the layout's RAM, which after a restore includes the RAM that the restore activates, less the interrupt-status page and the ISA hole. The protocol vectors and built-in CPU profiles that nvx-rs copies are unchanged. Refs #554 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
added a commit
to microsoft/nvx
that referenced
this pull request
Oct 29, 2026
Record the five commits that the previous commit pins as positions 355-359 of the microVM chain, and move the NVX pin to bd2b27a2e. Positions 355-359 restrict the DMA of the microVM virtio devices to the RAM ranges of the e820 map: guest memory views, the virtio queue's whole-buffer check, the MMIO transport's restriction and needs-reset report, the loader's e820 RAM ranges, and the microVM worker that applies them. Position 355 changes only guestmem, which no other commit of the chain changes, so it can go upstream on its own, as positions 350-352 can, and position 356 adds only the queue's check on top of it. Positions 357-359 build on the fork's shared interrupt-status transport, MP-table loader, and microVM worker; say so in the table's remarks. The pin is now 202 commits past the mapped tip. Positions 353 and 354 are no longer only a proposal: microsoft/openvmm#4775 merged them, so position 354, 832d5bb48, is the head of feature/microvm/dev. Say so in the scope, and name microsoft/openvmm#4794 as the proposal of positions 355-359. Refs #554 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
merged commit Oct 29, 2026
bd2b27a
into
feature/microvm/dev
92 of 108 checks passed
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.
Restricts the DMA of every microVM virtio device to the RAM ranges of the e820 map, for microsoft/nvx#554. A device now refuses a queue whose rings, or a descriptor whose buffer, lies even partly in the shared virtio interrupt-status page, the ISA hole, or the MMIO aperture: it reads and writes none of it, and its transport marks it as needing reset.
Based on
feature/microvm/dev. This branch is five commits directly on832d5bb48, the head offeature/microvm/devsince #4775 merged, which microsoft/nvx#948 pins. It was rebased there fromb92daa350:git range-diffshows the five patches unchanged, and #4775 changes none of their files.Why
The microVM memory layout requires that loader writes and DMA ranges lie wholly inside a RAM range and cross neither the MMIO aperture nor a reserved boot structure. The Linux direct loader meets this for its own writes, but each microVM virtio device received the whole guest memory, so a descriptor failed only when its buffer left guest memory, as in the aperture, and then only for the part that did. A guest could point a buffer at the interrupt-status page at
0x30000, which the devices themselves update, or at the ISA hole. The guest can write those pages directly, so this did not weaken isolation, but the devices enforced less than the layout states.Guest memory views (
2607a7d55)GuestMemory::restrictreturns a view of a memory that admits only the accesses that lie wholly inside a set of page-aligned ranges. The view shares the regions, and so the mappings, of the memory that it restricts, so an admitted access takes the same path as before, zero-copy page locks included.GuestMemoryInner::region, which every access takes before it reaches a mapping or a fallback, refuses any other access with an out-of-range error before touching memory and calls the view's refusal hook. Reads, writes, fills, compare-exchanges, probes, page locks, IOVA lookups, and subranges all pass through it.full_mappingandsharingreturnNonefor a view.GuestMemory::check_admittedchecks a range as an access would, without touching memory. Memory that is not a view pays one moreNonecheck per access.An emulated IOMMU-style view, which translates each access through the fallback methods, would have given up the mapping and with it the block path's zero-copy locks; the
bitmapfeature is OpenHCL's and would have added a bitmap check to every OpenVMM access.Whole-descriptor refusal in the queue (
35af55902)A device reads and writes a buffer in pieces, so through a view it could still touch the part of a buffer that lies inside RAM before reaching the part outside. The queue now checks each descriptor's whole buffer, and each indirect table, with
check_admittedbefore handing the chain to the device, and refuses the chain with the newQueueError::BufferNotAdmitted. As with any malformed chain, the queue does not consume it and retires its fetch side. The rings already lie in subranges that the queue creates when it starts, which a view refuses as a whole. Memory that is not a view admits every range, so nothing changes for other devices.MMIO transport restriction and reporting (
b7bec57cd)VirtioMmioDevice::restrict_dmahands the device a view that admits only the given ranges, through each queue'sQueueResources; the transport keeps the unrestricted memory for the shared interrupt-status word.A refusal marks the device as needing reset, as virtio 1.2 section 2.1.2 provides: the view's hook latches the refusal and wakes the transport, which sets
DEVICE_NEEDS_RESETand, if the driver has setDRIVER_OK, sends a configuration-change notification, once until the next reset. A refused ring fails the queue's start and soDRIVER_OK; the transport resets the device as for any failed start but leavesDEVICE_NEEDS_RESETset, without a notification. A device reset clears the mark and any latched refusal, as does a restore, which also checks the saved rings against the view.An error completion was the alternative, but no single form serves every device: the refused buffer may be the one that would carry the error, such as a virtio-blk status byte or a virtio-fs reply header, and virtio-net and virtio-console have none.
The e820 RAM ranges (
09a68a784,bd2b27a2e)loader::linux::microvm::mptable_ram_rangesreturns the RAM entries of the map thatload_x86_mptablebuilds, from the samebuild_mptable_e820, so the two cannot diverge.VirtioMmioSlotscomputes them for the first microVM device from the memory layout and the loader's reservations (the status page, plus the ISA hole that the loader adds) and restricts every device to them. On a snapshot restore the layout is the restored one, so the ranges include the RAM that a restore activates beyond the captured base, which the guest adds and onlines. The Linux direct-boot page of the Guide describes the restriction.Tests
guestmem:test_restrict(admitted accesses, also across abutting ranges; refused accesses through every path, without writing even their admitted part; the hook; locks, subranges, and checks; no whole mapping or sharing) andtest_restrict_rejects_invalid_restrictions.virtio(tests::dma), with memory restricted to the RAM of a 4-GiB microVM:queue_refuses_buffers_outside_ram: buffers in, or crossing into or out of, the status page, the ISA hole, and the aperture are refused once and leave the queue retired; buffers right beside them are handed over.queue_refuses_indirect_tables_outside_ram: a table in the ISA hole, and a table in RAM whose buffer is the interrupt-status word.queue_refuses_rings_outside_ram: a descriptor table, available ring, or used ring in or crossing into those ranges fails the queue's start.mmio_device_refuses_buffers_outside_ram_and_needs_reset: through the MMIO transport with the shared-status word in the status page, the device writes none of a refused writable buffer, the transport setsDEVICE_NEEDS_RESETand publishes a configuration change, which is the only change to the status page, and a reset clears both.mmio_device_writes_buffers_inside_ramandmmio_device_refuses_rings_outside_ram_and_needs_reset(DRIVER_OKfails, onlyDEVICE_NEEDS_RESETremains, no notification).loader:mptable_ram_ranges_exclude_status_page_isa_hole_and_mmio_gap, for 8 GiB and 256 MiB.Validation
cargo clippy --all-targets -- -D warningsandcargo doc --no-depsforguestmem(with and withoutbitmap),virtio,loader, andopenvmm_core;cargo nextest run --profile agentforguestmem(7, and 9 withbitmap),virtio(147),loader(55),openvmm_core(118), andvirtio_blk,virtio_net,virtiofs, andvirtio_console(262 passed, 1 skipped); andcargo xtask fmt(clean). The second commit's tree also passesvirtio's clippy and tests on its own.832d5bb48:cargo xtask fmt,cargo clippy --all-targets -- -D warningsforguestmem,virtio,loader,virtiofs, andopenvmm_core, andcargo nextest run --profile agentforguestmem,virtio,loader, andvirtiofs(322) andopenvmm_core(118), all clean.b92daa350(the previous head,d647d4de4), openvmm: refuse virtio device buffers outside the e820 RAM ranges nvx#974's CI passed every microVM suite on Linux/KVM, Linux/MSHV, and Windows/WHP, as well as OpenVMM's unit tests and VMM tests on those platforms. The NVX pull request will pin this head once openvmm: let a platform restore attach an aggregate to a dormant filesystem slot nvx#948 moves NVX's pin to832d5bb48.Notes