#line 1 "types.hpp" #include #include #include #include #include #include #include #include #include #include namespace iommu_demo { // 1. Address values: numeric addresses never become native pointers. // Entry contains both a packed word and its decoded teaching fields. // Host: PR bit 0, NextLevel bits 11:9, IR bit 61, IW bit 62. // Guest: P bit 0, R/W bit 1, U/S bit 2, address bits 51:12. // All guest A bits and leaf D bits are preset; this example omits A/D updates. using Address = std::uint64_t; struct Entry { Address target; Address raw; bool present; bool write; Address page_size; // Zero means another table; otherwise 4 KiB / 2 MiB / 1 GiB. unsigned next_level; // Host directory NextLevel; zero for a leaf. }; // Keep fault classification separate from human-readable diagnostics. // PR describes presence; PE describes a permission failure on a present page. struct PageFault : std::runtime_error { bool present; bool permission; PageFault(const char* message, bool is_present, bool is_permission) : std::runtime_error(message), present(is_present), permission(is_permission) { } }; constexpr std::size_t kPageSize = 4096; constexpr std::size_t kEntryBytes = 8; using RamPage = std::array; using EventRecord = std::array; using Memory = std::map>; struct AtcEntry { Address input_page; Address spa_page; bool read; bool write; }; struct PprRecord { unsigned group; Address page; bool write; const char* response; }; } // namespace iommu_demo #line 1 "scenario.hpp" // CURRENT playground snapshot. Compatible with C++17/20/23 and draft C++26. // Synthetic RAM only: no MMIO, hardware access or pointer dereferences. // Selected AMD 48882 rev 3.11 encodings; guest A/D preset. // Guest leaf: 4096 bytes; host leaf: 4096 bytes. // One leaf per context per stage; a smaller host leaf maps only the selected guest region. // Translation path: Guest + host: GVA -> GPA -> SPA. namespace iommu_demo { // 2. Applied UI configuration: roots, request identity and permissions. constexpr unsigned host_mode = 6; constexpr Address host_root = 0x100000ULL; constexpr Address gcr3_root = 0x180000ULL; constexpr Address device_table = 0x4000000ULL; constexpr Address interrupt_table = 0x1C0000ULL; constexpr unsigned requester_id = 512; constexpr unsigned pasid = 42; constexpr unsigned glx = 1; // Device capability and GLX capacity are separate limits. Their minimum is a // software range guard against aliases; it is not the hardware PASmax check. constexpr unsigned pcie_pasid_width = 16; constexpr unsigned iommu_glx_width = 18; constexpr unsigned effective_pasid_width = std::min(pcie_pasid_width, iommu_glx_width); constexpr unsigned pasid_capacity = 1U << effective_pasid_width; constexpr unsigned max_pasid = 65535; static_assert(max_pasid == pasid_capacity - 1); constexpr Address input_address = 0x123456789ABCULL; constexpr std::optional
cpu_linear_page = std::nullopt; constexpr bool write_access = false; constexpr bool translation_valid = true; constexpr bool guest_stage = true; constexpr bool host_stage = true; constexpr Address dte_words[]{0x6180000000100C03ULL, 0x300007ULL, 0x20000000001C000FULL, 0x0ULL}; // PCIe capability registers describe support; controls enable use of that support. // Offsets +4/+6 are relative to their own PASID or ATS extended capability. constexpr std::uint16_t pci_command = 4; constexpr std::uint16_t pasid_cap = 4102, pasid_ctl = 1; constexpr std::uint16_t ats_cap = 0, ats_ctl = 0; // DTE.I is the IOMMU-side ATS gate (Q1 bit 32), separate from ATSCtl.Enable. // AT is TWO bits: 00=untranslated memory, 01=translation query, // 10=translated (AMD: pretranslated) memory, 11=reserved. constexpr bool use_ats = false; constexpr bool use_pri = false; constexpr std::uint16_t pri_ctl = use_pri ? 1 : 0; // PCI_PRI_CTRL_ENABLE. // PRI recipe assumes EPHSup, global PPREn/PPRLogEn, and one allocated request. static_assert(!use_pri || ((dte_words[0] & (1ULL << 52)) && (pri_ctl & 1))); // Command/PPR rings below use semantic records, not packed hardware encodings. // The mock host accepts one group and restores only an injected missing leaf. // 3. Sparse table memory. Each outer map key is a backing SPA. // Missing slots contain zero. Guest and subordinate GCR3 targets are GPAs. // GCR3 subordinate pages use identity host mappings in this example. inline Memory host_tables{ {0x100000ULL, { {0, {0x101000ULL, 0x6000000000101A01ULL, true, true, 0x0ULL, 5}}, }}, {0x101000ULL, { {0, {0x102000ULL, 0x6000000000102801ULL, true, true, 0x0ULL, 4}}, }}, {0x102000ULL, { {0, {0x103000ULL, 0x6000000000103601ULL, true, true, 0x0ULL, 3}}, }}, {0x103000ULL, { {0, {0x104000ULL, 0x6000000000104401ULL, true, true, 0x0ULL, 2}}, }}, {0x104000ULL, { {2, {0x105000ULL, 0x6000000000105201ULL, true, true, 0x0ULL, 1}}, {4, {0x106000ULL, 0x6000000000106201ULL, true, true, 0x0ULL, 1}}, {0, {0x107000ULL, 0x6000000000107201ULL, true, true, 0x0ULL, 1}}, }}, {0x105000ULL, { {0, {0x2000000ULL, 0x6000000002000001ULL, true, true, 0x1000ULL, 0}}, {1, {0x2001000ULL, 0x6000000002001001ULL, true, true, 0x1000ULL, 0}}, {2, {0x2002000ULL, 0x6000000002002001ULL, true, true, 0x1000ULL, 0}}, {3, {0x2003000ULL, 0x6000000002003001ULL, true, true, 0x1000ULL, 0}}, {16, {0x2010000ULL, 0x6000000002010001ULL, true, true, 0x1000ULL, 0}}, {17, {0x2011000ULL, 0x6000000002011001ULL, true, true, 0x1000ULL, 0}}, {18, {0x2012000ULL, 0x6000000002012001ULL, true, true, 0x1000ULL, 0}}, {19, {0x2013000ULL, 0x6000000002013001ULL, true, true, 0x1000ULL, 0}}, }}, {0x106000ULL, { {0, {0x12000000ULL, 0x6000000012000001ULL, true, true, 0x1000ULL, 0}}, {256, {0x13000000ULL, 0x6000000013000001ULL, true, true, 0x1000ULL, 0}}, }}, {0x107000ULL, { {385, {0x181000ULL, 0x6000000000181001ULL, true, true, 0x1000ULL, 0}}, {386, {0x182000ULL, 0x6000000000182001ULL, true, true, 0x1000ULL, 0}}, }}, }; inline Memory gcr3_tables{ {0x180000ULL, { {0, {0x181000ULL, 0x181001ULL, true, true, 0x0ULL, 1}}, {127, {0x182000ULL, 0x182001ULL, true, true, 0x0ULL, 1}}, }}, {0x181000ULL, { {42, {0x400000ULL, 0x400001ULL, true, true, 0x0ULL, 0}}, }}, {0x182000ULL, { {511, {0x410000ULL, 0x410001ULL, true, true, 0x0ULL, 0}}, }}, }; inline Memory guest_tables_in_spa{ {0x2000000ULL, { {36, {0x401000ULL, 0x401027ULL, true, true, 0x0ULL, 3}}, }}, {0x2001000ULL, { {209, {0x402000ULL, 0x402027ULL, true, true, 0x0ULL, 2}}, }}, {0x2002000ULL, { {179, {0x403000ULL, 0x403027ULL, true, true, 0x0ULL, 1}}, }}, {0x2003000ULL, { {393, {0x800000ULL, 0x800067ULL, true, true, 0x1000ULL, 0}}, }}, {0x2010000ULL, { {36, {0x411000ULL, 0x411027ULL, true, true, 0x0ULL, 3}}, }}, {0x2011000ULL, { {209, {0x412000ULL, 0x412027ULL, true, true, 0x0ULL, 2}}, }}, {0x2012000ULL, { {179, {0x413000ULL, 0x413027ULL, true, true, 0x0ULL, 1}}, }}, {0x2013000ULL, { {393, {0x900000ULL, 0x900067ULL, true, true, 0x1000ULL, 0}}, }}, }; // Independent mocked CPU allocations for table pages, indexed by backing SPA. inline const std::map table_cpu_linear{ {0x100000ULL, 0xFFFF900000000000ULL}, {0x101000ULL, 0xFFFF900000001000ULL}, {0x102000ULL, 0xFFFF900000002000ULL}, {0x103000ULL, 0xFFFF900000003000ULL}, {0x104000ULL, 0xFFFF900000004000ULL}, {0x105000ULL, 0xFFFF900000005000ULL}, {0x106000ULL, 0xFFFF900000006000ULL}, {0x107000ULL, 0xFFFF900000007000ULL}, {0x180000ULL, 0xFFFF900000008000ULL}, {0x181000ULL, 0xFFFF900000009000ULL}, {0x182000ULL, 0xFFFF90000000A000ULL}, {0x2000000ULL, 0xFFFF90000000B000ULL}, {0x2001000ULL, 0xFFFF90000000C000ULL}, {0x2002000ULL, 0xFFFF90000000D000ULL}, {0x2003000ULL, 0xFFFF90000000E000ULL}, {0x2010000ULL, 0xFFFF90000000F000ULL}, {0x2011000ULL, 0xFFFF900000010000ULL}, {0x2012000ULL, 0xFFFF900000011000ULL}, {0x2013000ULL, 0xFFFF900000012000ULL}, }; struct Buffer { std::optional
linear; Address gpa; Address spa; Address span; }; inline const std::map buffers{ {42, {std::nullopt, 0x800000ULL, 0x12000000ULL, 0x1000ULL}}, {65535, {std::nullopt, 0x900000ULL, 0x13000000ULL, 0x1000ULL}}, }; constexpr std::size_t transfer_size = 16; constexpr bool read_pattern = false; constexpr std::array pattern{0xde, 0xad, 0xbe, 0xef, 0x1, 0x23, 0x45, 0x67}; constexpr unsigned irq_index = 3; constexpr bool irq_enabled = true; constexpr unsigned irq_vector = 81, irq_destination = 2; // Scenario-specific recovery: restore only a deliberately absent owned leaf. inline bool restore_missing_leaf(Address address) { if (!false || (address & ~0xFFFULL) != 0x123456789000ULL) { return false; } return true; } } // namespace iommu_demo #line 1 "page_tables.hpp" namespace iommu_demo { // 4. Fetch an entry, print its storage and encoding, then check presence. // A fault occurs before any packet bytes are copied. inline Entry fetch(const Memory& memory, Address table_spa, unsigned index, const char* label, unsigned level) { Entry e{}; const auto t = memory.find(table_spa); if (t != memory.end()) { const auto it = t->second.find(index); if (it != t->second.end()) { e = it->second; } } const Address offset = index * 8; std::cout << label << " index " << index << " raw " << e.raw << " -> next table / frame " << e.target << '\n'; std::cout << " backing table page SPA " << table_spa << ", entry SPA " << table_spa + offset << '\n'; const auto linear = table_cpu_linear.find(table_spa); if (linear != table_cpu_linear.end()) { std::cout << " mock table CPU linear page " << linear->second << ", entry linear " << linear->second + offset << '\n'; } std::cout << " mocked RAM bytes (little endian): "; for (unsigned byte = 0; byte < 8; ++byte) { std::cout << std::noshowbase << std::setw(2) << std::setfill('0') << ((e.raw >> (byte * 8)) & 255) << ' '; } std::cout << std::showbase << std::setfill(' ') << '\n'; // The snapshot stores a packed word beside decoded fields for readability. // Reject inconsistent manual edits rather than silently executing a different entry. const bool is_host = std::string_view(label) == "Host"; const bool is_guest = std::string_view(label) == "Guest"; const bool raw_write = is_host ? bool(e.raw & (1ULL << 62)) : bool(e.raw & 2); if (e.present != bool(e.raw & 1) || e.target != (e.raw & 0x000ffffffffff000ULL) || ((is_host || is_guest) && e.write != raw_write)) { throw std::runtime_error( "Packed entry and decoded fields disagree; regenerate or edit both"); } if (e.present && is_host && e.next_level != ((e.raw >> 9) & 7)) { throw std::runtime_error("Packed NextLevel and decoded next_level disagree"); } if (e.present && e.page_size) { if (level > 3 || e.page_size != (Address{1} << (12 + 9 * (level - 1))) || e.target % e.page_size) { throw PageFault("Unsupported or misaligned page leaf", true, false); } if (is_host && e.next_level != 0) { throw PageFault("This model uses NextLevel=0 for standard host leaves", true, false); } if (is_guest && level > 1 && !(e.raw & 128)) { throw std::runtime_error("Large guest leaf requires PS=1 in the packed word"); } } if (e.present && is_host && !(e.raw & (1ULL << 61))) { throw std::runtime_error("IR=0 host entries are outside this read-enabled teaching subset"); } if (!e.present) { throw PageFault("Entry is not present", false, false); } return e; } // 5. Host translation: each index consumes nine address bits. // The six-level top index uses only seven bits of a 64-bit input. inline Address last_host_size = 4096; inline Address translation_remaining = 4096; inline Address host_walk(Address gpa, bool write) { Address table = host_root; // Bits above the root width, or in skipped levels, must be zero. if constexpr (host_mode > 0 && host_mode < 6) { if (gpa >> (12 + 9 * host_mode)) { throw PageFault("GPA above Host Mode width", true, false); } } unsigned level = host_mode; while (level) { const unsigned index = (gpa >> (12 + 9 * (level - 1))) & 511; std::cout << "Host level " << level << '\n'; const auto e = fetch(host_tables, table, index, "Host", level); if (write && !e.write) { throw PageFault("Host write denied", true, true); } if (e.page_size) { last_host_size = e.page_size; return e.target | (gpa & (e.page_size - 1)); } if (e.next_level == 0 || e.next_level >= level) { throw PageFault("Invalid Host NextLevel", true, false); } const Address skipped_mask = (Address{1} << (12 + 9 * (level - 1))) - (Address{1} << (12 + 9 * e.next_level)); if (gpa & skipped_mask) { throw PageFault("Nonzero skipped host bits", true, false); } table = e.target; level = e.next_level; } throw PageFault("Host walk has no leaf", false, false); } // 6. Device lookup, PASID directory lookup, then guest and/or host walk. // Guest Only preserves the guest result because GPA = SPA (host Mode=0). inline Address translate(Address address, bool access_write = write_access) { std::cout << "DTE SPA " << device_table + requester_id * 32 << '\n'; for (unsigned i = 0; i < 4; ++i) { std::cout << " DTE Q" << i << " = " << dte_words[i] << '\n'; } if (!translation_valid) { throw PageFault("DTE V=1, TV=0: table walk unavailable", true, false); } Address gpa = address, guest_size = 0; if (guest_stage) { const Address high = address >> 48; if (high != ((address >> 47 & 1) ? 65535ULL : 0ULL)) { throw PageFault("Non-canonical guest address", true, false); } if (pasid > max_pasid) { throw std::runtime_error("PASID outside demo range; software rejects GLX aliases"); } Address table = gcr3_root; // global GCR3TRPMode=0: top pointer is SPA // PASID selects a root; it is not part of the data virtual address. for (unsigned level = glx + 1; level; --level) { if (level != glx + 1 && host_stage) { table = host_walk(table, false); } const unsigned index = (pasid >> (9 * (level - 1))) & 511; const auto e = fetch(gcr3_tables, table, index, "GCR3", level); table = e.target; } // Guest tables contain GPAs; nested mode translates their memory too. for (unsigned level = 4; level; --level) { const unsigned index = (address >> (12 + 9 * (level - 1))) & 511; const Address entry_gpa = table + index * 8; const Address entry_spa = host_stage ? host_walk(entry_gpa, false) : entry_gpa; std::cout << "Guest level " << level << " table GPA " << table << '\n'; const auto e = fetch(guest_tables_in_spa, entry_spa & ~4095ULL, (entry_spa & 4095) / 8, "Guest", level); if (access_write && !e.write) { throw PageFault("Guest write denied", true, true); } table = e.target; if (e.page_size) { guest_size = e.page_size; break; } } if (!guest_size) { throw PageFault("Guest walk has no supported leaf", false, false); } gpa = table | (address & (guest_size - 1)); std::cout << "GPA " << gpa << '\n'; } const Address spa = host_stage ? host_walk(gpa, access_write) : gpa; translation_remaining = guest_stage ? guest_size - address % guest_size : ~Address{0}; if (host_stage) { translation_remaining = std::min(translation_remaining, last_host_size - gpa % last_host_size); } return spa; } } // namespace iommu_demo #line 1 "log_buffers.hpp" namespace iommu_demo { // A bounded ring with one reserved slot. Pointer offsets advance by 16 bytes; // eight slots demonstrate wraparound and are not a hardware-valid allocation. template class Ring { static_assert(Slots >= 2, "A ring needs one usable slot and one reserved slot"); std::array, Slots> entries_{}; std::size_t head_ = 0, tail_ = 0; public: bool empty() const { return head_ == tail_; } void push(const T& value) { const auto next = (tail_ + 1) % Slots; if (next == head_) { throw std::runtime_error("Teaching ring is full"); } entries_[tail_] = value; tail_ = next; } T pop() { if (empty()) { throw std::runtime_error("Teaching ring is empty"); } const auto value = *entries_[head_]; entries_[head_].reset(); head_ = (head_ + 1) % Slots; return value; } void print(const char* name) const { std::cout << name << " Head=" << head_ * 16 << " Tail=" << tail_ * 16 << '\n'; } }; // 7. Event logging: AMD 48882 rev 3.11, Table 57, 16 bytes per record. // Logging is enabled and SA/SE are zero. This is ordinary DMA (TR=0, I=0), // not an ATS query. D/P contains PASID low bits when GN=1, otherwise DomainID. inline EventRecord make_page_fault(Address address, const PageFault& fault) { const std::uint32_t first = requester_id | (guest_stage ? (pasid >> 16) << 16 : 0); const std::uint32_t flags = (2U << 28) | (fault.present ? 1U << 20 : 0) | (fault.permission ? 1U << 22 : 0) | (fault.present && write_access ? 1U << 21 : 0) | (guest_stage ? (1U << 18) | (1U << 17) | (1U << 16) | (pasid & 0xffffU) : 7U); return {first, flags, static_cast(address), static_cast(address >> 32)}; } } // namespace iommu_demo #line 1 "ats_commands.hpp" namespace iommu_demo { // ATS/PRI service is separate from ordinary memory-request fault reporting. // The ATC entry is scoped to this descriptor's Requester ID and PASID. Each // new run starts empty, so a mapping edit cannot accidentally reuse stale data. inline std::optional atc; inline std::vector ppr_log; // Software work-item history, separate from the ring. inline Ring ppr_ring; inline std::map iommu_cache; enum class Command { InvalidateIommu, InvalidateDevice, CompletionWait, CompletePpr }; struct CommandRecord { Command operation; unsigned group = 0; bool success = false; }; inline Ring command_ring; inline bool iommu_invalidated = false, device_invalidated = false; inline bool completion_observed = false; inline unsigned completion_token = 0; // Real hardware can overlap command execution. This teaching processor fetches // FIFO and completes serially, making the required dependency easy to inspect. inline void process_commands() { std::vector inflight; while (!command_ring.empty()) { inflight.push_back(command_ring.pop()); command_ring.print("Command fetched, not completed:"); } for (const auto& command : inflight) { switch (command.operation) { case Command::InvalidateIommu: iommu_cache.clear(); iommu_invalidated = true; std::cout << "INVALIDATE_IOMMU_PAGES completed\n"; break; case Command::InvalidateDevice: if (!iommu_invalidated) { throw std::runtime_error("IOMMU invalidation still pending"); } atc.reset(); device_invalidated = true; std::cout << "INVALIDATE_IOTLB_PAGES: NIC acknowledgment received\n"; break; case Command::CompletionWait: // Serial execution has completed older queued commands. A wait itself // cannot compensate for an invalidate that software never issued. completion_observed = iommu_invalidated && device_invalidated; std::cout << "COMPLETION_WAIT observed token=" << ++completion_token << '\n'; break; case Command::CompletePpr: if (command.success && (!completion_observed || !iommu_invalidated || !device_invalidated)) { throw std::runtime_error("Cannot respond Success before invalidation completes"); } if (ppr_log.empty() || ppr_log.back().group != command.group) { throw std::runtime_error("PPR group mismatch"); } ppr_log.back().response = command.success ? "Success" : "Failure"; std::cout << "COMPLETE_PPR_REQUEST group=" << command.group << " response=" << ppr_log.back().response << '\n'; break; } } } inline void invalidate_before_dma() { completion_observed = iommu_invalidated = device_invalidated = false; command_ring.push({Command::InvalidateIommu}); command_ring.push({Command::InvalidateDevice}); command_ring.push({Command::CompletionWait}); command_ring.print("Software publishes command Tail:"); process_commands(); } inline void complete_page_request(unsigned group, bool success) { // One descriptor: Requester ID and PASID are the constants above. The group // is copied from the consumed request, not allocated again for the response. command_ring.push({Command::CompletePpr, group, success}); process_commands(); } inline AtcEntry ats_query(Address page) { std::cout << "ATS query AT=01 page=" << page << "; no payload copy\n"; if (const auto it = iommu_cache.find(page); it != iommu_cache.end()) { return it->second; } AtcEntry entry{page, 0, false, false}; // The model independently computes R and W. Architectural A/D updates // and packed ATS completions are intentionally outside this teaching code. try { entry.spa_page = translate(page, false) & ~Address{4095}; entry.read = true; } catch (const PageFault&) { } try { entry.spa_page = translate(page, true) & ~Address{4095}; entry.write = true; } catch (const PageFault&) { } std::cout << "ATS response R=" << entry.read << " W=" << entry.write << '\n'; iommu_cache[page] = entry; return entry; } inline bool mock_page_in(Address address) { // The original allocation already exists. Restore presence only, retaining // its frame and permissions. Do not map arbitrary pages or grant writes. if (!restore_missing_leaf(address)) { return false; } completion_observed = false; std::cout << "Mock OS page-in: set P/PR=1; caches need invalidation\n"; return true; } inline Address resolve_ats(Address address) { if (!completion_observed || !iommu_invalidated || !device_invalidated) { throw std::runtime_error("DMA blocked: required invalidations/completion missing"); } const Address page = address & ~Address{4095}; if (!atc || atc->input_page != page) { auto entry = ats_query(page); auto allowed = [&] { return write_access ? entry.write : entry.read; }; if (!allowed() && use_pri) { ppr_ring.push( {static_cast(ppr_log.size() + 1), page, write_access, "Pending"}); ppr_ring.print("IOMMU logs PAGE_SERVICE_REQUEST, pending=1:"); const auto work = ppr_ring.pop(); ppr_log.push_back(work); ppr_ring.print("Software consumes PPR, pending=0 (response still pending):"); std::cout << "PRI group " << work.group << " -> PPR log, Requester=" << requester_id << " PASID=" << (guest_stage ? pasid : 0) << "; DMA waits\n"; if (mock_page_in(address)) { invalidate_before_dma(); complete_page_request(work.group, true); std::cout << "Page response Success; retry ATS before memory DMA\n"; entry = ats_query(page); } else { complete_page_request(work.group, false); std::cout << "Page response Failure; no new mapping or permission upgrade\n"; } } if (!allowed()) { throw PageFault("ATS did not grant this access", entry.read, entry.read); } atc = entry; std::cout << "NIC caches 4 KiB translation\n"; } else { std::cout << "ATC hit; no page-table walk\n"; } if (!(write_access ? atc->write : atc->read)) { throw PageFault("ATC permission denied", true, true); } translation_remaining = 4096 - (address & 4095); const Address spa = atc->spa_page + (address & 4095); std::cout << "Pretranslated DMA AT=10 SPA=" << spa << '\n'; return spa; } } // namespace iommu_demo #line 1 "dma_engine.hpp" namespace iommu_demo { inline void print_bytes(const char* label, const std::vector& bytes) { std::cout << label; for (std::size_t i = 0; i < std::min(bytes.size(), 64); ++i) { const auto byte = bytes[i]; std::cout << std::hex << std::noshowbase << std::setw(2) << std::setfill('0') << static_cast(byte) << ' '; } std::cout << std::showbase << std::setfill(' ') << '\n'; } // 8. Mock one Ethernet NIC descriptor. RX writes NIC bytes into RAM; // TX reads RAM into the NIC. Splitting at page boundaries is essential: // translating only the first byte cannot authorize a whole DMA buffer. inline int run_dma() { // No transaction is issued when the NIC is disabled. This is not an // IOMMU translation failure and must not append an IO_PAGE_FAULT record. if (!(pci_command & 0x4) || (guest_stage && !(pasid_ctl & 0x1))) { std::cout << "NIC request not issued: enable Bus Master and, for guest DMA, PASID.\n"; return 2; } if (use_ats && (!(ats_ctl & 0x8000) || !(dte_words[1] & (1ULL << 32)))) { std::cout << "ATS DMA blocked: enable ATSCtl and DTE.I.\n"; return 2; } static_assert(transfer_size > 0 && transfer_size <= 65536, "Teaching descriptor is 1..65536 bytes"); static_assert(pattern.size() > 0 && pattern.size() <= 32, "Payload pattern is 1..32 bytes"); if (input_address > ~Address{0} - (transfer_size - 1)) { std::cout << "Descriptor wraps the 64-bit address space; no request issued\n"; return 4; } // fragile: AMD 48882 rev 3.11 Table 64; changed rules make TV=0 classification stale. // Stops before invalidate_before_dma() and ats_query(): there is no response to show. if (use_ats && !translation_valid) { std::cout << "ATS stopped by teaching-model guard: DTE V=1, TV=0 " "(INVALID_DEVICE_REQUEST, TR=1, Type=000b; AMD 48882 rev 3.11 Table 64). " "No response or event is modelled.\n"; std::cout << std::dec << "DMA copied 0/" << transfer_size << " bytes; remaining bytes blocked\n" << std::hex; return 3; } std::map ram; for (const auto& [id, buffer] : buffers) { (void)id; auto& page = ram[buffer.spa]; for (std::size_t i = 0; i < page.size(); ++i) { page[i] = static_cast(i & 255); } } std::vector packet(transfer_size); if (write_access) { for (std::size_t i = 0; i < packet.size(); ++i) { packet[i] = pattern[i % pattern.size()]; } } Ring event_buffer; // One reserved slot; one fault at most in this descriptor. std::size_t completed = 0; // The initial table image may already have P/PR=0 to model page-out. // Invalidate before querying it; page-in repeats this synchronization. if (use_ats) { invalidate_before_dma(); } while (completed < transfer_size) { const Address address = input_address + completed; try { // Translation and permissions must succeed before either endpoint changes. const Address spa = use_ats ? resolve_ats(address) : translate(address); // Validate both translation stages BEFORE reading or writing any byte // of this leaf-sized segment. A rejected segment copies zero bytes. const auto count = std::min( {transfer_size - completed, static_cast(translation_remaining), std::size_t{256}, kPageSize - static_cast(address % kPageSize)}); std::cout << (host_stage ? "SPA " : "GPA = SPA ") << spa << '\n'; // CPU linear aliases are independent metadata given by the scenario. for (const auto& [id, buffer] : buffers) { if (buffer.linear && spa >= buffer.spa && spa < buffer.spa + buffer.span) { std::cout << "Data CPU linear address " << *buffer.linear + (spa - buffer.spa) << " for PASID " << id << '\n'; } } // Reject unsupported edited mappings before touching RAM. The NIC // uses 256-byte requests, bounded by a 4 KiB PCIe address boundary. const bool backed = std::any_of(buffers.begin(), buffers.end(), [&](const auto& item) { const auto& b = item.second; return spa >= b.spa && spa - b.spa < b.span && count <= b.span - (spa - b.spa); }); if (!backed) { throw std::runtime_error("Translated segment has no mock RAM allocation"); } std::vector before, after; for (std::size_t i = 0; i < count; ++i) { // Allocate only touched 4 KiB backing blocks, even with 1 GiB leaves. const Address byte_spa = spa + i; const auto [it, inserted] = ram.try_emplace(byte_spa & ~Address{kPageSize - 1}); auto& page = it->second; if (inserted) { for (std::size_t j = 0; j < page.size(); ++j) { page[j] = static_cast(j & 255); } } const auto offset = static_cast(byte_spa & (kPageSize - 1)); // Optional RAM fixture preparation, not a device write. if (!write_access && read_pattern) { page[offset] = pattern[(completed + i) % pattern.size()]; } before.push_back(page[offset]); if (write_access) { page[offset] = packet[completed + i]; } else { packet[completed + i] = page[offset]; } after.push_back(page[offset]); } print_bytes("RAM before: ", before); print_bytes("RAM after: ", after); completed += count; } catch (const PageFault& fault) { if (use_ats) { // An ATS permission/missing-page response did not issue a // memory request: no ordinary DMA IO_PAGE_FAULT is appended. std::cout << "ATS stopped: no AT=10 request for this page; event buffer pending=0\n"; std::cout << std::dec << "DMA copied " << completed << '/' << transfer_size << " bytes; remaining bytes blocked\n" << std::hex; return 3; } // Earlier chunks stay copied; the rejected chunk has no side effects. const auto record = make_page_fault(address, fault); event_buffer.push(record); std::cout << "TRANSLATION FAULT: " << fault.what() << '\n'; std::cout << "IO_PAGE_FAULT code=2 address=" << address << '\n'; for (const auto value : record) { std::cout << value << ' '; } std::cout << "\nEvent buffer head=0x00 tail=0x10 pending=1\n"; std::cout << std::dec << "DMA copied " << completed << '/' << transfer_size << " bytes; remaining bytes blocked\n" << std::hex; print_bytes("NIC packet: ", packet); return 1; } } print_bytes("NIC packet: ", packet); std::cout << std::dec << "DMA copied " << completed << '/' << transfer_size << " bytes; event buffer pending=0\n" << std::hex; return 0; } } // namespace iommu_demo #line 1 "interrupts.hpp" namespace iommu_demo { // 9. Independent synthetic interrupt memory and APIC queue. // Legacy remapping uses a 4-byte IRTE: RemapEn[0], IntType[4:2], // Destination[15:8], Vector[23:16]. Fixed delivery uses IntType=0. // No physical MSI write, MMIO access or CPU interrupt is executed here. inline void deliver_completion_interrupt() { constexpr std::uint32_t irte = 1U | (irq_destination << 8) | (irq_vector << 16); const std::map interrupt_memory{{3, irte}, {7, irte}}; std::vector apic_pending; std::cout << "IRTE SPA " << interrupt_table + irq_index * 4 << '\n'; const auto it = interrupt_memory.find(irq_index); if (!irq_enabled || it == interrupt_memory.end() || !(it->second & 1)) { std::cout << "Interrupt stopped: blocked or absent IRTE\n"; return; } const auto word = it->second; const unsigned decoded_vector = (word >> 16) & 255; const unsigned decoded_destination = (word >> 8) & 255; apic_pending.push_back(decoded_vector); std::cout << "IRTE raw " << word << " Interrupt vector " << decoded_vector << " -> APIC " << decoded_destination << '\n'; // Mock ISR acknowledges the queued vector after reading NIC completion. apic_pending.erase(apic_pending.begin()); std::cout << "Mock ISR: completion acknowledged; APIC pending=" << apic_pending.size() << '\n'; } } // namespace iommu_demo #line 1 "main.cpp" // Start here: one descriptor, then an interrupt only on success. using namespace iommu_demo; inline int run_example() { std::cout << std::hex << std::showbase; if (cpu_linear_page) { std::cout << "Optional data CPU linear base " << *cpu_linear_page << '\n'; } std::cout << "PCI Command " << pci_command << " PASIDCap " << pasid_cap << " PASIDCtl " << pasid_ctl << " ATSCap " << ats_cap << " ATSCtl " << ats_ctl << '\n'; // The optional CPU address is metadata, never needed by the DMA walk. const int status = run_dma(); // A NIC completion interrupt follows successful DMA only. An IOMMU event // is a separate mechanism; a failed descriptor must not claim completion. if (status == 0) { deliver_completion_interrupt(); } else { std::cout << "No successful NIC completion interrupt.\n"; } return status; } int main() { try { return run_example(); } catch (const std::exception& error) { std::cerr << "Model stopped: " << error.what() << '\n'; return 4; // Invalid edited snapshot or unsupported model state. } }