Skip to content

Harden IPU7 PSYS userptr pinning - #264

Draft
AFOliveira wants to merge 2 commits into
omacom:masterfrom
AFOliveira:codex/harden-ipu7-userptr
Draft

Harden IPU7 PSYS userptr pinning#264
AFOliveira wants to merge 2 commits into
omacom:masterfrom
AFOliveira:codex/harden-ipu7-userptr

Conversation

@AFOliveira

@AFOliveira AFOliveira commented Sep 1, 2026

Copy link
Copy Markdown

What

  • Validate IPU7 PSYS userptr lengths and address arithmetic before calculating or allocating the page array.
  • Use overflow-safe allocation, remove FOLL_FORCE, require a complete long-term pin, clean up partial pins, and publish attachment state only after SG creation succeeds.
  • Clear page, page-count, and scatter-gather attachment state after release.
  • Replace world-writable PSYS and runtime-directory permissions with video/seat-scoped access.
  • Regenerate the driver patch with normal context and record that it has not yet been submitted upstream.
  • Run the boundary regression, Linux checkpatch.pl, and an actual DKMS module build from check().
  • Warn and continue package lifecycle work if immediate permission refresh fails; reboot then applies the packaged rules.

Why

The original driver narrows an attacker-controlled 64-bit length into signed page-count and allocation-size integers. A crafted request can therefore allocate space for one page pointer before pin_user_pages_fast() writes multiple pointers into that allocation. The package also exposed the PSYS device as mode 0666, making the ioctl reachable by every local account on affected hardware.

Review follow-up

  • Every driver hunk now carries three lines of context; a deliberate anchor mutation makes application with --fuzz=0 fail.
  • The package check compiles the patched driver and verifies intel-ipu7-psys.ko, rather than relying only on source greps and a duplicate arithmetic model.
  • Scriptlet permission failures no longer skip daemon reload, service migration, enablement, or the reboot warning.
  • The existing repeated-buffer pin/OOM possibility is an availability-hardening issue and remains outside this focused memory-corruption fix.

Verification

  • The safe AddressSanitizer model reproduces the original 8-byte allocation followed by a page-pointer heap overflow.
  • Both package patches apply to the pinned Intel source with --fuzz=0.
  • Linux checkpatch.pl --no-tree --strict --no-signoff: 0 errors and 0 warnings.
  • The exact package check() flow passes its ordinary, oversized, wrapping, and zero-length boundary cases.
  • All six patched DKMS modules, including intel-ipu7-psys.ko, build successfully against Arch Linux 7.1.9 headers.
  • PR linux-ptl: rebase to 7.2.2.arch1, carry all patches + Xe opportunistic compaction #271 does not publish its proposed linux-ptl 7.2.2 headers as an artifact, so compatibility with that unmerged kernel remains unverified.
  • Source-array lengths and all non-VCS SHA-256 entries match.
  • PKGBUILD and scriptlet Bash syntax checks pass.
  • A mocked permission-refresh failure confirms package lifecycle operations continue.

The audit host has no IPU7 hardware, so the real ioctl was not executed against a live device/KASAN kernel. The finding is a source-confirmed kernel memory-safety defect, not a demonstrated end-to-end root exploit.

Deployment

The upgrade immediately attempts to remove world access from an existing device. If permission refresh fails, the scriptlet emits a reboot-before-use warning and continues the remaining lifecycle work. If intel_ipu7_psys is already loaded, reboot is required to replace the running module with the fixed build.

@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed. The bug you are closing is real and worse than the PR body claims, the fix is correct against the pinned source, and there are two packaging problems worth addressing before this lands.

What was actually checked. I read the patch against the real driver source at the commit this PKGBUILD pins (intel/ipu7-drivers @ a88b1909), applied it by hand, and verified every declared sha256sum in the PKGBUILD against the file in the tree — all 19 non-SKIP entries match, and the source/sums arrays are both 24 entries. I did not build this package: intel-ipu7-camera compiles a large HAL stack and the DKMS modules are compiled on the user's machine, so nothing here was proved by a build. Everything below is from reading source, not from running it.

What class of bug this closes. attach->len is u64 (ipu7-psys.h:153), while the original npages and array_size are signed 32-bit int (ipu-psys.c:54). array_size = npages * sizeof(struct page *) is computed as size_t and then narrowed back into int — so 0x20000001 pages need 0x100000008 bytes and array_size becomes 8. kvzalloc(8) gets room for one pointer, and pin_user_pages_fast() then writes struct page * values into it. That is a kernel heap out-of-bounds write.

The part worth being clear about: this does not require the attacker to own 2 TiB of memory. GUP writes pointers as it pins and returns a short count, and the old if (nr < npages) guard at ipu-psys.c:87 runs after those writes. So a caller submits a page-aligned GETBUF with a length of 0x20000001 * 4096 — that length is just a number — needs only two writable pages at the start followed by a hole, and pages[1] has already overrun the 8-byte allocation by the time the short-count check is reached. Combined with the old MODE="0666" on the PSYS node, that was reachable by any local account on an ordinary laptop. I had initially read this as needing an implausible amount of pinnable memory; codex worked out the short-count mechanism and it is right.

Which is also why, of the two halves of this PR, the udev and tmpfiles change is doing at least as much work as the C: 0666GROUP="video", MODE="0660", TAG+="uaccess" and /run/camera 07770770 root:video is what takes this off the "any local user" list.

