R1: GEM objects, handles, names, devmap mmap and dumb buffers #2

Merged
forge merged 6 commits from r1-gem into main 2026-09-28 18:11:35 +00:00
Contributor

Stacked on #1 (R0): head r1-gem, base r0-uapi-core. Review and merge #1 first; this pull request then retargets to main unchanged.

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, with rdrmnull as the test driver. It depends on illumos-ddi bfc7ff7b2a (drm-r0, illumos/illumos-ddi#1), which adds the devmap(9E) entry point to the CharDevice glue.

What was built

  • Objects and references. drm::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 (handle count, FLINK name, fake offset, mapping list) and hands out GemRefs. 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 with DDI_NOSLEEP; an implicit drop takes the parking path. Destroying an object removes its offset, frees its memory and updates the statistics.
  • Tables. Per-file handles and per-device names are IdMaps: numbers from id_space(9F) (id_allocff_nosleep: from 1, lowest free, like Linux's idr_alloc(…, 1, 0, …)) and a BTreeMap behind the file's or the device's mutex. The handle/name rules of Linux's drm_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 in objects.rs. Fake offsets are page ranges from a vmem(9F) identifier arena over Linux's 64-bit DRM_FILE_PAGE_OFFSET_START/SIZE (256 MiB up, 64 GiB), one per object, assigned on first MAP_DUMB, freed with the object.
  • mmap. devmap(9E) finds the object whose range starts at the offset, takes a reference and calls devmap_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 to devmap_default_access(9F). Every piece a partial munmap(2) leaves gets private data with its own reference, and stays unsettled until its next fault, because the framework adjusts its memory offset only after devmap_unmap(9E) returns; the fault calls GemObject::mapped with the final geometry, and a movable object only invalidates unsettled pieces when it moves. The reference taken in devmap(9E) is parked by mapping handle and thread, because the framework can free the handle without any callback when the mmap(2) fails later (it does for MAP_PRIVATE); the thread's next entry point or close(9E) drops it.
  • System memory. SysMem is ddi_umem_alloc(9F) with DDI_UMEM_SLEEP (locked; the function zero-fills), mapped cached. With Driver::DMA_ATTR it is bound with ddi_dma_addr_bind_handle(9F) against the device node; sg_iter() yields the cookies, which are checked to cover the object exactly.
  • Ioctls. GEM_CLOSE (render allowed), GEM_FLINK and GEM_OPEN (DRM_AUTH, so EACCES on render nodes), MODE_CREATE_DUMB, MODE_MAP_DUMB, MODE_DESTROY_DUMB (primary, no flags), as in Linux's drm_ioctls[]; driver-private ioctls 0x40-0x9f via Driver::IOCTLS and Driver::ioctl, matched on the whole command value; 32-bit callers get ENOTSUP for all of them.
  • Dumb buffers. As Linux's drm_mode_create_dumb: any non-zero bpp up to U32_MAX - 8 (ceil(bpp/8) bytes per pixel), non-zero dimensions, flags ignored, Linux's 32-bit size limit, EINVAL for every overflow; the default layout (Driver::dumb_layout) pads rows to 64 bytes and rounds the size to a page. MAP_DUMB and GEM_CLOSE ignore pad, as Linux does. GET_CAP now reports DUMB_BUFFER (with Features::GEM | DUMB) and the driver's DUMB_PREFERRED_DEPTH/DUMB_PREFER_SHADOW; ADDFB2_MODIFIERS stays 0 until R3.
  • Observability. kstat <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.
  • 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 and a "move" for the refault experiment, and a reference dropped from a timeout(9F) callback.
  • Tests. Host: KArc and 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.c plus run-r1-test.sh, which also runs the R0 test and checks /etc and /devices before and after.

Decisions and deviations

  • Refcount: own KArc, not alloc::sync::Arc. Fallible construction with the context token, and KArc::release returns 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.
  • Deferred release: lock-free list in the object + one-shot 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).
  • Offsets: vmem(9F) identifier arena, as planned (VM_NOSLEEP | VM_BESTFIT; exhaustion is ENOSPC, never a wait).
  • Handles: id_space(9F) + BTreeMap, as planned. BTreeMap needs a global allocator: driver modules enable ddi's global-alloc-sleep (always KM_SLEEP); the core inserts only with the Sleepable token. Handle/name ids use the _nosleep first-fit call because the blocking one would wait forever on an exhausted space.
  • devmap glue in illumos-ddi: CharDevice::devmap and module! { ops: [char, devmap] } (installs ddi_devmap_segmap(9F) and D_DEVMAP) on the drm-r0 branch (94680b671c, 77df4ce177, bfc7ff7b2a), same style as clone_open; the callbacks and devmap_umem_setup stay in the core. The glue accepts a validated length up to the page holding the end, since mmap(2) passes the length unrounded, and refuses 0 or a partial page (devmap_setup(9F) would otherwise loop).
  • The callbacks find the device through a per-module table indexed by device number (devmap_map(9E) gets only dev), with a per-object-type tag checked on lookup; one DRM driver per module.
  • DDI_UMEM_PAGELOCK does not exist; non-pageable (DDI_UMEM_SLEEP without DDI_UMEM_PAGEABLE) is the locked default. ddi_umem_alloc zero-fills (bzero in vm/seg_dev.c), verified in the test.
  • DMA in rdrmnull: none. The pseudo nexus refuses DMA handles. Trial 1 ran rdrmnull as a root-nexus child: every object was bound (300 cookies for a 1,228,800-byte buffer, sizes summing up), but such a node leaves its instance in /etc/path_to_inst after rem_drv (restored by hand). Trial 2 bound against ddi_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 with DMA_ATTR = None; the sg path is exercised in trial 1 only.
  • Memory limits (new, not in the work item): object memory is locked kernel memory, so Driver::MAX_OBJECT_SIZE (256 MiB default) and Driver::MAX_MEMORY (1 GiB per device default) refuse with ENOMEM. The device budget is charged before the allocation (GemCore::reserve / create_reserved), so concurrent requests cannot exceed it transiently.
  • Stricter than Linux: mmap(2) of a GEM object with MAP_PRIVATE is refused (the devmap framework maps the object's own pages; there is no copy-on-write).
  • Different from Linux by necessity: every refused mmap(2) is ENXIO, because devmap_setup(9F) maps every driver error to ENXIO (Linux: EINVAL bad offset, EACCES no handle).
  • Module shape unchanged from R0: the core stays a library linked into each driver.

The refault experiment

Works. rdrmnull's movable object (four pages) has two SysMem backings; RDRMNULL_IOCTL_MOVE switches every recorded mapping piece to the other backing with devmap_umem_remap and invalidates it with devmap_unload(9F); the object's devmap_access(9E) reloads with devmap_load(9F). Measured through the kstat faults counter in the final run:

  • devmap_unload alone: no fault until the next access; one read then faults once (7332 → 7333) and sees the same memory (0x11).
  • Move with the new backing filled with 0x22: the existing mapping reads 0x22 across all four pages, one fault per page (7333 → 7337).
  • Write 0x33, move back: the mapping reads 0x11 again (first backing unchanged).
  • Fork, then one move from the parent: 2 pieces redirected; the child's inherited mapping and the parent's both read 0x33.
  • Partial munmap with per-page patterns (page i holds 0x40+i in one backing, 0x50+i in the other): after unmapping the head and a middle page, the pieces for pages 1 and 3 show 0x41/0x43; a move while they are unsettled redirects them at the right offsets (0x51/0x53); an unload and refault keeps them; a second move remaps the now settled pieces (0x41/0x43).

Findings for R5: devmap_umem_remap has no manual page and is not in the ddi mapfile (declared in rdrmnull only; the core does not use it; R5 needs it in the private tier or documented upstream). It needs DEVMAP_ALLOW_REMAP at setup, and segdev then refuses F_SOFTLOCK on 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" is EINVAL for GEM offsets. A piece split off by munmap(2) must not be remapped until the framework has finished it (devmap_handle_reduce_len runs 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 from c4a8512060 with illumos-ddi bfc7ff7b2a, after review round 1; logs in the workspace under logs/oibldofgate/:

Step Result Log
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 rdrmnull pass 20260928-r1-local-checks.log
xtask kernel-check --emit | mdb -k | --verify pass: ddi-sys sizes 53/60, offsets 185/185, symbols 983/984 (the known ddi_ncpus_expected); devmap_callback_ctl 0x28 with devmap_map at 8 and devmap_unmap at 0x20 (all five members checked by hand), ddi_dma_cookie_t 0x18, ddi_umem_cookie_t 8 (opaque), devmap_umem_remap resolves in genunix 20260928-r1-kernel-check.log
run-r1-test.sh: link (ld -ztype=kmod), 67 imported symbols all resolved, add_drv pass 20260928-r1-rdrmnull-load-test.log
drm_r0_test (R0 regression, updated for R1 caps and GEM_CLOSE) 105 PASS, 0 FAIL same
drm_r0_probe32 / drm_r1_probe32 5 / 4 PASS, 0 FAIL same
drm_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 from nobody before/after auth, render-node EACCES, 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 release 153 PASS, 0 FAIL; repeat run identical same, 20260928-r1-rdrmnull-load-test-repeat.log
gem kstat idle at start, after every part and at the end; before unload: objects 0, umem_bytes 0, handles 0, names 0, offsets 0, mappings 0, pending 0; deferred_frees 1, drain_tasks 1 (the timer release) pass same
vmem: rdrmnull0.names and rdrmnull0.mmap in 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 16 umem_np segments on the machine are 128 KiB, a size no test object has, 15 of them from before any test), attributed to another consumer pass same
modunload refused while /dev/dri/card0 is open, succeeds after close; rem_drv -C pass same
/etc driver files (incl. path_to_inst) and /devices hash identically before and after; /dev/dri gone pass same, 20260928-r1-vm-state-{before,after}.log

::findleaks was not used: the VM runs a non-debug kernel (kmem_flags 0); leaks are covered by the kstat gauges, the vmem arenas and umem_np.

VM state left behind: /root/illumos-drm-test/r1-* (final run directory r1-c4a8512060, the pre-review run r1-c2dec98c12, seven trial directories); no driver, no /dev/dri, /etc and /devices as found. Trial 1 added "/rdrmnull@0,0" 0 "rdrmnull" to /etc/path_to_inst (root-nexus node); the file was restored from path_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 against ddi_root_node), which rebooted; the driver registration it left was removed with rem_drv, and stale devfs node attributes from that boot were cleared by rem_drv -C in a later run.

