Visitar URL original
uffd: refactor pagefault handling to support multiple uffd fd by lucido-simon · Pull Request #9017 · cloud-hypervisor/cloud-hypervisor · GitHub
Skip to content

uffd: refactor pagefault handling to support multiple uffd fd - #9017

Open
lucido-simon wants to merge 6 commits into
cloud-hypervisor:mainfrom
lucido-simon:accept_multiple_uffd_fd
Open

lucido-simon wants to merge 6 commits into
cloud-hypervisor:mainfrom
lucido-simon:accept_multiple_uffd_fd

Conversation

@lucido-simon

Copy link
Copy Markdown
Contributor

As part of fixing #8967, we need to be able to handle pagefaults from a different userfault fds during postcopy: the VMM will always have its own userfaultfd, but we should also have a userfaultfd for each vhost-user backend.

This PR makes the necessary changes in the userfaultfd registering/handling code so that we do support mulitple userfaultfds. For now, only the VMM registers its userfaultfd still. There should be no behavior changes.

@lucido-simon
lucido-simon requested a review from a team as a code owner October 6, 2026 13:27
@lucido-simon

Copy link
Copy Markdown
Contributor Author

cc @sboeuf @phip1611

@lucido-simon
lucido-simon force-pushed the accept_multiple_uffd_fd branch from 0b10ded to 7ae80f5 Compare October 6, 2026 13:29
@phip1611
phip1611 self-requested a review October 6, 2026 13:41
@phip1611

phip1611 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

nice! do you need/want this in the v54 release? Otherwise, I'd work towards releasing v54 by the end of the week (#9010) and only merge this week what we either really need or what is cheap/trivial

@lucido-simon

Copy link
Copy Markdown
Contributor Author

nice! do you need/want this in the v54 release? Otherwise, I'd work towards releasing v54 by the end of the week (#9010) and only merge this week what we either really need or what is cheap/trivial

I don't think we'll be able to review/land every piece that's needed to fix #8967 by EOW. I'll work on a commit that refuses postcopy with vhost-user/vfio-user devices fullstop, which would be great to merge before v54.

@sboeuf

sboeuf commented Oct 6, 2026

Copy link
Copy Markdown
Member

nice! do you need/want this in the v54 release? Otherwise, I'd work towards releasing v54 by the end of the week (#9010) and only merge this week what we either really need or what is cheap/trivial

I don't think we'll be able to review/land every piece that's needed to fix #8967 by EOW. I'll work on a commit that refuses postcopy with vhost-user/vfio-user devices fullstop, which would be great to merge before v54.

Yeah no there's no rush, this is part of a broader fix which will need multiple patches and that will depend on the vhost crate update as well.

@lucido-simon
lucido-simon force-pushed the accept_multiple_uffd_fd branch from 7ae80f5 to f11cf7d Compare October 7, 2026 14:10
Comment thread vmm/src/memory_manager.rs
Comment thread vmm/src/uffd.rs Outdated
/// A userfaultfd and the base addresses of the ranges registered in it.
pub(crate) struct Uffd {
fd: OwnedFd,
base_addrs: Vec<u64>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand you could have registered multiple ranges on the same userfault FD, but how do you know which range corresponds to which base given the ranges are stored through a different structure?
I can see there's the locate() method but it's quite hard to read and I'm wondering if we could have the Uffd struct declare a field ranges?

The point I'm trying to make is that decoupling the base addresses from their associated ranges feels very unnatural here.

Comment thread vmm/src/uffd.rs Outdated
Comment on lines +283 to +294
pub(crate) fn new(fd: OwnedFd, base_addrs: Vec<u64>) -> Result<Self, Error> {
// SAFETY: `F_GETFL` takes no argument.
let flags = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFL) };
if flags < 0 {
return Err(Error::last_os_error());
}
// SAFETY: `F_SETFL` takes an integer argument, no pointer.
if unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
return Err(Error::last_os_error());
}
Ok(Self { fd, base_addrs })
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks like it's a new behavior here. I'm not opposed to the addition of flock here but shouldn't this be in a separate commit?