The fix itself reads correct. Applied against the pinned source it produces the expected function: length validated before any arithmetic, check_add_overflow on start + len - 1, npages kept in size_t, kvmalloc_array instead of a pre-narrowed byte count, FOLL_FORCE dropped, a short pin rejected with the partial pin unwound, and attach->pages/npages/sgt published only after sg_alloc_table_from_pages succeeds. long nr losing its = 0 initialiser is safe — every path to unpin_pages: is after the pin_user_pages_fast() assignment, and the allocation-failure path jumps past it to free_sgt. MAX_RW_COUNT is INT_MAX & PAGE_MASK from <linux/fs.h>, which this file includes directly at line 9.

One thing I suspected and could not make stick, recorded so nobody re-treads it: ipu7_psys_put_userpages() (ipu-psys.c:118) still leaves attach->pages and attach->npages set after kvfree, and ipu7_dma_buf_vmap()/vunmap() test exactly those fields. Codex traced the lifecycle and found no reachable use-after-free — detach frees the private struct immediately after put_userpages(), vunmap happens before detach, and attach->sgt = NULL short-circuits a repeat call. Clearing all three would still be cheap hygiene while you are in this function.

Two packaging problems:

  1. The patch is diff -U0, and three of its hunks are pure insertions@@ -15,0 +16 @@, @@ -98,0 +101,2 @@ and @@ -579,0 +581,5 @@ (patch lines 18, 82, 94). A hunk with no context and no removed lines gives patch(1) nothing to verify: it inserts at a line number and reports success either way. Today it is fine — 0004 only touches old line 1539, which is below 0005's highest anchor at 579, so nothing shifts — but the +581 hunk is the exposed one, with no anchoring hunk between old lines 108 and 579. If the pinned commit moves or 0004 grows a hunk higher in the file, that five-line splice lands somewhere else and still "applies". Regenerating this with normal context (-U3) costs nothing and removes the whole class.

    Related: the @@ -579,0 +581,5 @@ hunk opens a brace it does not close, and relies on the original lines 580-581 to close it. It is correct against the pinned source — I worked it through and so did codex — but it is very hard to review, and that is a bad property for a security patch.

  2. check() cannot catch that. PKGBUILD:162-175 greps for two strings, asserts FOLL_FORCE is absent, and compiles check-userptr-range.c — which re-implements the arithmetic rather than testing the driver. It would pass a misplaced #include <linux/overflow.h> or a attach->pages = pages; spliced into the wrong block, because the grepped strings would still be present. A syntax-damaging splice would first show up when DKMS builds on a user's machine, leaving them without a camera. The helper is a fine unit test of the formula; it just is not evidence about the shipped module.

intel-ipu7-camera.install. _apply_runtime_permissions || return 1 in post_install/post_upgrade does not do what it looks like it does. A non-zero return from a pacman scriptlet does not roll back or fail the transaction — libalpm runs scriptlets after committing the package and only logs the failure. So if systemd-tmpfiles --create or the udev reload fails, the package is installed but everything after that line is skipped: daemon-reload, the camera-init/v4l2-relayd service migration, systemctl enable intel-ipu7-camera.service, the reboot warning, and the per-user config. That is a worse state than the one it is guarding against, and it buys no fail-closed guarantee. Warning and continuing would be safer. (The container/chroot case is already handled — line 28 skips udevadm unless /run/udev/control exists.)

Permissions check out. 70- is early enough: it runs before systemd's 73-seat-late.rules, so TAG+="uaccess" is set when logind assigns the seat ACL. Nothing in the package needs the old permissions — camera-init.service, intel-ipu7-camera.service and the upstream v4l2-relayd@.service all run as root with no User=/Group=, the sleep hook runs in system context, and no other consumer of /run/camera assumes 0777.

Not addressed, and pre-existing: there is still no aggregate pin limit or locked-memory accounting. Each MAPBUF retains another long-term-pinned range in fh->bufmap, so a video-group or active-seat user can pin repeatedly until OOM. Availability rather than corruption, and older than this PR, but it sits squarely under "harden userptr pinning" if you want it in scope.

On PR #271 (linux-ptl → 7.2.2.arch1): no file overlap — #271 touches only pkgbuilds/linux-ptl/, and its "carries all patches" refers to linux-ptl's own series, not this one. The interaction is that this patch is verified against 7.1.9 only. These are DKMS modules, so a 7.2.2 incompatibility surfaces as a build failure on users' machines at kernel upgrade rather than in CI. I could not test it — linux-ptl in the repo is still 7.1.8, so 7.2.2 headers were not available.

An upstream backport should name its commit, and this one does not. The patch header (From 0000000…, Subject: [PATCH] media: intel/ipu7: harden PSYS userptr pinning) is authored here rather than cherry-picked, with no Upstream-Status or lore link. If this has been sent to linux-media, say so in the file; if it has not, saying that is just as useful, because it tells the next person whether to expect it to disappear at the next rebase.

Second opinion: codex at xhigh reviewed this independently. It contributed the short-count exploitation mechanism above, which changed my conclusion about severity; it disproved the use-after-free I suspected in put_userpages(); and it established the pacman scriptlet semantics and the service-unit ownership in the permissions section. Where we agree — the zero-context insertion hunks, the check() coverage gap, the nr initialisation being safe — its independence is not currently guaranteed, so read that as two readings rather than two proofs.

Nothing was pushed to this branch: the patch format and the install-script behaviour are both your calls to make rather than mine to make for you. Waiting on the author.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants