R1: GEM objects, handles, names, devmap mmap and dumb buffers #2
Loading…
Reference in a new issue
No description provided.
Delete branch "r1-gem"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacked on #1 (R0): head
r1-gem, baser0-uapi-core. Review and merge #1 first; this pull request then retargets tomainunchanged.Milestone R1 of the Rust DRM core: GEM objects, handles, FLINK names, fake mmap offsets, mmap through
devmap(9E), system-memory objects, dumb buffers, and interrupt-safe release, withrdrmnullas the test driver. It depends on illumos-ddibfc7ff7b2a(drm-r0, illumos/illumos-ddi#1), which adds thedevmap(9E)entry point to theCharDeviceglue.What was built
drm::gem::GemObjectis what a driver's buffer type provides (size, theddi_umemcookie its mappings show, hooks for movable objects). The core wraps it in a countedGemInner(handle count, FLINK name, fake offset, mapping list) and hands outGemRefs.GemRef::put(&Sleepable)destroys the object on the last reference;GemRef::put_atomic(&Atomic)parks it on a lock-free list (linked through the object, no allocation) and dispatches a task-queue drain withDDI_NOSLEEP; an implicit drop takes the parking path. Destroying an object removes its offset, frees its memory and updates the statistics.IdMaps: numbers fromid_space(9F)(id_allocff_nosleep: from 1, lowest free, like Linux'sidr_alloc(…, 1, 0, …)) and aBTreeMapbehind the file's or the device's mutex. The handle/name rules of Linux'sdrm_gem.c(handle count and name changed only with the name table locked, name gone with the last handle, name table locked before a file's table) are plain data inobjects.rs. Fake offsets are page ranges from avmem(9F)identifier arena over Linux's 64-bitDRM_FILE_PAGE_OFFSET_START/SIZE(256 MiB up, 64 GiB), one per object, assigned on firstMAP_DUMB, freed with the object.devmap(9E)finds the object whose range starts at the offset, takes a reference and callsdevmap_umem_setup(9F)with map/dup/unmap/access callbacks.devmap_map(9E)refuses private mappings and files without a handle to the object; dup (fork) and unmap (munmap, exit, split) take and drop a reference per piece; access defaults todevmap_default_access(9F). Every piece a partialmunmap(2)leaves gets private data with its own reference, and stays unsettled until its next fault, because the framework adjusts its memory offset only afterdevmap_unmap(9E)returns; the fault callsGemObject::mappedwith the final geometry, and a movable object only invalidates unsettled pieces when it moves. The reference taken indevmap(9E)is parked by mapping handle and thread, because the framework can free the handle without any callback when themmap(2)fails later (it does forMAP_PRIVATE); the thread's next entry point orclose(9E)drops it.SysMemisddi_umem_alloc(9F)withDDI_UMEM_SLEEP(locked; the function zero-fills), mapped cached. WithDriver::DMA_ATTRit is bound withddi_dma_addr_bind_handle(9F)against the device node;sg_iter()yields the cookies, which are checked to cover the object exactly.GEM_CLOSE(render allowed),GEM_FLINKandGEM_OPEN(DRM_AUTH, soEACCESon render nodes),MODE_CREATE_DUMB,MODE_MAP_DUMB,MODE_DESTROY_DUMB(primary, no flags), as in Linux'sdrm_ioctls[]; driver-private ioctls0x40-0x9fviaDriver::IOCTLSandDriver::ioctl, matched on the whole command value; 32-bit callers getENOTSUPfor all of them.drm_mode_create_dumb: any non-zero bpp up toU32_MAX - 8(ceil(bpp/8)bytes per pixel), non-zero dimensions,flagsignored, Linux's 32-bit size limit,EINVALfor every overflow; the default layout (Driver::dumb_layout) pads rows to 64 bytes and rounds the size to a page.MAP_DUMBandGEM_CLOSEignorepad, as Linux does.GET_CAPnow reportsDUMB_BUFFER(withFeatures::GEM | DUMB) and the driver'sDUMB_PREFERRED_DEPTH/DUMB_PREFER_SHADOW;ADDFB2_MODIFIERSstays 0 until R3.<driver>:<instance>:gem: objects, umem_bytes, dma_segments, handles, names, offsets, mappings, pending_maps (gauges) and deferred_frees, drain_tasks, faults (counters).detach(9E)waits for the drain task and refuses while any gauge is non-zero.timeout(9F)callback.KArcand the deferral rule from eight threads (half in simulated interrupt context, with a concurrent drainer), the handle/name rules from eight threads with an invariant checker holding all locks, id maps, the offset map, dumb-buffer arithmetic including every overflow. VM:tools/test/drm_r1_test.cplusrun-r1-test.sh, which also runs the R0 test and checks/etcand/devicesbefore and after.Decisions and deviations
KArc, notalloc::sync::Arc. Fallible construction with the context token, andKArc::releasereturns the last value in place, which is what lets interrupt context park an object without allocating.upgrade(get-unless-zero) serves the offset table, which holds no references.ddi_taskq_dispatch(DDI_NOSLEEP); if the dispatch fails, the next thread-context entry point or detach drains. High-level interrupt handlers must hand references to a soft interrupt (documented).vmem(9F)identifier arena, as planned (VM_NOSLEEP | VM_BESTFIT; exhaustion isENOSPC, never a wait).id_space(9F)+BTreeMap, as planned.BTreeMapneeds a global allocator: driver modules enableddi'sglobal-alloc-sleep(alwaysKM_SLEEP); the core inserts only with theSleepabletoken. Handle/name ids use the_nosleepfirst-fit call because the blocking one would wait forever on an exhausted space.CharDevice::devmapandmodule! { ops: [char, devmap] }(installsddi_devmap_segmap(9F)andD_DEVMAP) on thedrm-r0branch (94680b671c,77df4ce177,bfc7ff7b2a), same style asclone_open; the callbacks anddevmap_umem_setupstay in the core. The glue accepts a validated length up to the page holding the end, sincemmap(2)passes the length unrounded, and refuses 0 or a partial page (devmap_setup(9F)would otherwise loop).devmap_map(9E)gets onlydev), with a per-object-type tag checked on lookup; one DRM driver per module.DDI_UMEM_PAGELOCKdoes not exist; non-pageable (DDI_UMEM_SLEEPwithoutDDI_UMEM_PAGEABLE) is the locked default.ddi_umem_alloczero-fills (bzeroinvm/seg_dev.c), verified in the test./etc/path_to_instafterrem_drv(restored by hand). Trial 2 bound againstddi_root_node(9F)from the pseudo node and panicked the VM at the first bind (no dump device, so no stack). The final configuration keeps rdrmnull pseudo withDMA_ATTR = None; the sg path is exercised in trial 1 only.Driver::MAX_OBJECT_SIZE(256 MiB default) andDriver::MAX_MEMORY(1 GiB per device default) refuse withENOMEM. The device budget is charged before the allocation (GemCore::reserve/create_reserved), so concurrent requests cannot exceed it transiently.mmap(2)of a GEM object withMAP_PRIVATEis refused (the devmap framework maps the object's own pages; there is no copy-on-write).mmap(2)isENXIO, becausedevmap_setup(9F)maps every driver error toENXIO(Linux:EINVALbad offset,EACCESno handle).The refault experiment
Works. rdrmnull's movable object (four pages) has two
SysMembackings;RDRMNULL_IOCTL_MOVEswitches every recorded mapping piece to the other backing withdevmap_umem_remapand invalidates it withdevmap_unload(9F); the object'sdevmap_access(9E)reloads withdevmap_load(9F). Measured through the kstatfaultscounter in the final run:devmap_unloadalone: no fault until the next access; one read then faults once (7332 → 7333) and sees the same memory (0x11).Findings for R5:
devmap_umem_remaphas no manual page and is not in theddimapfile (declared in rdrmnull only; the core does not use it; R5 needs it in the private tier or documented upstream). It needsDEVMAP_ALLOW_REMAPat setup, and segdev then refusesF_SOFTLOCKon such mappings (FC_NOSUPPORT: raw I/O straight from a movable mapping fails).devmap_unload(9F)counts offsets from the piece's own user offset, so the manual's "offset 0, length 0 = whole mapping" isEINVALfor GEM offsets. A piece split off bymunmap(2)must not be remapped until the framework has finished it (devmap_handle_reduce_lenruns after the callback and adds the unmapped length to the offset); the core therefore settles such pieces at their next fault and a move only invalidates them.Verification
All on oibldofgate (root, global zone, illumos
cda5b9a49f, Hipster 2026.04), final run fromc4a8512060with illumos-ddibfc7ff7b2a, after review round 1; logs in the workspace underlogs/oibldofgate/:cargo check/clippy -D warnings/fmt --check/doc -D warnings(hosted illumos target),cargo test -p drm -p drm-uapi(42 + 1),xtask uapi-check(103 sizes, 448 offsets, 115 ioctls, 0 differences),xtask build rdrmnull20260928-r1-local-checks.logxtask kernel-check --emit | mdb -k | --verifyddi_ncpus_expected);devmap_callback_ctl0x28 withdevmap_mapat 8 anddevmap_unmapat 0x20 (all five members checked by hand),ddi_dma_cookie_t0x18,ddi_umem_cookie_t8 (opaque),devmap_umem_remapresolves in genunix20260928-r1-kernel-check.logrun-r1-test.sh: link (ld -ztype=kmod), 67 imported symbols all resolved,add_drv20260928-r1-rdrmnull-load-test.logdrm_r0_test(R0 regression, updated for R1 caps andGEM_CLOSE)drm_r0_probe32/drm_r1_probe32drm_r1_test: caps, dumb args (Linux rules) and limits, create/MAP_DUMB/mmap (zeros)/pattern/second and short mapping, refused mappings, fork (inherited + new mapping, writes both ways), partial munmap split, FLINK/OPEN from a separate root process and fromnobodybefore/after auth, render-nodeEACCES, no-handle mmap refused, name gone with last handle while mapping keeps object, close(2) releasing objects, 8 threads × 150 rounds, refault experiment incl. head/middle munmap of a movable mapping, interrupt-context release20260928-r1-rdrmnull-load-test-repeat.logrdrmnull0.namesandrdrmnull0.mmapin use 0 before unload, gone after;umem_np: the repeat run returned exactly to its baseline (2097152 → 2097152), as did trial 7 and both runs of the pre-review build; the final run's before/after differs by one 128 KiB zero-filled segment (all 16umem_npsegments on the machine are 128 KiB, a size no test object has, 15 of them from before any test), attributed to another consumermodunloadrefused while/dev/dri/card0is open, succeeds after close;rem_drv -C/etcdriver files (incl.path_to_inst) and/deviceshash identically before and after;/dev/drigone20260928-r1-vm-state-{before,after}.log::findleakswas not used: the VM runs a non-debug kernel (kmem_flags0); leaks are covered by the kstat gauges, the vmem arenas andumem_np.VM state left behind:
/root/illumos-drm-test/r1-*(final run directoryr1-c4a8512060, the pre-review runr1-c2dec98c12, seven trial directories); no driver, no/dev/dri,/etcand/devicesas found. Trial 1 added"/rdrmnull@0,0" 0 "rdrmnull"to/etc/path_to_inst(root-nexus node); the file was restored frompath_to_inst.old(identical to the pre-test hash) and the reboot after trial 2 dropped the in-kernel entry. Trial 2 panicked the VM (DMA againstddi_root_node), which rebooted; the driver registration it left was removed withrem_drv, and stale devfs node attributes from that boot were cleared byrem_drv -Cin a later run.Review round 1
An Anima-bar review rejected the first version; fixed in
4932bb4andc4a8512: split mapping pieces always keep private data and a reference (use-after-free and missing reference before); split pieces are settled at their next fault instead of remapped insidedevmap_unmap(9E)(double offset shift); the memory budget is reserved before the locked allocation; the registry sets its type tag only after claiming the slot; the ddi glue refuses a zero or partial-page devmap length; dumb-bufferbpp/flagsandpadhandling now match Linux. Kept as the reviewer asked:KArc/deferand their tests, the handle/name rules and 8-thread test, the offset/id-map semantics, the pending-map design, the permission table, the refault experiment and the kstat harness.Open issues
devmap_umem_remapneeds a binding (private tier or manual page upstream) before R5; the settle-at-next-fault rule for split pieces is tested single-threaded (a move racing a concurrentmunmap(2)of the same mapping is covered by design, not by a racing test).rdrmnull's memory limit (64 MiB) and the default (1 GiB) are fixed numbers; a limit derived from physical memory would be better.PROT_*/MAP_*constants are missing; the core definesPROT_ALL,MAP_TYPEandMAP_SHAREDlocally.Anima work item: WI-453
01a0e387-264f-7f43-9774-e4590c12ceddThe core gains the GEM object model of Linux's drm_gem.c and the dumb buffers of drm_dumb_buffers.c, built on illumos primitives: * gem::GemObject is what a driver's buffer type provides (size, the ddi_umem cookie its mappings show, hooks for movable objects); the core wraps it in a counted GemInner with its handle count, FLINK name and fake mmap offset. GemRef::put drops a reference in thread context and destroys the object on the last one; GemRef::put_atomic parks it for the device's task queue; an implicit drop takes the parking path. * Handles per file and names per device are IdMaps: numbers from id_space(9F) (first fit, from 1, like idr_alloc) and a BTreeMap under the file's or the device's mutex. The handle and name rules live in objects.rs as plain data, with Linux's lock order (name table, then a file's table), and are tested on the host from eight threads. * Fake mmap offsets come from a vmem(9F) identifier arena over Linux's 64-bit range (DRM_FILE_PAGE_OFFSET_START/SIZE), one page range per object, assigned on first use and freed with the object. * mmap(2) goes through the new ddi devmap(9E) glue: the core finds the object whose range starts at the offset, calls devmap_umem_setup(9F) with map, dup, unmap and access callbacks that take and drop a reference per mapping piece, refuses private mappings and files without a handle to the object, and parks the reference taken in devmap(9E) until devmap_map(9E), dropping it if the mmap fails later. The callbacks find the device through a table indexed by device number. * gem::SysMem is zero-filled, locked kernel memory from ddi_umem_alloc(9F) and, with Driver::DMA_ATTR, bound with ddi_dma_addr_bind_handle(9F); SysMem::sg_iter yields the cookies as the scatter/gather table, which is checked to cover the object exactly. Mappings are cached. * Ioctls: GEM_CLOSE (render nodes too), GEM_FLINK and GEM_OPEN (auth), MODE_CREATE_DUMB, MODE_MAP_DUMB and MODE_DESTROY_DUMB (primary), with the flags of Linux's drm_ioctls[], and driver-private ioctls from DRM_COMMAND_BASE, matched on the whole command value. Dumb buffers accept 8/16/24/32 bpp unless the driver's dumb_layout says otherwise, pad rows to 64 bytes and refuse non-zero flags and every overflow; MAP_DUMB and GEM_CLOSE refuse a non-zero pad. Objects are limited per object and per device (MAX_OBJECT_SIZE, MAX_MEMORY), because their memory is locked. * GET_CAP reports DUMB_BUFFER for drivers with Features::GEM and DUMB and the driver's DUMB_PREFERRED_DEPTH and DUMB_PREFER_SHADOW; ADDFB2_MODIFIERS stays 0 until R3. * The gem kstat counts objects, object memory, DMA segments, handles, names, offsets, mappings, pending mmaps, deferred releases, drain tasks and faults; detach refuses while any gauge is not zero. illumos-ddi moves to 77df4ce177 (drm-r0 branch), which adds CharDevice::devmap and `module! { ops: [char, devmap] }`. rdrmnull gets GEM and dumb buffers (16 MiB per object, 64 MiB per device) and three test-only private ioctls: movable objects with two backings for the refault experiment (moved with devmap_umem_remap, which has no manual page and is declared in rdrmnull only, invalidated with devmap_unload(9F), reloaded from devmap_access(9E) with devmap_load(9F)), and a reference dropped from a timeout(9F) callback. As a pseudo device it does no DMA.