Comment thread vmm/src/memory_manager.rs
Comment thread vmm/src/memory_manager.rs Outdated
Comment on lines +1518 to +1531
if idx != 0 && source.requires_uffd_minor_mode() {
uffd::uffd_continue(
uffds.fd(0),
uffds.page_addr(0, range_idx, page_idx),
range.page_size,
)
.or_else(|e| {
if e.raw_os_error() == Some(libc::EEXIST) {
Ok(())
} else {
Err(e)
}
})?;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here, please dedicate a commit to this since I think that deserves it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Comment thread vmm/src/memory_manager.rs
@lucido-simon
lucido-simon force-pushed the accept_multiple_uffd_fd branch from f11cf7d to 8e9ddd4 Compare October 8, 2026 17:40
@lucido-simon
lucido-simon requested a review from sboeuf October 8, 2026 17:42

@phip1611 phip1611 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this! Left a few remarks

Comment thread vmm/src/uffd.rs

/// A userfaultfd and the ranges registered in it, at the addresses they are
/// mapped at in the process owning it.
pub(crate) struct Uffd {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ultra nit: Would UfFd or UfFdWithMeta be a better name?

feel free to ignore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not a fan of UfFdWithMeta. And in general all other structures have the prefix Uffd, so I'd keep Uffd name here.

Comment thread vmm/src/uffd.rs Outdated
/// mapped at in the process owning it.
pub(crate) struct Uffd {
fd: OwnedFd,
ranges: Vec<UffdRange>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can't this be Box<[UffdRange]>? Can the vector grow or is it fixed after construction. If it is fixed, please refactor and remove the Vector. It is easier to reason about code when one sees it does't grow/shrink over time

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Even if you're correct, it feels really going against the Rust language to use Box<[UffdRange]> instead of Vec<UffdRange>.

Comment thread vmm/src/memory_manager.rs
}

Ok(Some(Uffd::new(uffd_fd, handler_ranges)))
Ok(Some(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not about this line:

from commit message:

The VMM creates its userfaultfd with O_NONBLOCK, but we're working
towards serving userfaultfds created by other processes (vhost-user
backends)

could a vhost-user process set this FD again to blocking and thus influence the VMM? Or could CH this way break vhost-user backends in unexpected ways?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

could a vhost-user process set this FD again to blocking and thus influence the VMM?

Yes, they could. I don't think there's anything we can do against that though.

Or could CH this way break vhost-user backends in unexpected ways?

Technically maybe, but a vhost-user backend is really not supposed to do anything with its copy of the userfaultfd. I'll be extremely surprised if it does. However, I can't expect the unexpected 😅 .

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@phip1611 I think you always consider the vhost-user with the same level of trust you consider the VMM, right? So I don't think there's a security issue that could be related to this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

could a vhost-user process set this FD again to blocking and thus influence the VMM?

Yes, they could. I don't think there's anything we can do against that though.

Thinking about it, we could, on EPOLLERR, set O_NOBLOCK flag again on the fd, since this is what the kernel returns if the fd is not NOBLOCK. However, I would say that this is some edge case we don't want to handle to avoid bloating the code even more.

Comment thread vmm/src/memory_manager.rs
}
return Err(err);
}
if n == 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

from commit message:

a userfaultfd never reports
EPOLLHUP, nor does it return EOF,

why is that? Could we link to some manpage or kernel documentation (in the commit message)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There's no manpage or kernel documentation about that, that info comes from the kernel source code itself. Updated the commit message with links to the relevant function, they're easy to read.

Comment thread vmm/src/memory_manager.rs Outdated
#[expect(clippy::needless_pass_by_value)]
fn uffd_handler_loop(
uffd: Uffd,
uffds: Vec<Uffd>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this can be &[Uffd] or Box<[Uffd]> - I'd really like to see the vec go away if possible

Comment thread vmm/src/memory_manager.rs Outdated
// preemptively fault that page in the guest, as it's very
// likely it's going to access it soon, causing minor fault
// if it's enabled.
if idx != 0 && source.requires_uffd_minor_mode() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the idx != 0 seems magic to me - why is that?

Perhaps

let we_can_do_foo = idx != 0;
if we_can_do_foo && source....() {}

The UFFD handler takes a userfaultfd and the ranges registered in it as
two separate arguments. We're working towards serving more than one
userfaultfd (one for each vhost-user backend), each registering the
same guest memory at different addresses in its own process.

Group the fd and its ranges in a new Uffd struct. Each userfaultfd will
own its copy of the ranges, with host addresses matching its own address
space, so a fault can be located in the ranges of the userfaultfd it
came from.

No functional change.

Signed-off-by: Simon Lucido <simonlucido@meta.com>
The UFFD handler polls its userfaultfd with epoll and expects read() to
return EAGAIN rather than block when another thread already consumed
the message.

The VMM creates its userfaultfd with O_NONBLOCK, but we're working
towards serving userfaultfds created by other processes (vhost-user
backends), which may not set it. Set O_NONBLOCK on every userfaultfd
when building a Uffd so the handler never blocks on read().

No functional change.

Signed-off-by: Simon Lucido <simonlucido@meta.com>
The UFFD handler exits when its userfaultfd reports EPOLLHUP, or when
read() returns 0. Neither can happen: a userfaultfd never reports
EPOLLHUP, nor does it return EOF, even after the process that faulted
through it exited.

See userfaultfd_poll() and userfaultfd_read_iter() in fs/userfaultfd.c:
https://github.com/torvalds/linux/blob/038d61fd642278bab63ee8ef722c50d10ab01e8f/fs/userfaultfd.c#L915-L949
https://github.com/torvalds/linux/blob/038d61fd642278bab63ee8ef722c50d10ab01e8f/fs/userfaultfd.c#L1134-L1163

Remove both checks, which simplifies the event loop before.

Signed-off-by: Simon Lucido <simonlucido@meta.com>
Handle userfaults coming from multiple userfaultfds. To do so, register
all the userfaultfds with epoll, then add some logic to fault the
correct userfaultfd.

Page tracking stays per guest page and is shared across userfaultfds.

Signed-off-by: Simon Lucido <simonlucido@meta.com>
When a backend have a missing fault on a guest page, the guest is very
likely to access that page soon after, and take a minor fault we could
have foreseen, so, instead, once a page is resolved through a backend
userfaultfd and minor faults are enabled, map it in the VMM's
userfaultfd right away with UFFDIO_CONTINUE.

Signed-off-by: Simon Lucido <simonlucido@meta.com>
A missing fault on a page that has already been served is handled as a
if it was discarded, and the page is fetched from the source again.

With a single userfaultfd this is the only way such a fault can happen:
resolving a page wakes any other waiter that's waiting on it and drops
their queued messages.

However, a backend faulting on the same page than the VMM while it
resolves it still has its message queued afterwards. It's then handled,
and if it's a MISSING, it fetches the page again and overwrites whatever
was written to the page in between.

Tell the two apart with mincore() on the VMM's own mapping: a discarded
page is gone, a stale fault finds it present.

This is a dance we have to do because we support balooning/discards
during postcopy. Maybe we should just not support that during postcopy,
and then we wouldn't need this ugly piece of code.

Signed-off-by: Simon Lucido <simonlucido@meta.com>
@lucido-simon
lucido-simon force-pushed the accept_multiple_uffd_fd branch from 8e9ddd4 to c4eeeda Compare October 9, 2026 09:41

@sboeuf sboeuf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Comment thread vmm/src/uffd.rs

/// A userfaultfd and the ranges registered in it, at the addresses they are
/// mapped at in the process owning it.
pub(crate) struct Uffd {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not a fan of UfFdWithMeta. And in general all other structures have the prefix Uffd, so I'd keep Uffd name here.

Comment thread vmm/src/uffd.rs Outdated
/// mapped at in the process owning it.
pub(crate) struct Uffd {
fd: OwnedFd,
ranges: Vec<UffdRange>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Even if you're correct, it feels really going against the Rust language to use Box<[UffdRange]> instead of Vec<UffdRange>.

@phip1611

phip1611 commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Even if you're correct, it feels really going against the Rust language to use Box<[UffdRange]> instead of Vec.

But why? There is a std function for that (trivial conversion after final vec construction): https://doc.rust-lang.org/std/vec/struct.Vec.html#method.into_boxed_slice

I think this is generally preferable. I submitted a PR that refactored some vecs into boxes a couple of months ago btw: #8231

@rbradford
rbradford added this pull request to the merge queue Oct 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 9, 2026
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.

4 participants