Skip to content

[rlc-9/5.14.0-687.15.1.el9_8] crypto: rng - Fix spurious EFAULT in the FIPS per-CPU DRBG#1371

Merged
kerneltoast merged 3 commits into
rlc-9/5.14.0-687.15.1.el9_8from
{sultan}_rlc-9/5.14.0-687.15.1.el9_8_rng-fixes
Jun 23, 2026
Merged

[rlc-9/5.14.0-687.15.1.el9_8] crypto: rng - Fix spurious EFAULT in the FIPS per-CPU DRBG#1371
kerneltoast merged 3 commits into
rlc-9/5.14.0-687.15.1.el9_8from
{sultan}_rlc-9/5.14.0-687.15.1.el9_8_rng-fixes

Conversation

@kerneltoast

Copy link
Copy Markdown
Collaborator

JIRA: GOOGLE-52

These are the FIPS-mode fixes for the fast per-CPU DRBG that backs /dev/random, /dev/urandom, and getrandom() under fips=1.

The main fix is for a spurious -EFAULT: the optimized read path copies into the user buffer with copy_to_user_nofault() under the RNG lock, but a GUP-pinned page doesn't keep its PTE pinned, so the destination can be zapped by reclaim or madvise(MADV_DONTNEED) and the nofault copy fails. It now falls back to a faultable copy through an on-stack bounce buffer instead of bailing out. The second fix avoids a spurious -EFAULT when the iovec leads with a zero-length segment, reachable via readv() on /dev/urandom. The mm/gup change reverts the pin_user_pages_fast_only() reintroduction since the EFAULT fix drops GUP pinning entirely.

Testing: built under KASAN (+KASAN_STACK) with fips=1 and hammered the RNG read path with:

  • the leading zero-length iovec reproducer on both /dev/random and /dev/urandom
  • a getrandom()/madvise(MADV_DONTNEED) PTE-zap race, 16+16 threads, ~1.88M calls
  • a 32-thread mixed read()/readv() workload with 1 MiB buffers, ~108 GiB
  • a 48-thread concurrent run

Zero KASAN reports, and the kernel stayed untainted.

This PR was opened with the assistance of Claude (Opus 4.8).

The fast per-CPU DRBG path computes its initial user destination address
straight from the iov_iter. For an ITER_IOVEC iter it reads iter_iov_addr()
and iter_iov_len() of the current segment, but when the iovec leads with
one or more zero-length segments, the current segment is one of those empty
entries. iter_iov_addr() then hands back the base of an empty segment,
which is whatever userspace put there: its base can be NULL or some other
unwritable address, since a zero-length segment is never actually touched.

Right after the setup, that address is prefaulted, and on a bogus base it
fails. A failed prefault on the very first address is treated as fatal, so
the whole read bails out to -EFAULT even though there are perfectly good
non-empty segments later in the iovec. This is reachable with something as
simple as readv() on /dev/urandom where the first iovec entry is {NULL, 0}.

Fix it by advancing the iterator by zero before reading the first address.
The iovec advance loop walks past every leading empty segment and stops at
the first non-empty one, and there's guaranteed to be such a segment
because iov_iter_count() is nonzero at this point. Empty segments that crop
up mid-stream are already skipped by the per-copy advance, so this only
needs to run once during setup.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Sultan Alsawaf <sultan@ciq.com>
While GUP pinning makes it possible to pin the page _backing_ a user
address, it *doesn't* pin the page table entry (PTE) for that mapping. This
means the pinned physical page can be separated from the user address it
was backing, and even back a _different_ user address within the same
process. PTE zapping naturally happens during memory reclaim when memory
pressure is elevated, and can even be done directly by userspace via
madvise(MADV_DONTNEED).

Since the optimized per-CPU DRBG loop assumes copy_to_user_nofault() will
always succeed on a GUP-pinned page, it immediately bails out when the
nofault copy actually *does* fail for the reasons described above. This
results in either fewer than requested random bytes copied or, more
seriously, a spurious EFAULT returned to userspace when no random bytes
were copied.

As it turns out, there's no way to pin a PTE. That means it's not possible
to guarantee a 100% success rate for the copy_to_user_nofault() attempt.

Fix this by handling copy_to_user_nofault() errors correctly with a fall
back to a faultable copy attempt outside of the RNG lock. In order to
guarantee forward progress for the caller, an on-stack bounce buffer is
used to copy up to 256 bytes of the generated random bytes whenever this
happens rather than discarding the whole thing.

There's no need to use GUP pinning anymore since there's no use for having
a page pinned without pinning a PTE to go along with that page, hence the
page pinning is eliminated which saves a software page table walk that was
performed for _at least_ every destination page.

Reported-by: Kun Yi <kunyi@google.com>
Signed-off-by: Sultan Alsawaf <sultan@ciq.com>
This reverts commit ef467f3.

This helper is no longer used by the FIPS-mode RNG, which was the
motivation for reintroducing it. Remove it.

Signed-off-by: Sultan Alsawaf <sultan@ciq.com>
@kerneltoast kerneltoast requested a review from a team June 23, 2026 22:06
@github-actions

Copy link
Copy Markdown

🤖 Validation Checks In Progress Workflow run: https://github.com/ctrliq/kernel-src-tree/actions/runs/28060404046

@github-actions

Copy link
Copy Markdown

Validation checks completed successfully View full results: https://github.com/ctrliq/kernel-src-tree/actions/runs/28060404046

@bmastbergen bmastbergen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥌

@bmastbergen bmastbergen requested a review from a team June 23, 2026 22:51
@kerneltoast kerneltoast merged commit eacd8fe into rlc-9/5.14.0-687.15.1.el9_8 Jun 23, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants