diff options
| author | Joshua Bakita <bakitajoshua@gmail.com> | 2025-05-05 03:53:01 -0400 |
|---|---|---|
| committer | Joshua Bakita <bakitajoshua@gmail.com> | 2025-05-05 03:53:13 -0400 |
| commit | 293430fcb5d4013b573556c58457ee706e482b7f (patch) | |
| tree | 9328fa680f55b4e1a08d24714275b8437be3be5d /mmu.c | |
| parent | 494df296bf4abe9b2b484bde1a4fad28c989afec (diff) | |
Snapshot for ECRTS'25 artifact evaluation
Diffstat (limited to 'mmu.c')
| -rw-r--r-- | mmu.c | 414 |
1 files changed, 411 insertions, 3 deletions
| @@ -1,9 +1,13 @@ | |||
| 1 | /* Copyright 2024 Joshua Bakita | 1 | /* Copyright 2024 Joshua Bakita |
| 2 | * Helpers to deal with NVIDIA's MMU and associated page tables | 2 | * Helpers to deal with NVIDIA's MMU and associated page tables |
| 3 | */ | 3 | */ |
| 4 | #include <linux/dma-mapping.h> // dma_map_page() and dma_unmap_page() | ||
| 4 | #include <linux/err.h> // ERR_PTR() etc. | 5 | #include <linux/err.h> // ERR_PTR() etc. |
| 6 | #include <linux/gfp.h> // alloc_pages() | ||
| 5 | #include <linux/iommu.h> // iommu_get_domain_for_dev() and iommu_iova_to_phys() | 7 | #include <linux/iommu.h> // iommu_get_domain_for_dev() and iommu_iova_to_phys() |
| 6 | #include <linux/kernel.h> // Kernel types | 8 | #include <linux/kernel.h> // Kernel types |
| 9 | #include <linux/list.h> // struct list_head and associated functions | ||
| 10 | #include <linux/mm.h> // put_page() | ||
| 7 | 11 | ||
| 8 | #include "nvdebug.h" | 12 | #include "nvdebug.h" |
| 9 | 13 | ||
| @@ -15,6 +19,11 @@ int g_verbose = 0; | |||
| 15 | #define printk_debug if (g_verbose >= 2) printk | 19 | #define printk_debug if (g_verbose >= 2) printk |
| 16 | #define printk_info if (g_verbose >= 1) printk | 20 | #define printk_info if (g_verbose >= 1) printk |
| 17 | 21 | ||
| 22 | // At least map_page_directory() assumes that pages are 4 KiB | ||
| 23 | #if PAGE_SIZE != 4096 | ||
| 24 | #error nvdebug assumes and requires a 4 KiB page size. | ||
| 25 | #endif | ||
| 26 | |||
| 18 | /* Convert a page directory (PD) pointer and aperture to be kernel-accessible | 27 | /* Convert a page directory (PD) pointer and aperture to be kernel-accessible |
| 19 | 28 | ||
| 20 | I/O MMU handling inspired by amdgpu_iomem_read() in amdgpu_ttm.c of the | 29 | I/O MMU handling inspired by amdgpu_iomem_read() in amdgpu_ttm.c of the |
| @@ -22,7 +31,8 @@ int g_verbose = 0; | |||
| 22 | 31 | ||
| 23 | @param addr Pointer from page directory entry (PDE) | 32 | @param addr Pointer from page directory entry (PDE) |
| 24 | @param pd_ap PD-type aperture (target address space) for `addr` | 33 | @param pd_ap PD-type aperture (target address space) for `addr` |
| 25 | @return A dereferencable kernel address, or an ERR_PTR-wrapped error | 34 | @return A dereferencable kernel address, 0 if an I/O MMU is in use and has |
| 35 | no available mapping for the bus address, or an ERR_PTR-wrapped error | ||
| 26 | */ | 36 | */ |
| 27 | static void __iomem *pd_deref(struct nvdebug_state *g, uintptr_t addr, | 37 | static void __iomem *pd_deref(struct nvdebug_state *g, uintptr_t addr, |
| 28 | enum PD_TARGET pd_ap) { | 38 | enum PD_TARGET pd_ap) { |
| @@ -56,7 +66,7 @@ static void __iomem *pd_deref(struct nvdebug_state *g, uintptr_t addr, | |||
| 56 | // Check for, and translate through, the I/O MMU (if any) | 66 | // Check for, and translate through, the I/O MMU (if any) |
| 57 | if ((dom = iommu_get_domain_for_dev(g->dev))) { | 67 | if ((dom = iommu_get_domain_for_dev(g->dev))) { |
| 58 | phys = iommu_iova_to_phys(dom, addr); | 68 | phys = iommu_iova_to_phys(dom, addr); |
| 59 | printk_debug(KERN_DEBUG "[nvdebug] I/O MMU translated SYS_MEM I/O VA %#lx to physical address %#llx.\n", addr, phys); | 69 | printk_debug(KERN_DEBUG "[nvdebug] %s: I/O MMU translated SYS_MEM I/O VA %#lx to physical address %#llx.\n", __func__, addr, phys); |
| 60 | } else | 70 | } else |
| 61 | phys = addr; | 71 | phys = addr; |
| 62 | 72 | ||
| @@ -143,6 +153,327 @@ uint64_t search_page_directory(struct nvdebug_state *g, | |||
| 143 | return 0; | 153 | return 0; |
| 144 | } | 154 | } |
| 145 | 155 | ||
| 156 | /* GPU Virtual address -> Physical address ("forward" translation) for V2 tables | ||
| 157 | Index the page directories and tables used by the GPU MMU to determine which | ||
| 158 | physical address a given GPU virtual address has been mapped to. | ||
| 159 | |||
| 160 | The page directory and tables may be located in VID_MEM, SYS_MEM, or spread | ||
| 161 | across multiple apertures. | ||
| 162 | |||
| 163 | @param pd_config Page Directory configuration, containing pointer and | ||
| 164 | aperture for the start of the PDE3 entries | ||
| 165 | @param addr_to_find Virtual address to translate to a physical address | ||
| 166 | @param found_addr Where to store found physical address (0 if unfound) | ||
| 167 | @param found_aperture Where to store aperture of found physical address | ||
| 168 | @return 0 on success, -ENXIO if not found, and -errno on error. | ||
| 169 | */ | ||
| 170 | int translate_page_directory(struct nvdebug_state *g, | ||
| 171 | page_dir_config_t pd_config, | ||
| 172 | uint64_t addr_to_find, | ||
| 173 | uint64_t *found_addr /* out */, | ||
| 174 | enum INST_TARGET *found_aperture /* out */) { | ||
| 175 | page_dir_entry_t entry; | ||
| 176 | void __iomem *next_kva; | ||
| 177 | unsigned int level, pde_idx; | ||
| 178 | uintptr_t next = (uintptr_t)pd_config.page_dir << 12; | ||
| 179 | enum PD_TARGET next_target = INST2PD_TARGET(pd_config.target); | ||
| 180 | |||
| 181 | *found_addr = 0; | ||
| 182 | *found_aperture = TARGET_INVALID; | ||
| 183 | |||
| 184 | // Make sure that the query is page-aligned (likely mistake otherwise) | ||
| 185 | if (addr_to_find & 0xfff) { | ||
| 186 | printk(KERN_WARNING "[nvdebug] Attempting to translate unaligned address %#llx in translate_page_directory()!\n", addr_to_find); | ||
| 187 | return -EINVAL; | ||
| 188 | } | ||
| 189 | |||
| 190 | printk_info(KERN_INFO "[nvdebug] Translating addr %#018llx in V2 page table with base %#018llx\n", (u64)addr_to_find, (u64)next); | ||
| 191 | |||
| 192 | // Step through each PDE level and the PTE level | ||
| 193 | for (level = 0; level < 5; level++) { | ||
| 194 | // Index into this level | ||
| 195 | pde_idx = (addr_to_find >> NV_MMU_PT_V2_LSB[level]) & (NV_MMU_PT_V2_SZ[level] - 1); | ||
| 196 | printk_debug(KERN_DEBUG "[nvdebug] Using index %u in lvl %d\n", pde_idx, level); | ||
| 197 | // Hack to workaround PDE0 being double-size and strangely formatted | ||
| 198 | if (NV_MMU_PT_V2_ENTRY_SZ[level] == 16) | ||
| 199 | next += 8; | ||
| 200 | // Obtain a kernel-dereferencable address | ||
| 201 | next_kva = pd_deref(g, next, next_target); | ||
| 202 | if (IS_ERR_OR_NULL(next_kva)) { | ||
| 203 | printk(KERN_ERR "[nvdebug] %s: Unable to resolve %#lx in GPU %s to a kernel-accessible address. Error %ld.\n", __func__, next, pd_target_to_text(next_target), PTR_ERR(next_kva)); | ||
| 204 | return PTR_ERR(next_kva); | ||
| 205 | } | ||
| 206 | // Obtain entry at this level | ||
| 207 | entry.raw_w = readq(next_kva + NV_MMU_PT_V2_ENTRY_SZ[level] * pde_idx); | ||
| 208 | if (entry.target == PD_AND_TARGET_INVALID) | ||
| 209 | return -ENXIO; | ||
| 210 | printk_debug(KERN_DEBUG "[nvdebug] Found %s pointing to %#018llx in ap '%s' at lvl %d (raw: %#018llx)\n", entry.is_pte ? "PTE" : "PDE", ((u64)entry.addr) << 12, pd_target_to_text(entry.target), level, entry.raw_w); | ||
| 211 | // Just return the physical address if this is the PTE level | ||
| 212 | if (entry.is_pte) { // level == 4 for 4 KiB pages, == 3 for 2 MiB | ||
| 213 | *found_addr = ((uint64_t)entry.addr) << 12; | ||
| 214 | *found_aperture = entry.aperture; | ||
| 215 | return 0; | ||
| 216 | } | ||
| 217 | // Otherwise step to the next table level | ||
| 218 | // TODO: Use addr_w as appropriate | ||
| 219 | next = (uint64_t)entry.addr << 12; | ||
| 220 | next_target = entry.target; | ||
| 221 | } | ||
| 222 | |||
| 223 | return 0; | ||
| 224 | } | ||
| 225 | |||
| 226 | // This struct is very special. We will never directly allocate this struct; | ||
| 227 | // its sole purpose is to provide more intuitive names to the offsets at which | ||
| 228 | // we store data in Linux's struct page. Such (ab)use of struct page is | ||
| 229 | // explictly permitted (see linux/mm_types.h). This struct is thus used by | ||
| 230 | // casting a pointer of struct page to a pointer of struct nvdebug_pd_page, | ||
| 231 | // then accessing the associated fields. This pointer may also be freely cast | ||
| 232 | // back to a sturct page pointer. | ||
| 233 | // We have 24 (32-bit) or 44 (64-bit) bytes available in the page struct | ||
| 234 | // (according to the documentation on struct page). Our comments indicate what | ||
| 235 | // available parts of struct page we repurpose for our own needs. | ||
| 236 | struct nvdebug_pd_page { | ||
| 237 | unsigned long __flags; // From struct page; do not touch! | ||
| 238 | // Overlaps struct page.lru | ||
| 239 | struct list_head list; // 4/8 bytes | ||
| 240 | // Overlaps struct page.mapping (and page.share on 32-bit) | ||
| 241 | uintptr_t parent_addr; // 8 bytes | ||
| 242 | // Overlaps struct page.share (page.private on 32-bit) | ||
| 243 | enum PD_TARGET parent_aperture; // 4 bytes | ||
| 244 | // Overlaps page.private (page.page_type on 32-bit) | ||
| 245 | dma_addr_t dma_addr; // 4/8 bytes | ||
| 246 | }; | ||
| 247 | |||
| 248 | /* Collect and free any now-unused page directory/table allocations | ||
| 249 | |||
| 250 | @param force Deallocate all page directories/tables created by this module, | ||
| 251 | no matter if they appear to be in-use or not. | ||
| 252 | @returns Number of freed pages on success, -errno on error. | ||
| 253 | */ | ||
| 254 | int gc_page_directory(struct nvdebug_state *g, bool force) { | ||
| 255 | struct nvdebug_pd_page *page, *_page; | ||
| 256 | void __iomem *parent_kva; | ||
| 257 | page_dir_entry_t parent_entry; | ||
| 258 | int freed_pages = 0; | ||
| 259 | |||
| 260 | // Depth-first traversal (from perspective of each page table) of page | ||
| 261 | // allocations. | ||
| 262 | // (This is depth-first because map_page_directory() always allocates and | ||
| 263 | // pushes page directory allocations before page table allocations.) | ||
| 264 | list_for_each_entry_safe_reverse(page, _page, &g->pd_allocs, list) { | ||
| 265 | printk_debug(KERN_DEBUG "[nvdebug] %s: Checking if page directory/table at %llx (SYS_MEM_?) with parent at %lx (%s) is unused...\n", __func__, page->dma_addr, page->parent_addr, pd_target_to_text(page->parent_aperture)); | ||
| 266 | // Try to determine if we're still in-use. We consider ourselves | ||
| 267 | // potentially in-use if our parent still points to us. | ||
| 268 | parent_kva = pd_deref(g, page->parent_addr, page->parent_aperture); | ||
| 269 | if (IS_ERR(parent_kva)) { | ||
| 270 | printk(KERN_ERR "[nvdebug] %s: Error resolving %#lx in GPU %s to a kernel-accessible address. Error %ld.\n", __func__, page->parent_addr, pd_target_to_text(page->parent_aperture), PTR_ERR(parent_kva)); | ||
| 271 | return -ENOTRECOVERABLE; | ||
| 272 | } | ||
| 273 | // A NULL kva indicates parent no longer exists | ||
| 274 | parent_entry.raw_w = parent_kva ? readq(parent_kva) : 0; | ||
| 275 | // Page directory/table still in-use; do not free unless forced | ||
| 276 | if (parent_entry.addr_w == (page->dma_addr >> 12) && !force) | ||
| 277 | continue; | ||
| 278 | // Free this page table/directory and delete our parent's pointer to us | ||
| 279 | if (parent_entry.addr_w == (page->dma_addr >> 12)) { | ||
| 280 | printk(KERN_WARNING "[nvdebug] %s: Deleting page table/directory at %llx (SYS_MEM_?) with parent at %lx (%s) that may still be in-use!\n", __func__, page->dma_addr, page->parent_addr, pd_target_to_text(page->parent_aperture)); | ||
| 281 | writeq(0, parent_kva); | ||
| 282 | } | ||
| 283 | // Unmap, zero, free, and remove from tracking (these all return void) | ||
| 284 | dma_unmap_page(g->dev, page->dma_addr, PAGE_SIZE, DMA_TO_DEVICE); | ||
| 285 | memset(page_to_virt((struct page*)page), 0, PAGE_SIZE); | ||
| 286 | // Necessary to reset mapcount as we (ab)use its state for other things | ||
| 287 | page_mapcount_reset((struct page*)page); | ||
| 288 | // Same reset needed for mapping | ||
| 289 | ((struct page*)page)->mapping = NULL; | ||
| 290 | // Remove this page from our list of allocated pages | ||
| 291 | list_del(&page->list); | ||
| 292 | // Free the page | ||
| 293 | put_page((struct page*)page); | ||
| 294 | freed_pages++; | ||
| 295 | } | ||
| 296 | printk_debug(KERN_DEBUG "[nvdebug] %s: Freed %d pages.", __func__, freed_pages); | ||
| 297 | return freed_pages; | ||
| 298 | } | ||
| 299 | |||
| 300 | /* Map a GPU virtual address to a physical address in a GPU page table | ||
| 301 | Search for a mapping for specified GPU virtual address, and create a new one | ||
| 302 | if none is found. Automatically creates page directories and page table | ||
| 303 | entries as necessary. | ||
| 304 | |||
| 305 | The page directory and tables may be located in VID_MEM, SYS_MEM, or spread | ||
| 306 | across multiple apertures. | ||
| 307 | |||
| 308 | @param pd_config Page Directory configuration, containing pointer and | ||
| 309 | aperture for the start of the PDE3 entries | ||
| 310 | @param vaddr_to_find Virtual address to check, and map to a physical address | ||
| 311 | if nothing is already mapped (up to 49 bits long) | ||
| 312 | @param paddr_to_map Physical address to use (up to 36 bits long if VID_MEM, | ||
| 313 | and up to 58 bits if SYS_MEM) | ||
| 314 | @param paddr_target Which space does the physical address refer to? | ||
| 315 | @param huge_page Set to map a 2 MiB, rather than 4 KiB, page | ||
| 316 | @return 0 on success, 1 if mapping already exists, -EADDRINUSE if virtual | ||
| 317< | |||
