Skip to content

Commit c60a118

Browse files
vttaclaude
andcommitted
Add copy_wp() method for UFFDIO_COPY with UFFDIO_COPY_MODE_WP
Add a new method that combines UFFDIO_COPY with UFFDIO_COPY_MODE_WP to resolve a page fault and enable write-protection tracking in a single ioctl call, useful for dirty-page tracking scenarios. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aac58f5 commit c60a118

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

src/lib.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,49 @@ impl Uffd {
175175
}
176176
}
177177

178+
/// Atomically copy a continuous memory chunk into the userfaultfd-registered range and
179+
/// register the pages as write-protected, returning the number of bytes successfully copied.
180+
///
181+
/// This combines `UFFDIO_COPY` with `UFFDIO_COPY_MODE_WP` to resolve a page fault and
182+
/// enable write-protection tracking in a single ioctl.
183+
///
184+
/// If `wake` is `true`, wake up the thread waiting for page fault resolution on the memory
185+
/// range.
186+
#[cfg(feature = "linux5_7")]
187+
pub unsafe fn copy_wp(
188+
&self,
189+
src: *const c_void,
190+
dst: *mut c_void,
191+
len: usize,
192+
wake: bool,
193+
) -> Result<usize> {
194+
let mut copy = raw::uffdio_copy {
195+
src: src as u64,
196+
dst: dst as u64,
197+
len: len as u64,
198+
mode: raw::UFFDIO_COPY_MODE_WP
199+
| if wake {
200+
0
201+
} else {
202+
raw::UFFDIO_COPY_MODE_DONTWAKE
203+
},
204+
copy: 0,
205+
};
206+
207+
let _ =
208+
raw::copy(self.as_raw_fd(), &mut copy as *mut raw::uffdio_copy).map_err(|errno| {
209+
match errno {
210+
Errno::EAGAIN => Error::PartiallyCopied(copy.copy as usize),
211+
_ => Error::CopyFailed(errno),
212+
}
213+
})?;
214+
if copy.copy < 0 {
215+
Err(Error::CopyFailed(Errno::from_i32(-copy.copy as i32)))
216+
} else {
217+
Ok(copy.copy as usize)
218+
}
219+
}
220+
178221
/// Zero out a memory address range registered with userfaultfd, and return the number of bytes
179222
/// that were successfully zeroed.
180223
///
@@ -708,4 +751,100 @@ mod test {
708751

709752
Ok(())
710753
}
754+
755+
/// Test that `copy_wp()` resolves a missing fault and applies write-protection in one ioctl.
756+
///
757+
/// 1. Create a uffd registered for both MISSING and WRITE_PROTECT
758+
/// 2. Prepare a source page with value `42`
759+
/// 3. Spawn a thread that reads then writes the mapping
760+
/// 4. Handle the missing fault with `copy_wp()` — copies source data and sets WP in one ioctl
761+
/// 5. The thread's read succeeds (sees `42`), then the write triggers a WP fault
762+
/// 6. Handle the WP fault by removing write-protection
763+
/// 7. Verify the thread's write (`99`) landed
764+
#[cfg(feature = "linux5_7")]
765+
#[test]
766+
fn test_copy_wp() -> Result<()> {
767+
const PAGE_SIZE: usize = 4096;
768+
769+
unsafe {
770+
let uffd = UffdBuilder::new()
771+
.require_features(FeatureFlags::PAGEFAULT_FLAG_WP)
772+
.close_on_exec(true)
773+
.create()?;
774+
775+
let mapping = libc::mmap(
776+
ptr::null_mut(),
777+
PAGE_SIZE,
778+
libc::PROT_READ | libc::PROT_WRITE,
779+
libc::MAP_PRIVATE | libc::MAP_ANON,
780+
-1,
781+
0,
782+
);
783+
784+
assert!(!mapping.is_null());
785+
786+
assert!(uffd
787+
.register_with_mode(
788+
mapping,
789+
PAGE_SIZE,
790+
RegisterMode::MISSING | RegisterMode::WRITE_PROTECT
791+
)?
792+
.contains(IoctlFlags::WRITE_PROTECT));
793+
794+
// Prepare a source page to copy from
795+
let src = libc::mmap(
796+
ptr::null_mut(),
797+
PAGE_SIZE,
798+
libc::PROT_READ | libc::PROT_WRITE,
799+
libc::MAP_PRIVATE | libc::MAP_ANON,
800+
-1,
801+
0,
802+
);
803+
assert!(!src.is_null());
804+
*(src as *mut u8) = 42;
805+
806+
let ptr = mapping as usize;
807+
let thread = thread::spawn(move || {
808+
let ptr = ptr as *mut u8;
809+
// First access triggers missing fault; after copy_wp resolves it
810+
// with WP, this read succeeds but the subsequent write triggers
811+
// a write-protect fault.
812+
assert_eq!(*ptr, 42);
813+
*ptr = 99;
814+
});
815+
816+
loop {
817+
match uffd.read_event()? {
818+
Some(Event::Pagefault { kind, addr, .. }) => match kind {
819+
FaultKind::Missing => {
820+
assert_eq!(addr, mapping);
821+
// Resolve the missing fault AND set write-protection in one call
822+
let copied =
823+
uffd.copy_wp(src as *const c_void, mapping, PAGE_SIZE, true)?;
824+
assert_eq!(copied, PAGE_SIZE);
825+
}
826+
FaultKind::WriteProtected => {
827+
assert_eq!(addr, mapping);
828+
// Page should have the copied content
829+
assert_eq!(*(addr as *const u8), 42);
830+
uffd.remove_write_protection(mapping, PAGE_SIZE, true)?;
831+
break;
832+
}
833+
},
834+
_ => panic!("unexpected event"),
835+
}
836+
}
837+
838+
thread.join().expect("failed to join thread");
839+
840+
assert_eq!(*(mapping as *const u8), 99);
841+
842+
uffd.unregister(mapping, PAGE_SIZE)?;
843+
844+
assert_eq!(libc::munmap(mapping, PAGE_SIZE), 0);
845+
assert_eq!(libc::munmap(src, PAGE_SIZE), 0);
846+
}
847+
848+
Ok(())
849+
}
711850
}

0 commit comments

Comments
 (0)