Review round 1

An Anima-bar review rejected the first version; fixed in 4932bb4 and c4a8512: 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 inside devmap_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-buffer bpp/flags and pad handling now match Linux. Kept as the reviewer asked: KArc/defer and 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

  • The sg-table is exercised only in trial 1 (root-nexus child), not in the final configuration; a real DMA-capable node (virtio-gpu, R4) will exercise it.
  • devmap_umem_remap needs 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 concurrent munmap(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.
  • The ddi-sys PROT_*/MAP_* constants are missing; the core defines PROT_ALL, MAP_TYPE and MAP_SHARED locally.

Anima work item: WI-453 01a0e387-264f-7f43-9774-e4590c12cedd

Stacked on #1 (R0): head `r1-gem`, base `r0-uapi-core`. Review and merge #1 first; this pull request then retargets to `main` unchanged. 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, with `rdrmnull` as the test driver. It depends on illumos-ddi `bfc7ff7b2a` (`drm-r0`, illumos/illumos-ddi#1), which adds the `devmap(9E)` entry point to the `CharDevice` glue. ## What was built - **Objects and references.** `drm::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` (handle count, FLINK name, fake offset, mapping list) and hands out `GemRef`s. `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 with `DDI_NOSLEEP`; an implicit drop takes the parking path. Destroying an object removes its offset, frees its memory and updates the statistics. - **Tables.** Per-file handles and per-device names are `IdMap`s: numbers from `id_space(9F)` (`id_allocff_nosleep`: from 1, lowest free, like Linux's `idr_alloc(…, 1, 0, …)`) and a `BTreeMap` behind the file's or the device's mutex. The handle/name rules of Linux's `drm_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 in `objects.rs`. Fake offsets are page ranges from a `vmem(9F)` identifier arena over Linux's 64-bit `DRM_FILE_PAGE_OFFSET_START/SIZE` (256 MiB up, 64 GiB), one per object, assigned on first `MAP_DUMB`, freed with the object. - **mmap.** `devmap(9E)` finds the object whose range starts at the offset, takes a reference and calls `devmap_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 to `devmap_default_access(9F)`. Every piece a partial `munmap(2)` leaves gets private data with its own reference, and stays *unsettled* until its next fault, because the framework adjusts its memory offset only after `devmap_unmap(9E)` returns; the fault calls `GemObject::mapped` with the final geometry, and a movable object only invalidates unsettled pieces when it moves. The reference taken in `devmap(9E)` is parked by mapping handle and thread, because the framework can free the handle without any callback when the `mmap(2)` fails later (it does for `MAP_PRIVATE`); the thread's next entry point or `close(9E)` drops it. - **System memory.** `SysMem` is `ddi_umem_alloc(9F)` with `DDI_UMEM_SLEEP` (locked; the function zero-fills), mapped cached. With `Driver::DMA_ATTR` it is bound with `ddi_dma_addr_bind_handle(9F)` against the device node; `sg_iter()` yields the cookies, which are checked to cover the object exactly. - **Ioctls.** `GEM_CLOSE` (render allowed), `GEM_FLINK` and `GEM_OPEN` (`DRM_AUTH`, so `EACCES` on render nodes), `MODE_CREATE_DUMB`, `MODE_MAP_DUMB`, `MODE_DESTROY_DUMB` (primary, no flags), as in Linux's `drm_ioctls[]`; driver-private ioctls `0x40`-`0x9f` via `Driver::IOCTLS` and `Driver::ioctl`, matched on the whole command value; 32-bit callers get `ENOTSUP` for all of them. - **Dumb buffers.** As Linux's `drm_mode_create_dumb`: any non-zero bpp up to `U32_MAX - 8` (`ceil(bpp/8)` bytes per pixel), non-zero dimensions, `flags` ignored, Linux's 32-bit size limit, `EINVAL` for every overflow; the default layout (`Driver::dumb_layout`) pads rows to 64 bytes and rounds the size to a page. `MAP_DUMB` and `GEM_CLOSE` ignore `pad`, as Linux does. `GET_CAP` now reports `DUMB_BUFFER` (with `Features::GEM | DUMB`) and the driver's `DUMB_PREFERRED_DEPTH`/`DUMB_PREFER_SHADOW`; `ADDFB2_MODIFIERS` stays 0 until R3. - **Observability.** kstat `<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. - **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 and a "move" for the refault experiment, and a reference dropped from a `timeout(9F)` callback. - **Tests.** Host: `KArc` and 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.c` plus `run-r1-test.sh`, which also runs the R0 test and checks `/etc` and `/devices` before and after. ## Decisions and deviations - **Refcount: own `KArc`, not `alloc::sync::Arc`.** Fallible construction with the context token, and `KArc::release` returns 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. - **Deferred release:** lock-free list in the object + one-shot `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). - **Offsets: `vmem(9F)`** identifier arena, as planned (`VM_NOSLEEP | VM_BESTFIT`; exhaustion is `ENOSPC`, never a wait). - **Handles: `id_space(9F)` + `BTreeMap`**, as planned. `BTreeMap` needs a global allocator: driver modules enable `ddi`'s `global-alloc-sleep` (always `KM_SLEEP`); the core inserts only with the `Sleepable` token. Handle/name ids use the `_nosleep` first-fit call because the blocking one would wait forever on an exhausted space. - **devmap glue in illumos-ddi**: `CharDevice::devmap` and `module! { ops: [char, devmap] }` (installs `ddi_devmap_segmap(9F)` and `D_DEVMAP`) on the `drm-r0` branch (`94680b671c`, `77df4ce177`, `bfc7ff7b2a`), same style as `clone_open`; the callbacks and `devmap_umem_setup` stay in the core. The glue accepts a validated length up to the page holding the end, since `mmap(2)` passes the length unrounded, and refuses 0 or a partial page (`devmap_setup(9F)` would otherwise loop). - **The callbacks find the device** through a per-module table indexed by device number (`devmap_map(9E)` gets only `dev`), with a per-object-type tag checked on lookup; one DRM driver per module. - **`DDI_UMEM_PAGELOCK` does not exist**; non-pageable (`DDI_UMEM_SLEEP` without `DDI_UMEM_PAGEABLE`) is the locked default. `ddi_umem_alloc` zero-fills (`bzero` in `vm/seg_dev.c`), verified in the test. - **DMA in rdrmnull: none.** The pseudo nexus refuses DMA handles. Trial 1 ran rdrmnull as a root-nexus child: every object was bound (300 cookies for a 1,228,800-byte buffer, sizes summing up), but such a node leaves its instance in `/etc/path_to_inst` after `rem_drv` (restored by hand). Trial 2 bound against `ddi_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 with `DMA_ATTR = None`; the sg path is exercised in trial 1 only. - **Memory limits** (new, not in the work item): object memory is locked kernel memory, so `Driver::MAX_OBJECT_SIZE` (256 MiB default) and `Driver::MAX_MEMORY` (1 GiB per device default) refuse with `ENOMEM`. The device budget is charged before the allocation (`GemCore::reserve` / `create_reserved`), so concurrent requests cannot exceed it transiently. - **Stricter than Linux:** `mmap(2)` of a GEM object with `MAP_PRIVATE` is refused (the devmap framework maps the object's own pages; there is no copy-on-write). - **Different from Linux by necessity:** every refused `mmap(2)` is `ENXIO`, because `devmap_setup(9F)` maps every driver error to `ENXIO` (Linux: `EINVAL` bad offset, `EACCES` no handle). - **Module shape unchanged from R0**: the core stays a library linked into each driver. ## The refault experiment Works. rdrmnull's movable object (four pages) has two `SysMem` backings; `RDRMNULL_IOCTL_MOVE` switches every recorded mapping piece to the other backing with `devmap_umem_remap` and invalidates it with `devmap_unload(9F)`; the object's `devmap_access(9E)` reloads with `devmap_load(9F)`. Measured through the kstat `faults` counter in the final run: - `devmap_unload` alone: no fault until the next access; one read then faults once (7332 → 7333) and sees the same memory (0x11). - Move with the new backing filled with 0x22: the existing mapping reads 0x22 across all four pages, one fault per page (7333 → 7337). - Write 0x33, move back: the mapping reads 0x11 again (first backing unchanged). - Fork, then one move from the parent: 2 pieces redirected; the child's inherited mapping and the parent's both read 0x33. - Partial munmap with per-page patterns (page i holds 0x40+i in one backing, 0x50+i in the other): after unmapping the head and a middle page, the pieces for pages 1 and 3 show 0x41/0x43; a move while they are unsettled redirects them at the right offsets (0x51/0x53); an unload and refault keeps them; a second move remaps the now settled pieces (0x41/0x43). Findings for R5: `devmap_umem_remap` has no manual page and is not in the `ddi` mapfile (declared in rdrmnull only; the core does not use it; R5 needs it in the private tier or documented upstream). It needs `DEVMAP_ALLOW_REMAP` at setup, and segdev then refuses `F_SOFTLOCK` on 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" is `EINVAL` for GEM offsets. A piece split off by `munmap(2)` must not be remapped until the framework has finished it (`devmap_handle_reduce_len` runs 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 from `c4a8512060` with illumos-ddi `bfc7ff7b2a`, after review round 1; logs in the workspace under `logs/oibldofgate/`: | Step | Result | Log | | --- | --- | --- | | `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 rdrmnull` | pass | `20260928-r1-local-checks.log` | | `xtask kernel-check --emit \| mdb -k \| --verify` | pass: ddi-sys sizes 53/60, offsets 185/185, symbols 983/984 (the known `ddi_ncpus_expected`); `devmap_callback_ctl` 0x28 with `devmap_map` at 8 and `devmap_unmap` at 0x20 (all five members checked by hand), `ddi_dma_cookie_t` 0x18, `ddi_umem_cookie_t` 8 (opaque), `devmap_umem_remap` resolves in genunix | `20260928-r1-kernel-check.log` | | `run-r1-test.sh`: link (`ld -ztype=kmod`), 67 imported symbols all resolved, `add_drv` | pass | `20260928-r1-rdrmnull-load-test.log` | | `drm_r0_test` (R0 regression, updated for R1 caps and `GEM_CLOSE`) | 105 PASS, 0 FAIL | same | | `drm_r0_probe32` / `drm_r1_probe32` | 5 / 4 PASS, 0 FAIL | same | | `drm_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 from `nobody` before/after auth, render-node `EACCES`, 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 release | 153 PASS, 0 FAIL; repeat run identical | same, `20260928-r1-rdrmnull-load-test-repeat.log` | | gem kstat idle at start, after every part and at the end; before unload: objects 0, umem_bytes 0, handles 0, names 0, offsets 0, mappings 0, pending 0; deferred_frees 1, drain_tasks 1 (the timer release) | pass | same | | vmem: `rdrmnull0.names` and `rdrmnull0.mmap` in 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 16 `umem_np` segments on the machine are 128 KiB, a size no test object has, 15 of them from before any test), attributed to another consumer | pass | same | | `modunload` refused while `/dev/dri/card0` is open, succeeds after close; `rem_drv -C` | pass | same | | `/etc` driver files (incl. `path_to_inst`) and `/devices` hash identically before and after; `/dev/dri` gone | pass | same, `20260928-r1-vm-state-{before,after}.log` | `::findleaks` was not used: the VM runs a non-debug kernel (`kmem_flags` 0); leaks are covered by the kstat gauges, the vmem arenas and `umem_np`. VM state left behind: `/root/illumos-drm-test/r1-*` (final run directory `r1-c4a8512060`, the pre-review run `r1-c2dec98c12`, seven trial directories); no driver, no `/dev/dri`, `/etc` and `/devices` as found. Trial 1 added `"/rdrmnull@0,0" 0 "rdrmnull"` to `/etc/path_to_inst` (root-nexus node); the file was restored from `path_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 against `ddi_root_node`), which rebooted; the driver registration it left was removed with `rem_drv`, and stale devfs node attributes from that boot were cleared by `rem_drv -C` in a later run. ## Review round 1 An Anima-bar review rejected the first version; fixed in `4932bb4` and `c4a8512`: 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 inside `devmap_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-buffer `bpp`/`flags` and `pad` handling now match Linux. Kept as the reviewer asked: `KArc`/`defer` and 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 - The sg-table is exercised only in trial 1 (root-nexus child), not in the final configuration; a real DMA-capable node (virtio-gpu, R4) will exercise it. - `devmap_umem_remap` needs 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 concurrent `munmap(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. - The ddi-sys `PROT_*`/`MAP_*` constants are missing; the core defines `PROT_ALL`, `MAP_TYPE` and `MAP_SHARED` locally. Anima work item: WI-453 `01a0e387-264f-7f43-9774-e4590c12cedd`
GEM objects are shared between handles, names, mappings and in-flight
work, and their last reference can be dropped in interrupt context (a
fence callback, from R2 on), where releasing their memory must not
happen. Two pieces make that possible without allocating at release
time:

* karc::KArc is a counted reference on the kernel heap. Construction is
  fallible and takes the context decision from the caller (KM_SLEEP with
  the Sleepable token, KM_NOSLEEP without). KArc::release hands the last
  reference's value back in place (KArcLast) instead of dropping it, and
  KArc::upgrade takes a reference from a bare pointer unless the count is
  zero (kref_get_unless_zero), for tables that must not keep their
  entries alive. The count saturates rather than overflows.

* defer::DeferList is a lock-free list linked through the parked objects
  themselves; defer::put destroys the value in thread context and parks
  it in interrupt context, for a task queue to drain.

The raw kmem allocation that KVec used moves to kmem.rs so that KArc
shares it. Host tests drive KArc from eight threads, and the deferral
rule from eight threads of which half pretend to be in interrupt context
while a drainer runs: every object is destroyed exactly once and never
in (simulated) interrupt context.
The 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.
drm_r1_test.c exercises GEM and dumb buffers through the installed
libdrm and libkstat, as root against rdrmnull: capabilities, the
argument checks and limits of MODE_CREATE_DUMB, MAP_DUMB and mmap(2) of
a dumb buffer (zero-filled, pattern written and read back, a second and
a short mapping, the refused mappings), fork (inherited mapping and a
new mapping in the child, writes visible both ways), partial munmap,
GEM_FLINK/GEM_OPEN from a separate root process and from an
unprivileged one before and after authentication, render-node
permissions, a file without a handle being unable to map an object,
the name disappearing with the last handle while a mapping keeps the
object, close(2) releasing a file's objects, eight threads of GEM
ioctls and mappings, the refault experiment on rdrmnull's movable
objects, and a reference dropped in interrupt context. The rdrmnull:0:gem
kstat must be idle at the start, after each part and at the end. A
32-bit probe checks that GEM, dumb and driver ioctls are ENOTSUP for
ILP32 callers.

run-r1-test.sh follows run-r0-test.sh: it links the module, checks its
imports against the running kernel, builds both tests and probes, loads
the module, runs R0 and R1, records the kstat and the device's vmem
arenas, checks the unload refusal while a file is open, and removes
everything with rem_drv -C. It records the driver files in /etc and the
/devices tree before and after and fails unless they are unchanged.

The R0 test now expects the R1 behaviour: DUMB_BUFFER 1 with rdrmnull's
depth and shadow preferences, and GEM_CLOSE of handle 0 EINVAL instead
of ENOTTY.
Review fixes for the R1 core:

* devmap_unmap(9E): the framework keeps every piece a partial munmap
  leaves (the old handle for the head, a copy that still points at the
  old private data for the tail), so each piece now always gets private
  data with its own object reference, even when it cannot be listed.
  Before, a failed record left the piece pointing at freed private data
  and holding no reference.

* Split pieces start unsettled: the framework adds the unmapped length to
  a piece's memory offset (devmap_handle_reduce_len) only after the
  callback returns, so remapping the piece inside the callback, or from a
  concurrent move, would shift it twice. GemObject::mapped no longer runs
  for split pieces in devmap_unmap(9E); the piece's next fault, which
  holds the address-space lock the munmap held, settles it with the final
  geometry. Mapping::is_settled tells a movable object to invalidate, not
  remap, an unsettled piece; rdrmnull's move does so.

* The device memory budget is charged before the locked allocation
  (GemCore::reserve, GemCore::create_reserved), so concurrent CREATE_DUMB
  calls cannot exceed MAX_MEMORY even transiently.

* The registry sets a device's type tag only after it has claimed the
  slot.

* Linux compatibility: MODE_CREATE_DUMB accepts any non-zero bpp up to
  U32_MAX - 8 (ceil(bpp / 8) bytes per pixel) and ignores flags;
  MODE_MAP_DUMB and GEM_CLOSE ignore pad, as drm_mode_create_dumb,
  drm_mode_mmap_dumb_ioctl and drm_gem_close_ioctl do.

illumos-ddi moves to bfc7ff7b2a, whose devmap(9E) glue refuses a
validated length of zero or of a partial page instead of letting
devmap_setup(9F) loop.
Test partial munmap of movable mappings and the Linux-compatible dumb rules
All checks were successful
ci / check-hosted (pull_request) Successful in 4m32s
ci / kmod-illumos (pull_request) Successful in 4m51s
c4a8512060
The refault experiment now uses a four-page movable object with a
per-page pattern in each backing, unmaps the head and a middle page,
and checks that the remaining pieces show their own pages after the
splits, follow a move while unsettled, keep their offsets across an
unload and refault, and are remapped at the right offsets by a second
move. The dumb-buffer checks expect Linux's behaviour for bpp, flags and
pad. The README describes split pieces, the reservation of the memory
budget and the Linux-compatible argument handling.
forge changed target branch from r0-uapi-core to main 2026-09-28 18:11:26 +00:00
forge merged commit 81828e6a65 into main 2026-09-28 18:11:35 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
illumos/illumos-drm!2
No description provided.