Skip to content

Commit ffec696

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 ffec696

File tree

1 file changed

+143
-0
lines changed

1 file changed

+143
-0
lines changed

src/lib.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,45 @@ 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 { 0 } else { raw::UFFDIO_COPY_MODE_DONTWAKE },
200+
copy: 0,
201+
};
202+
203+
let _ =
204+
raw::copy(self.as_raw_fd(), &mut copy as *mut raw::uffdio_copy).map_err(|errno| {
205+
match errno {
206+
Errno::EAGAIN => Error::PartiallyCopied(copy.copy as usize),
207+
_ => Error::CopyFailed(errno),
208+
}
209+
})?;
210+
if copy.copy < 0 {
211+
Err(Error::CopyFailed(Errno::from_i32(-copy.copy as i32)))
212+
} else {
213+
Ok(copy.copy as usize)
214+
}
215+
}
216+
178217
/// Zero out a memory address range registered with userfaultfd, and return the number of bytes
179218
/// that were successfully zeroed.
180219
///
@@ -708,4 +747,108 @@ mod test {
708747

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

0 commit comments

Comments
 (0)