feat(mm): unify shared anonymous mappings with internal shmem files (#2266)

* feat(mm): unify shared anonymous mappings with internal shmem files

Give shared anonymous mmap and writable-capable shared /dev/zero mappings
one authoritative vm_file backed by the existing tmpfs shmem PageCache.
Independent mmap calls create independent unlinked inodes; fork, VMA splits
and mremap retain the file identity, page offsets and initial backing size.

Generalize the internal shmem constructor to accept a diagnostic name and
retain its state through an Arc<File>. Remove wrapper-owned logical-size
charges and use existing page-cache membership accounting and teardown.
Reuse the constructor for System V SHM while keeping IPC permissions,
attach accounting, IPC_RMID and SHM_LOCK in the IPC layer. Derive SYSV names
from the 32-bit IPC key.

Let the inode mmap hook return the final mapping file after originating
access checks. Forward this contract through mount, overlayfs and FUSE.
Require readable mapping fds even for PROT_NONE, and preserve Linux's
VM_SHARED/VM_MAYSHARE distinction for read-only shared mappings.

Remove AnonSharedMapping and its fault, futex, mincore, msync and mremap
branches. Use ordinary file-cache paths for shared memory and describe
retained anonymous-page inodes explicitly so read-only /dev/zero shared
futex operations return EFAULT instead of using the device inode key.
Render unlinked diagnostic names from real inode metadata in procfs.

For architectures without demand paging, populate shared shmem from the
same cache and roll back unpublished PTEs, reverse mappings and reservations
on failure. Existing architecture-level fault/SIGBUS limitations remain.

Add 20 dunitest cases covering mapping identity, sparse and concurrent
faults, MAP_POPULATE, fork, split/remap offsets and EOF, futex behavior,
permissions, final-reference accounting, and System V diagnostic names.
Register the suite in the CI whitelist and no-skip list.

Validation:
- make kernel and Rust formatting/whitespace checks passed.
- All 20 new tests passed on Linux and in an x86_64 QEMU/KVM guest.
- The guest regression run passed 276 tests, skipped 5 prerequisite-bound
  cases and failed none across 13 dunitest/gVisor binaries.
- Non-x86 architecture runtime and dedicated FUSE/overlay integration
  tests were not run.

Refs: #2186
Signed-off-by: longjin <longjin@dragonos.org>

* style(mm): use div_ceil for the shmem page count

Replace the handwritten rounded-up division in the eager shmem mapping
path with usize::div_ceil. This preserves the page count and EOF checks
while satisfying the manual_is_multiple_of lint enforced by make fmt.

Validation: make fmt completed and kernel formatting/Clippy passed.
Some existing user application directories lack fmt targets; their
formatting steps cannot be treated as passing.

Signed-off-by: longjin <longjin@dragonos.org>

---------

Signed-off-by: longjin <longjin@dragonos.org>
This commit is contained in:
LoGin
2026-09-08 11:59:12 +08:00
committed by GitHub
parent b0c037e634
commit 87271de2a6
24 changed files with 764 additions and 373 deletions
+2 -10
View File
@@ -174,11 +174,7 @@ impl FileSystem for DevFS {
if !is_zero_inode(pfm) {
return VmFaultReason::VM_FAULT_SIGBUS;
}
if pfm.vma().lock().shared_anon.is_some() {
PageFaultHandler::shared_anon_fault(pfm)
} else {
PageFaultHandler::zero_fault(pfm)
}
PageFaultHandler::zero_fault(pfm)
}
unsafe fn page_mkwrite(&self, pfm: &mut PageFaultMessage) -> VmFaultReason {
@@ -197,11 +193,7 @@ impl FileSystem for DevFS {
if !is_zero_inode(pfm) {
return VmFaultReason::VM_FAULT_SIGBUS;
}
if pfm.vma().lock().shared_anon.is_some() {
PageFaultHandler::shared_anon_map_pages(pfm, start_pgoff, end_pgoff)
} else {
PageFaultHandler::zero_map_pages(pfm, start_pgoff, end_pgoff)
}
PageFaultHandler::zero_map_pages(pfm, start_pgoff, end_pgoff)
}
}
+18 -3
View File
@@ -1,6 +1,6 @@
use crate::driver::base::device::device_number::{DeviceNumber, Major};
use crate::filesystem::devfs::LockedDevFSInode;
use crate::filesystem::vfs::file::FileFlags;
use crate::filesystem::vfs::file::{File, FileFlags};
use crate::filesystem::vfs::InodeMode;
use crate::filesystem::vfs::{
utils::DName, vcore::generate_inode_id, FilePrivateData, FileSystem, FileType, IndexNode,
@@ -165,8 +165,23 @@ impl IndexNode for LockedZeroInode {
Ok(())
}
fn mmap_uses_shared_anon(&self, vm_flags: VmFlags) -> bool {
vm_flags.contains(VmFlags::VM_SHARED)
fn mmap_uses_anonymous_pages(&self) -> bool {
true
}
fn mmap_file(
&self,
file: &Arc<File>,
_start: usize,
len: usize,
_offset: usize,
vm_flags: VmFlags,
) -> Result<Arc<File>, SystemError> {
if vm_flags.contains(VmFlags::VM_SHARED) {
Ok(crate::filesystem::tmpfs::create_unlinked_shmem_file("dev/zero", len)?.file())
} else {
Ok(file.clone())
}
}
fn parent(&self) -> Result<Arc<dyn IndexNode>, SystemError> {
+2 -2
View File
@@ -129,7 +129,7 @@ impl IndexNode for FuseNode {
len: usize,
offset: usize,
vm_flags: crate::mm::VmFlags,
) -> Result<(), SystemError> {
) -> Result<Arc<crate::filesystem::vfs::file::File>, SystemError> {
let _ = (start, len, offset);
self.check_not_stale()?;
if file.file_type() != FileType::File {
@@ -152,7 +152,7 @@ impl IndexNode for FuseNode {
}
self.ensure_page_cache()?;
Ok(())
Ok(file.clone())
}
fn getxattr(&self, name: &str, buf: &mut [u8]) -> Result<usize, SystemError> {
+1 -1
View File
@@ -214,7 +214,7 @@ pub(super) fn mmap_file(
len: usize,
offset: usize,
vm_flags: VmFlags,
) -> Result<(), SystemError> {
) -> Result<Arc<File>, SystemError> {
let (backing_file, _) = backing_file_for_io(inode, file.private_data.lock())?;
backing_file
.inode()
+1 -1
View File
@@ -652,7 +652,7 @@ impl IndexNode for OvlInode {
len: usize,
offset: usize,
vm_flags: VmFlags,
) -> Result<(), SystemError> {
) -> Result<Arc<File>, SystemError> {
file::mmap_file(self, file, start, len, offset, vm_flags)
}
+1 -1
View File
@@ -2278,7 +2278,7 @@ impl PageCache {
self.unevictable.load(Ordering::Relaxed)
}
fn is_shmem(&self) -> bool {
pub(crate) fn is_shmem(&self) -> bool {
self.kind == PageCacheKind::Shmem
}
+10 -2
View File
@@ -56,7 +56,7 @@ fn perms_from_vm_flags(vm_flags: VmFlags) -> [u8; 4] {
} else {
b'-'
};
let s = if vm_flags.contains(VmFlags::VM_SHARED) {
let s = if vm_flags.contains(VmFlags::VM_MAYSHARE) {
b's'
} else {
b'p'
@@ -81,8 +81,16 @@ fn format_dev_inode_and_path(
.map(|inode| inode.procfs_path())
.unwrap_or_else(|| inode.absolute_path())
.unwrap_or_default();
// An unlinked internal inode has a diagnostic dname but no
// namespace path. Do not apply chroot path stripping to it.
let diagnostic = path.is_empty() && md.nlinks == 0;
if diagnostic {
if let Ok(name) = inode.dname() {
path = format!("/{} (deleted)", name.as_ref());
}
}
// 尊重进程的 chroot:去掉根目录前缀
if !root_prefix.is_empty() && root_prefix != "/" {
if !diagnostic && !root_prefix.is_empty() && root_prefix != "/" {
if let Some(rest) = path.strip_prefix(root_prefix) {
path = if rest.is_empty() {
"/".to_string()
+41 -48
View File
@@ -34,10 +34,12 @@ use alloc::{
use system_error::SystemError;
use super::vfs::{
file::FilePrivateData, mount::MountFlags, utils::DName, FileSystem, FsInfo,
FsReconfigureRequest, IndexNode, InodeFlags, InodeId, InodeMode, LinkMutationCoordinator,
LinkRemovalOutcome, Metadata, OpenFileBehavior, PostWriteSyncPolicy, RenameOutcome,
SetMetadataMask, SpecialNodeData,
file::{File, FileFlags, FilePrivateData},
mount::MountFlags,
utils::DName,
FileSystem, FsInfo, FsReconfigureRequest, IndexNode, InodeFlags, InodeId, InodeMode,
LinkMutationCoordinator, LinkRemovalOutcome, Metadata, OpenFileBehavior, PostWriteSyncPolicy,
RenameOutcome, SetMetadataMask, SpecialNodeData,
};
use linkme::distributed_slice;
@@ -340,24 +342,30 @@ pub struct Tmpfs {
#[derive(Debug)]
pub struct TmpfsShmemFile {
inode: Arc<dyn IndexNode>,
fs: Arc<Tmpfs>,
inode_id: InodeId,
page_cache: Arc<PageCache>,
charged_size: usize,
file: Arc<File>,
}
impl TmpfsShmemFile {
pub fn file(&self) -> Arc<File> {
self.file.clone()
}
pub fn inode(&self) -> Arc<dyn IndexNode> {
self.inode.clone()
self.file.inode()
}
pub fn inode_id(&self) -> InodeId {
self.inode_id
// This wrapper is constructed only from a live internal tmpfs inode.
self.file
.metadata()
.expect("internal shmem metadata")
.inode_id
}
pub fn page_cache(&self) -> Arc<PageCache> {
self.page_cache.clone()
self.inode()
.page_cache()
.expect("internal shmem page cache")
}
pub fn set_locked(&self, locked: bool) -> (Arc<PageCache>, bool) {
@@ -367,12 +375,6 @@ impl TmpfsShmemFile {
}
}
impl Drop for TmpfsShmemFile {
fn drop(&mut self) {
self.fs.decrease_size(self.charged_size);
}
}
#[derive(Debug)]
pub struct TmpfsInode {
parent: Weak<LockedTmpfsInode>,
@@ -717,17 +719,8 @@ impl Tmpfs {
if size > i64::MAX as usize {
return Err(SystemError::EOVERFLOW);
}
let charged_size = size
.checked_add(MMArch::PAGE_SIZE - 1)
.ok_or(SystemError::EOVERFLOW)?
& !(MMArch::PAGE_SIZE - 1);
let charged_size_u64 = charged_size as u64;
let blocks_u64 = Self::bytes_to_blocks_ceil(size as u64);
if blocks_u64 > usize::MAX as u64 {
return Err(SystemError::EOVERFLOW);
}
self.increase_size(charged_size_u64)?;
// Logical size is not resident tmpfs quota. PageCache membership
// reserves/releases actual pages, including creation failure rollback.
let inode_id = generate_inode_id();
let result: Arc<LockedTmpfsInode> = Arc::new(LockedTmpfsInode::new(TmpfsInode {
parent: Weak::default(),
@@ -739,7 +732,7 @@ impl Tmpfs {
inode_id,
size: size as i64,
blk_size: TMPFS_BLOCK_SIZE as usize,
blocks: blocks_u64 as usize,
blocks: 0,
atime: PosixTimeSpec::default(),
mtime: PosixTimeSpec::default(),
ctime: PosixTimeSpec::default(),
@@ -767,30 +760,30 @@ impl Tmpfs {
let pc = new_tmpfs_page_cache(Arc::downgrade(&inode_dyn), backend, &Arc::downgrade(self))?;
result.0.lock().page_cache = Some(pc.clone());
Ok(Arc::new(TmpfsShmemFile {
inode: inode_dyn,
fs: self.clone(),
inode_id,
page_cache: pc,
charged_size,
}))
let file = Arc::new(File::new(
inode_dyn,
FileFlags::O_RDWR | FileFlags::O_LARGEFILE,
)?);
Ok(Arc::new(TmpfsShmemFile { file }))
}
}
lazy_static! {
static ref SYSV_SHMEM_TMPFS: Arc<Tmpfs> = Tmpfs::new_internal_shmem(Some(InodeMode::S_IRWXUGO));
// Bare tmpfs inodes hold a Weak filesystem reference. Keep the private
// mount alive independently of IPC namespaces, files and VMAs.
static ref INTERNAL_SHMEM_TMPFS: Arc<Tmpfs> = Tmpfs::new_internal_shmem(Some(InodeMode::S_IRWXUGO));
}
pub fn create_unlinked_shmem_file(size: usize) -> Result<Arc<TmpfsShmemFile>, SystemError> {
static NEXT_SYSV_SHMEM_NAME: AtomicU64 = AtomicU64::new(1);
let name = format!(
"SYSV{:08x}",
NEXT_SYSV_SHMEM_NAME.fetch_add(1, Ordering::Relaxed)
);
let name = DName::from(name.as_str());
SYSV_SHMEM_TMPFS.create_unlinked_shmem_inode(
name,
InodeMode::S_IRUSR | InodeMode::S_IWUSR,
/// Create a fixed-size, unlinked shmem object without publishing a path or fd.
/// The file/inode owns its PageCache; retaining just file() is sufficient.
/// `name` is diagnostic only and must be chosen by the kernel caller.
pub fn create_unlinked_shmem_file(
name: &str,
size: usize,
) -> Result<Arc<TmpfsShmemFile>, SystemError> {
INTERNAL_SHMEM_TMPFS.create_unlinked_shmem_inode(
DName::from(name),
InodeMode::S_IFREG | InodeMode::S_IRWXUGO,
size,
)
}
+12 -5
View File
@@ -731,8 +731,10 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync {
Ok(vm_flags)
}
/// Whether this file mapping should use MM's per-mmap shared-anonymous backing.
fn mmap_uses_shared_anon(&self, _vm_flags: VmFlags) -> bool {
/// Whether mappings retaining this inode produce anonymous pages rather
/// than file-backed pages (Linux vma_set_anonymous). A replacement file's
/// inode supplies the property after mmap_file returns.
fn mmap_uses_anonymous_pages(&self) -> bool {
false
}
@@ -740,15 +742,20 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync {
Ok(file.clone())
}
/// Run mmap admission after the originating file's access checks, outside
/// the address-space lock. Success returns the authoritative VMA file;
/// an inode may replace it with an internal backing (e.g. shmem).
/// The returned file receives subsequent VMA open/close notifications.
fn mmap_file(
&self,
_file: &Arc<File>,
file: &Arc<File>,
start: usize,
len: usize,
offset: usize,
_vm_flags: VmFlags,
) -> Result<(), SystemError> {
self.mmap(start, len, offset)
) -> Result<Arc<File>, SystemError> {
self.mmap(start, len, offset)?;
Ok(file.clone())
}
fn read_sync(&self, _offset: usize, _buf: &mut [u8]) -> Result<usize, SystemError> {
+3 -3
View File
@@ -5045,8 +5045,8 @@ impl IndexNode for MountFSInode {
self.dentry.inode.mmap_vm_flags(file, vm_flags)
}
fn mmap_uses_shared_anon(&self, vm_flags: VmFlags) -> bool {
self.dentry.inode.mmap_uses_shared_anon(vm_flags)
fn mmap_uses_anonymous_pages(&self) -> bool {
self.dentry.inode.mmap_uses_anonymous_pages()
}
fn mmap_effective_file(
@@ -5063,7 +5063,7 @@ impl IndexNode for MountFSInode {
len: usize,
offset: usize,
vm_flags: VmFlags,
) -> Result<(), SystemError> {
) -> Result<Arc<super::file::File>, SystemError> {
self.dentry
.inode
.mmap_file(file, start, len, offset, vm_flags)
+6 -2
View File
@@ -489,8 +489,12 @@ impl ShmManager {
return Ok(shm_id.data());
}
pub fn create_default_backing(size: usize) -> Result<SysVShmBackingRef, SystemError> {
Ok(create_unlinked_shmem_file(size)?)
pub fn create_default_backing(
key: ShmKey,
size: usize,
) -> Result<SysVShmBackingRef, SystemError> {
let name = alloc::format!("SYSV{:08x}", key.data() as u32);
Ok(create_unlinked_shmem_file(&name, size)?)
}
pub fn contains_key(&self, key: &ShmKey) -> Option<&ShmId> {
+2 -2
View File
@@ -61,7 +61,7 @@ pub(super) fn do_kernel_shmget(
let shm_manager_guard = ipcns.shm.lock();
shm_manager_guard.validate_new_segment_size(size)?
};
let backing = ShmManager::create_default_backing(size)?;
let backing = ShmManager::create_default_backing(key, size)?;
let mut shm_manager_guard = ipcns.shm.lock();
shm_manager_guard.add_prepared(key, size, shmflg, backing, numpages)
}
@@ -82,7 +82,7 @@ pub(super) fn do_kernel_shmget(
shm_manager_guard.validate_new_segment_size(size)?
};
let backing = ShmManager::create_default_backing(size)?;
let backing = ShmManager::create_default_backing(key, size)?;
let mut shm_manager_guard = ipcns.shm.lock();
if let Some(id) = shm_manager_guard.contains_key(&key).copied() {
return existing_segment_result(&mut shm_manager_guard, id, size, shmflg);
+10 -16
View File
@@ -208,8 +208,6 @@ pub struct FutexKey {
pub enum SharedKeyKind {
/// 文件映射的 futex
File { dev: u64, ino: u64 },
/// 显式共享的匿名映射(MAP_SHARED | MAP_ANONYMOUS
SharedAnon { id: u64 },
/// 私有匿名映射上的 FUTEX_SHARED(栈、堆等)
/// 只能在同一进程的线程间同步
PrivateAnonShared { as_id: u64 },
@@ -671,20 +669,16 @@ impl Futex {
let page_index =
((uaddr.data() - vma_guard.region().start().data()) >> MMArch::PAGE_SHIFT) as u64;
if let Some(shared_anon) = &vma_guard.shared_anon {
let base_pgoff = vma_guard.backing_page_offset().unwrap_or(0) as u64;
let shared = SharedKey {
kind: SharedKeyKind::SharedAnon { id: shared_anon.id },
page_offset: base_pgoff + page_index,
};
let key = FutexKey {
ptr: 0,
word: 0,
offset: offset as u32,
key: InnerFutexKey::Shared(shared),
};
return Ok(key);
} else if let Some(file) = vma_guard.vm_file() {
let file = vma_guard.vm_file();
let anonymous_pages = file
.as_ref()
.is_some_and(|file| file.inode().mmap_uses_anonymous_pages());
if anonymous_pages && !vma_guard.vm_flags().contains(VmFlags::VM_WRITE) {
// Read-only anonymous pages cannot participate in shared futexes.
// In particular, a retained /dev/zero file is not their identity.
return Err(SystemError::EFAULT);
}
if let Some(file) = file.filter(|_| !anonymous_pages) {
// 共享文件映射:使用 inode 唯一标识 + 文件页偏移
let md = file.metadata()?;
let dev = md.dev_id as u64;
+3 -87
View File
@@ -612,10 +612,10 @@ impl PageFaultHandler {
let vm_flags = pfm.vm_flags();
let vma = pfm.vma.clone();
// Shared anonymous mappings must always use their shared backing. A
// missing backing is a malformed shared VMA, not a private fallback.
// Shared memory must have a file backing; never fall back to
// allocating a private page for a malformed shared VMA.
if vm_flags.contains(VmFlags::VM_SHARED) {
return Self::shared_anon_fault(pfm);
return VmFaultReason::VM_FAULT_SIGBUS;
}
// Private anonymous page.
@@ -647,90 +647,6 @@ impl PageFaultHandler {
}
}
/// Fault one page from a VMA's shared-anonymous backing.
pub unsafe fn shared_anon_fault(pfm: &mut PageFaultMessage) -> VmFaultReason {
if crate::mm::oom::should_inject_fault_oom() {
return VmFaultReason::VM_FAULT_OOM | VmFaultReason::VM_FAULT_OOM_INJECTED;
}
let address = pfm.address_aligned_down();
let pgoff = match pfm.backing_pgoff() {
Some(pgoff) => pgoff,
None => return VmFaultReason::VM_FAULT_SIGBUS,
};
let vma = pfm.vma();
let (shared, flags, mlocked) = {
let guard = vma.lock();
(
guard.shared_anon.clone(),
guard.flags(),
guard.vm_flags().contains(VmFlags::VM_LOCKED),
)
};
let Some(shared) = shared else {
return VmFaultReason::VM_FAULT_SIGBUS;
};
if pgoff >= shared.size_pages() {
return VmFaultReason::VM_FAULT_SIGBUS;
}
let page = match shared.get_or_create_page(pgoff) {
Ok(page) => page,
Err(_) => return VmFaultReason::VM_FAULT_OOM,
};
let mm = pfm.mm().clone();
let _pt_edit = mm.page_table_edit();
if let Some(flush) = pfm.mapper.map_phys(address, page.phys_address(), flags) {
flush.flush();
Self::account_new_present_mapping(&mm);
Self::attach_fault_mapped_page(&page, &vma, mlocked);
VmFaultReason::VM_FAULT_COMPLETED
} else {
VmFaultReason::VM_FAULT_OOM
}
}
/// Map resident neighbours from a shared-anonymous backing without
/// allocating cold pages.
pub unsafe fn shared_anon_map_pages(
pfm: &mut PageFaultMessage,
start_pgoff: usize,
end_pgoff: usize,
) -> VmFaultReason {
let vma = pfm.vma();
let (shared, base_pgoff, base, flags, mlocked) = {
let guard = vma.lock();
(
guard.shared_anon.clone(),
guard.backing_page_offset(),
guard.region().start(),
guard.flags(),
guard.vm_flags().contains(VmFlags::VM_LOCKED),
)
};
let (Some(shared), Some(base_pgoff)) = (shared, base_pgoff) else {
return VmFaultReason::VM_FAULT_SIGBUS;
};
let mm = pfm.mm().clone();
let _pt_edit = mm.page_table_edit();
for pgoff in start_pgoff..end_pgoff {
if pgoff < base_pgoff || pgoff >= shared.size_pages() {
continue;
}
let addr = VirtAddr::new(base.data() + ((pgoff - base_pgoff) << MMArch::PAGE_SHIFT));
if pfm.mapper.get_entry(addr, 0).is_some() {
continue;
}
let Some(page) = shared.lookup_page(pgoff) else {
continue;
};
if let Some(flush) = pfm.mapper.map_phys(addr, page.phys_address(), flags) {
flush.flush();
Self::account_new_present_mapping(&mm);
Self::attach_fault_mapped_page(&page, &vma, mlocked);
}
}
VmFaultReason::empty()
}
/// 处理文件映射页的缺页异常
/// ## 参数
///
-6
View File
@@ -88,12 +88,6 @@ impl LockedVMA {
let guard = self.lock();
let pgoff = ((start_addr.data() - guard.region().start().data()) >> MMArch::PAGE_SHIFT)
+ guard.backing_page_offset().unwrap();
if let Some(shared) = guard.shared_anon.as_ref() {
for i in 0..nr {
vec[vec_offset + i] = u8::from(shared.lookup_page(pgoff + i).is_some());
}
return nr;
}
if guard.vm_file().is_none() {
vec[vec_offset..vec_offset + nr].fill(0);
return nr;
+2 -6
View File
@@ -69,7 +69,7 @@ impl Syscall for SysMsyncHandle {
loop {
if let Some(vma) = next_vma.clone() {
// 读取VMA信息,确保在调用find_nearest前释放锁
let (vm_start, vm_end, vm_flags, file, backing_pgoff, has_shared_anon);
let (vm_start, vm_end, vm_flags, file, backing_pgoff);
{
let guard = vma.lock();
vm_start = guard.region().start().data();
@@ -77,7 +77,6 @@ impl Syscall for SysMsyncHandle {
vm_flags = *guard.vm_flags();
file = guard.vm_file();
backing_pgoff = guard.backing_page_offset();
has_shared_anon = guard.shared_anon.is_some();
if start < vm_start {
if flags == MsFlags::MS_ASYNC {
@@ -102,10 +101,7 @@ impl Syscall for SysMsyncHandle {
let sync_end = end.min(vm_end);
start = vm_end;
if flags.contains(MsFlags::MS_SYNC)
&& vm_flags.contains(VmFlags::VM_SHARED)
&& !has_shared_anon
{
if flags.contains(MsFlags::MS_SYNC) && vm_flags.contains(VmFlags::VM_SHARED) {
if let Some(file) = file {
if sync_start < sync_end {
let file_start = backing_pgoff
+122 -57
View File
@@ -885,6 +885,23 @@ impl AddressSpace {
allocate_at_once: bool,
) -> Result<VirtPageFrame, SystemError> {
let len = page_align_up(len);
if map_flags.contains(MapFlags::MAP_SHARED) {
if len == 0 {
return Err(SystemError::EINVAL);
}
let file =
crate::filesystem::tmpfs::create_unlinked_shmem_file("dev/zero", len)?.file();
return self.file_mapping_with_file(
file,
start_vaddr,
len,
prot_flags,
map_flags,
0,
round_to_min,
allocate_at_once,
);
}
loop {
let mut guard = self.write();
let fixed_hint =
@@ -1085,22 +1102,16 @@ impl AddressSpace {
return Err(SystemError::EBADF);
}
let wants_access = prot_flags != ProtFlags::PROT_NONE;
if wants_access && !file_mode.contains(FileMode::FMODE_READ) {
// Linux requires a readable mapping fd even for PROT_NONE.
if !file_mode.contains(FileMode::FMODE_READ) {
return Err(SystemError::EACCES);
}
if prot_flags.contains(ProtFlags::PROT_EXEC) && !file_mode.contains(FileMode::FMODE_READ) {
if prot_flags.contains(ProtFlags::PROT_WRITE)
&& map_flags.contains(MapFlags::MAP_SHARED)
&& !file_mode.contains(FileMode::FMODE_WRITE)
{
return Err(SystemError::EACCES);
}
if prot_flags.contains(ProtFlags::PROT_WRITE) {
if map_flags.contains(MapFlags::MAP_SHARED) {
if !file_mode.contains(FileMode::FMODE_WRITE) {
return Err(SystemError::EACCES);
}
} else if !file_mode.contains(FileMode::FMODE_READ) {
return Err(SystemError::EACCES);
}
}
if matches!(file.file_type(), FileType::Pipe | FileType::Dir) {
return Err(SystemError::ENODEV);
@@ -1114,7 +1125,6 @@ impl AddressSpace {
let may_write =
!map_flags.contains(MapFlags::MAP_SHARED) || file_mode.contains(FileMode::FMODE_WRITE);
let vma_file = file.inode().mmap_effective_file(&file)?;
let mut shared_anon = None;
loop {
let mut guard = self.write();
@@ -1194,15 +1204,16 @@ impl AddressSpace {
}
if may_write {
vm_flags |= VmFlags::VM_MAYWRITE;
} else {
// A read-only shared fd retains MAYSHARE, but cannot create
// a writable shared backing (notably mmap_zero in Linux).
vm_flags.remove(VmFlags::VM_SHARED);
}
vm_flags = match vma_file.inode().mmap_vm_flags(&vma_file, vm_flags) {
Ok(flags) => flags,
Err(err) => map_fail!(err),
};
if vma_file.inode().mmap_uses_shared_anon(vm_flags) && shared_anon.is_none() {
shared_anon = Some(AnonSharedMapping::new(page_count.data()));
}
if vm_flags.contains(VmFlags::VM_LOCKED) {
let error = if map_flags.contains(MapFlags::MAP_LOCKED)
@@ -1292,25 +1303,6 @@ impl AddressSpace {
} else {
false
};
let lazy_vma = if MMArch::PAGE_FAULT_ENABLED {
let vma = LockedVMA::new(VMA::new(
region,
vm_flags,
entry_flags,
Some(vma_file.clone()),
Some(pgoff),
false,
));
if let Some(sysv_shm) = sysv_shm.clone() {
vma.lock().set_sysv_shm(Some(sysv_shm));
}
if let Some(shared_anon) = shared_anon.clone() {
vma.lock().shared_anon = Some(shared_anon);
}
Some(vma)
} else {
None
};
drop(guard);
#[cfg(target_arch = "x86_64")]
if let Some(uprobe_change) = uprobe_change.take() {
@@ -1324,6 +1316,11 @@ impl AddressSpace {
.inode()
.mmap_file(&vma_file, region.start().data(), len, offset, vm_flags);
let file_mmap_opened = hook_result.is_ok();
let (vma_file, hook_error) = match hook_result {
Ok(backing) => (backing, None),
Err(SystemError::ENOSYS) => (vma_file.clone(), None),
Err(err) => (vma_file.clone(), Some(err)),
};
let mut guard = self.write();
macro_rules! close_file_mmap_if_opened {
() => {
@@ -1365,39 +1362,92 @@ impl AddressSpace {
}};
}
if let Err(err) = hook_result {
if err != SystemError::ENOSYS {
cancel_reservation_and_unlock_pages!();
return Err(err);
}
if let Some(err) = hook_error {
cancel_reservation_and_unlock_pages!();
return Err(err);
}
let new_vma = if let Some(vma) = lazy_vma {
vma
} else {
let mut flusher = crate::mm::page::DeferredFlusher::new();
compiler_fence(Ordering::SeqCst);
let _pt_edit = self.page_table_edit();
match VMA::zeroed(
page,
page_count,
let eager_cache =
if !MMArch::PAGE_FAULT_ENABLED && vm_flags.contains(VmFlags::VM_MAYSHARE) {
vma_file
.inode()
.page_cache()
.filter(|cache| cache.is_shmem())
} else {
None
};
// Do not wait on invalidation while holding MM write: truncate
// may already be waiting to unmap this address space. Keep the
// successful admission through publication in the VMA index.
let eager_invalidate = match eager_cache.as_ref() {
Some(cache) => match cache.try_invalidate_read() {
Some(invalidate) => Some(invalidate),
None => {
cancel_reservation_and_unlock_pages!();
close_file_mmap_if_opened!();
return Err(SystemError::EAGAIN_OR_EWOULDBLOCK);
}
},
None => None,
};
let new_vma = if MMArch::PAGE_FAULT_ENABLED {
let vma = LockedVMA::new(VMA::new(
region,
vm_flags,
entry_flags,
&mut guard.user_mapper.utable,
&mut flusher,
Some(vma_file.clone()),
Some(pgoff),
) {
false,
));
if let Some(sysv_shm) = sysv_shm.clone() {
vma.lock().set_sysv_shm(Some(sysv_shm));
}
vma
} else {
let result = {
let mut flusher = crate::mm::page::DeferredFlusher::new();
compiler_fence(Ordering::SeqCst);
let _pt_edit = self.page_table_edit();
if let Some(cache) = eager_cache.as_ref() {
VMA::map_shmem_eager(
page,
page_count,
vm_flags,
entry_flags,
&mut guard.user_mapper.utable,
vma_file.clone(),
cache,
pgoff,
)
} else {
VMA::zeroed(
page,
page_count,
vm_flags,
entry_flags,
&mut guard.user_mapper.utable,
&mut flusher,
Some(vma_file.clone()),
Some(pgoff),
)
}
};
match result {
Ok(vma) => {
if let Some(sysv_shm) = sysv_shm.clone() {
vma.lock().set_sysv_shm(Some(sysv_shm));
}
if let Some(shared_anon) = shared_anon.clone() {
vma.lock().shared_anon = Some(shared_anon);
}
vma
}
Err(err) => {
self.flush_tlb_range(
region.start(),
region.end(),
MMArch::PAGE_SHIFT as u8,
true,
);
drop(eager_invalidate);
cancel_reservation_and_unlock_pages!();
close_file_mmap_if_opened!();
return Err(err);
@@ -1405,8 +1455,21 @@ impl AddressSpace {
}
};
macro_rules! rollback_unpublished_vma {
() => {
if new_vma.mapped() {
let _pt_edit = self.page_table_edit();
let mut tlb = MmuGather::gather(self);
new_vma.unmap(&mut guard.user_mapper.utable, &mut tlb);
tlb.finish();
}
};
}
let sysv_opened = if let Some(sysv_shm) = sysv_shm.as_ref() {
if let Err(err) = sysv_shm.open_vma() {
rollback_unpublished_vma!();
drop(eager_invalidate);
cancel_reservation_and_unlock_pages!();
close_file_mmap_if_opened!();
return Err(err);
@@ -1433,14 +1496,16 @@ impl AddressSpace {
.commit_reserved_vma(reservation_id, new_vma.clone())
{
let sysv_to_close = if sysv_opened { sysv_shm.clone() } else { None };
release_locked_pages_if_reserved!();
drop(guard);
rollback_unpublished_vma!();
drop(eager_invalidate);
cancel_reservation_and_unlock_pages!();
close_file_mmap_if_opened!();
if let Some(sysv_shm) = sysv_to_close {
sysv_shm.close_vma();
}
return Err(err);
}
drop(eager_invalidate);
// Match Linux's uprobe_mmap ordering: publish probes for the new
// executable file VMA while the address-space write lock still
+5 -12
View File
@@ -71,6 +71,11 @@ impl InnerAddressSpace {
where
F: FnMut(&mut Self, &[VirtRegion]) -> Result<(), SystemError>,
{
// Shared mappings need the outer file-mapping reservation and fault
// path. Locked callers here only create private stacks, heaps or ELF.
if map_flags.contains(MapFlags::MAP_SHARED) {
return Err(SystemError::EINVAL.into());
}
let allocate_at_once = if MMArch::PAGE_FAULT_ENABLED {
allocate_at_once
} else {
@@ -93,13 +98,6 @@ impl InnerAddressSpace {
if allocate_at_once {
let vma =
VMA::zeroed(page, count, vm_flags, flags, mapper, flusher, None, None)?;
// For shared anonymous mappings, allocate a stable identity
if vm_flags.contains(VmFlags::VM_SHARED) {
let mut g = vma.lock();
g.shared_anon = Some(AnonSharedMapping::new(count.data()));
// Set backing_pgoff to 0 as the base offset for shared-anon mappings.
g.backing_pgoff = Some(0);
}
Ok(vma)
} else {
let vma = LockedVMA::new(VMA::new(
@@ -110,11 +108,6 @@ impl InnerAddressSpace {
None,
false,
));
if vm_flags.contains(VmFlags::VM_SHARED) {
let mut g = vma.lock();
g.shared_anon = Some(AnonSharedMapping::new(count.data()));
g.backing_pgoff = Some(0);
}
Ok(vma)
}
},
+1 -5
View File
@@ -14,7 +14,6 @@ use alloc::{
vec::Vec,
};
use defer::defer;
use hashbrown::HashMap;
use hashbrown::HashSet;
use ida::IdAllocator;
use log::{error, warn};
@@ -61,7 +60,6 @@ use super::{
syscall::{MadvFlags, MapFlags, MremapFlags, ProtFlags},
MemoryManagementArch, PageTableKind, VirtAddr, VirtRegion, VmFaultReason, VmFlags,
};
use crate::arch::mm::LockedFrameAllocator;
/// Default value for MMAP_MIN_ADDR
/// The following content from Linux 5.19:
@@ -142,6 +140,4 @@ pub use uprobe::{
UprobeTaskScope, XolPool, XolSlotLease,
};
#[allow(unused_imports)]
pub use vma::{
AnonSharedMapping, LockedVMA, PhysmapParams, PresentPfn, Provider, VMASplitResult, VMA,
};
pub use vma::{LockedVMA, PhysmapParams, PresentPfn, Provider, VMASplitResult, VMA};
+3 -15
View File
@@ -429,7 +429,7 @@ impl InnerAddressSpace {
let Some(old_vma) = self.mappings.contains(old_vaddr) else {
mremap_fail!(SystemError::EFAULT);
};
let (old_region, vm_flags, vm_file, shared_anon, base_pgoff, sysv_shm) = {
let (old_region, vm_flags, vm_file, base_pgoff, sysv_shm) = {
let g = old_vma.lock();
let region = *g.region();
let vma_start = region.start();
@@ -439,14 +439,7 @@ impl InnerAddressSpace {
.backing_page_offset()
.unwrap_or(0)
.saturating_add(off_pages);
(
region,
*g.vm_flags(),
g.vm_file(),
g.shared_anon.clone(),
base,
g.sysv_shm(),
)
(region, *g.vm_flags(), g.vm_file(), base, g.sysv_shm())
};
let prot_flags: ProtFlags = vm_flags.into();
@@ -762,18 +755,13 @@ impl InnerAddressSpace {
vm_flags,
entry_flags,
vm_file.clone(),
if vm_file.is_some() || shared_anon.is_some() {
if vm_file.is_some() {
Some(base_pgoff)
} else {
None
},
false,
));
if let Some(shared) = shared_anon.clone() {
let mut vg = vma.lock();
vg.shared_anon = Some(shared);
vg.backing_pgoff = Some(base_pgoff);
}
if let Some(sysv_shm) = sysv_shm.clone() {
vma.lock().set_sysv_shm(Some(sysv_shm));
}
+74 -89
View File
@@ -646,14 +646,12 @@ pub struct VMA {
pub(super) self_ref: Weak<LockedVMA>,
pub(super) vm_file: Option<Arc<File>>,
/// The offset (in pages) of the VMA's backing object (file/shared-anonymous) relative to the entire backing object
/// The offset (in pages) of the VMA's backing object (file) relative to the entire backing object
pub(super) backing_pgoff: Option<usize>,
pub(super) provider: Provider,
/// SysV SHM attach identity, used for Linux-style VMA open/close lifecycle.
pub(super) sysv_shm: Option<Arc<SysVShmAttach>>,
/// Stable identity of a shared anonymous mapping (used for cross-process futex key sharing)
pub(crate) shared_anon: Option<Arc<AnonSharedMapping>>,
}
impl core::hash::Hash for VMA {
@@ -670,89 +668,6 @@ pub enum Provider {
Allocated, // TODO: others
}
/// Stable identity of a shared anonymous mapping
#[derive(Debug)]
pub struct AnonSharedMapping {
pub id: u64,
/// Fixed backing size in pages, established at creation time.
/// Linux semantics: mremap() expanding a MAP_SHARED|MAP_ANONYMOUS mapping does not grow the
/// underlying shmem object; access beyond this size should SIGBUS.
size_pages: usize,
// Per-page cache keyed by page index within the backing object.
pages: SpinLock<HashMap<usize, Arc<Page>>>,
}
impl AnonSharedMapping {
fn new_id() -> u64 {
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
return NEXT_ID.fetch_add(1, Ordering::Relaxed);
}
pub fn new(size_pages: usize) -> Arc<Self> {
Arc::new(Self {
id: Self::new_id(),
size_pages,
pages: SpinLock::new(HashMap::new()),
})
}
#[inline(always)]
pub fn size_pages(&self) -> usize {
self.size_pages
}
/// Get or create a shared page for the given offset atomically.
/// This prevents the double-allocation race when multiple processes fault the same page.
pub fn get_or_create_page(&self, pgoff: usize) -> Result<Arc<Page>, SystemError> {
if let Some(page) = self.lookup_page(pgoff) {
return Ok(page);
}
// Page allocation may sleep, so it must not run under the backing's
// irqsave spinlock. Keep the existing PageManager mutex until the
// candidate is published, and recheck after acquiring it: this closes
// the allocate-before-publish window without adding another sleeping
// lock or per-index in-flight state.
let mut pm = page_manager_lock();
if let Some(page) = self.lookup_page(pgoff) {
return Ok(page);
}
let mut allocator = LockedFrameAllocator;
let candidate = pm.create_one_page(PageType::Normal, PageFlags::empty(), &mut allocator)?;
candidate.write().add_backing_lifetime_pin();
self.pages.lock_irqsave().insert(pgoff, candidate.clone());
Ok(candidate)
}
/// Look up an already instantiated backing page without allocating it.
pub fn lookup_page(&self, pgoff: usize) -> Option<Arc<Page>> {
let guard = self.pages.lock_irqsave();
guard.get(&pgoff).cloned()
}
}
impl Drop for AnonSharedMapping {
fn drop(&mut self) {
// When the backing object is destroyed, allow cached pages to be freed.
let pages: alloc::vec::Vec<Arc<Page>> = {
let guard = self.pages.lock_irqsave();
guard.values().cloned().collect()
};
let mut pm = page_manager_lock();
for page in pages {
let paddr = page.phys_address();
let mut pg = page.write();
pg.remove_backing_lifetime_pin();
if pg.can_deallocate() {
drop(pg);
pm.remove_page(&paddr);
}
}
}
}
#[allow(dead_code)]
impl VMA {
pub fn new(
@@ -774,7 +689,6 @@ impl VMA {
vm_file: file,
backing_pgoff: pgoff,
sysv_shm: None,
shared_anon: None,
}
}
@@ -855,7 +769,6 @@ impl VMA {
backing_pgoff: self.backing_pgoff,
vm_file: self.vm_file.clone(),
sysv_shm: self.sysv_shm.clone(),
shared_anon: self.shared_anon.clone(),
};
}
@@ -871,7 +784,6 @@ impl VMA {
backing_pgoff: self.backing_pgoff,
vm_file: self.vm_file.clone(),
sysv_shm: self.sysv_shm.clone(),
shared_anon: self.shared_anon.clone(),
};
}
@@ -1031,6 +943,79 @@ impl VMA {
return Ok(r);
}
/// Eagerly map shared shmem pages on architectures without demand faults.
/// The caller owns an empty reserved range and the MM/page-table edit locks.
/// Mutable backings also require invalidation exclusion through VMA commit.
/// EOF is rejected before publishing any PTE: these architectures cannot
/// currently deliver the SIGBUS that a demand-fault implementation would.
#[allow(clippy::too_many_arguments)]
pub fn map_shmem_eager(
destination: VirtPageFrame,
page_count: PageFrameCount,
vm_flags: VmFlags,
flags: EntryFlags<MMArch>,
mapper: &mut PageMapper,
file: Arc<File>,
page_cache: &Arc<crate::filesystem::page_cache::PageCache>,
pgoff: usize,
) -> Result<Arc<LockedVMA>, SystemError> {
debug_assert!(page_cache.is_shmem() && vm_flags.contains(VmFlags::VM_MAYSHARE));
let size = file.inode().metadata()?.size.max(0) as usize;
let file_pages = size.div_ceil(MMArch::PAGE_SIZE);
let end = pgoff
.checked_add(page_count.data())
.ok_or(SystemError::EOVERFLOW)?;
if end > file_pages {
return Err(SystemError::ENXIO);
}
// Pins keep the exact cache entries alive through mapping or rollback.
let mut mapped = Vec::new();
mapped
.try_reserve_exact(page_count.data())
.map_err(|_| SystemError::ENOMEM)?;
let vma = LockedVMA::new(VMA::new(
VirtRegion::new(destination.virt_address(), page_count.bytes()),
vm_flags,
flags,
Some(file),
Some(pgoff),
false,
));
let mlocked = vm_flags.contains(VmFlags::VM_LOCKED);
let result = (|| {
for index in pgoff..end {
let pin = page_cache.manager().commit_overwrite_pinned(index)?;
let page = pin.page();
if vm_flags.contains(VmFlags::VM_WRITE) {
page_cache.manager().prepare_page_mkwrite(index, &page)?;
}
let address = destination.next_by(index - pgoff).virt_address();
let flush = unsafe { mapper.map_phys(address, page.phys_address(), flags) }
.ok_or(SystemError::ENOMEM)?;
flush.flush();
page.write().insert_vma(vma.clone(), mlocked);
mapped.push((address, pin));
}
Ok::<(), SystemError>(())
})();
if let Err(error) = result {
for (address, pin) in mapped.into_iter().rev() {
let (_, _, flush, _) =
unsafe { mapper.unmap_phys_with_freed_tables(address, true) }
.expect("new shmem PTE disappeared under page-table edit lock");
flush.flush();
let page = pin.page();
page.write().remove_vma(&vma);
InnerAddressSpace::remove_page_unevictable_if_unneeded(&page);
// Cache membership owns the physical page, including on error.
}
return Err(error);
}
vma.lock().set_mapped(true);
Ok(vma)
}
/// Allocate some physical pages from the page allocator, map them to the specified virtual address, and then create a VMA.
/// ## Parameters
///
+2
View File
@@ -23,3 +23,5 @@ normal/tcp_self_connect_semantics
normal/poll_timeout_semantics
normal/epoll_pwait2_semantics
normal/internal_shmem
@@ -0,0 +1,441 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <gtest/gtest.h>
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <linux/futex.h>
#include <signal.h>
#include <string>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <thread>
#include <unistd.h>
namespace {
size_t PageSize() { return static_cast<size_t>(sysconf(_SC_PAGESIZE)); }
struct Mapping {
void* addr = MAP_FAILED;
size_t size = 0;
explicit Mapping(size_t length) : size(length) {}
~Mapping() { if (addr != MAP_FAILED) munmap(addr, size); }
Mapping(const Mapping&) = delete;
Mapping& operator=(const Mapping&) = delete;
volatile unsigned char* bytes() const {
return static_cast<volatile unsigned char*>(addr);
}
};
struct MapInfo {
unsigned long long inode = 0;
unsigned long long offset = 0;
std::string line;
};
bool ReadMap(void* address, MapInfo* info) {
FILE* file = fopen("/proc/self/maps", "r");
if (!file) return false;
char line[1024];
bool found = false;
while (fgets(line, sizeof(line), file)) {
unsigned long start, end;
char perms[5], dev[32];
unsigned long long offset, inode;
if (sscanf(line, "%lx-%lx %4s %llx %31s %llu", &start, &end, perms,
&offset, dev, &inode) == 6 &&
start <= reinterpret_cast<uintptr_t>(address) &&
reinterpret_cast<uintptr_t>(address) < end) {
*info = {inode, offset, line};
found = true;
break;
}
}
fclose(file);
return found;
}
// Fork keeps a fault assertion from terminating the test binary.
int ReadInChild(volatile unsigned char* address) {
pid_t child = fork();
if (child < 0) return -1;
if (child == 0) {
const unsigned char value = *address;
_exit(value == 0 ? 0 : 1);
}
int status = 0;
if (waitpid(child, &status, 0) != child) return -1;
return status;
}
class InternalShmem : public testing::TestWithParam<bool> {
protected:
int zero_fd_ = -1;
void SetUp() override {
if (GetParam()) {
zero_fd_ = open("/dev/zero", O_RDWR);
ASSERT_GE(zero_fd_, 0) << strerror(errno);
}
}
void TearDown() override { if (zero_fd_ >= 0) close(zero_fd_); }
void Map(Mapping& mapping, void* fixed = nullptr) {
int flags = MAP_SHARED | (GetParam() ? 0 : MAP_ANONYMOUS);
if (fixed) flags |= MAP_FIXED;
mapping.addr = mmap(fixed, mapping.size, PROT_READ | PROT_WRITE, flags, zero_fd_, 0);
}
};
TEST_P(InternalShmem, IndependentMappingsHaveDistinctDeletedInodes) {
Mapping first(PageSize()), second(PageSize());
Map(first); Map(second);
ASSERT_NE(first.addr, MAP_FAILED); ASSERT_NE(second.addr, MAP_FAILED);
MapInfo a, b;
ASSERT_TRUE(ReadMap(first.addr, &a)); ASSERT_TRUE(ReadMap(second.addr, &b));
EXPECT_NE(a.inode, 0u); EXPECT_NE(b.inode, 0u); EXPECT_NE(a.inode, b.inode);
EXPECT_NE(a.line.find("/dev/zero (deleted)"), std::string::npos) << a.line;
EXPECT_NE(b.line.find("/dev/zero (deleted)"), std::string::npos) << b.line;
first.bytes()[0] = 31;
EXPECT_EQ(second.bytes()[0], 0);
if (GetParam()) {
int another = open("/dev/zero", O_RDWR);
ASSERT_GE(another, 0);
Mapping third(PageSize());
third.addr = mmap(nullptr, third.size, PROT_READ | PROT_WRITE, MAP_SHARED, another, 0);
close(another);
ASSERT_NE(third.addr, MAP_FAILED);
MapInfo c;
ASSERT_TRUE(ReadMap(third.addr, &c));
EXPECT_NE(a.inode, c.inode); EXPECT_NE(b.inode, c.inode);
EXPECT_EQ(third.bytes()[0], 0);
}
}
TEST_P(InternalShmem, ChildFaultPublishesOneSparsePageAndSyncSucceeds) {
const size_t page = PageSize();
Mapping mapping(8 * page);
Map(mapping); ASSERT_NE(mapping.addr, MAP_FAILED);
unsigned char resident[8] = {};
ASSERT_EQ(mincore(mapping.addr, mapping.size, resident), 0);
for (auto value : resident) EXPECT_EQ(value & 1, 0);
pid_t child = fork(); ASSERT_GE(child, 0);
if (child == 0) {
if (mapping.bytes()[3 * page] != 0) _exit(1);
mapping.bytes()[3 * page] = 73;
_exit(0);
}
int status = 0; ASSERT_EQ(waitpid(child, &status, 0), child);
ASSERT_TRUE(WIFEXITED(status)); ASSERT_EQ(WEXITSTATUS(status), 0);
ASSERT_EQ(mincore(mapping.addr, mapping.size, resident), 0);
for (size_t i = 0; i < 8; ++i) EXPECT_EQ(resident[i] & 1, i == 3 ? 1 : 0);
EXPECT_EQ(mapping.bytes()[3 * page], 73);
EXPECT_EQ(msync(mapping.addr, mapping.size, MS_SYNC), 0) << strerror(errno);
}
TEST_P(InternalShmem, PopulateAndProtectionChangesKeepTheSameBacking) {
const size_t page = PageSize();
Mapping source(8 * page), alias(8 * page);
const int flags = MAP_SHARED | MAP_POPULATE | (GetParam() ? 0 : MAP_ANONYMOUS);
source.addr = mmap(nullptr, source.size, PROT_READ | PROT_WRITE, flags, zero_fd_, 0);
ASSERT_NE(source.addr, MAP_FAILED) << strerror(errno);
unsigned char resident[8] = {};
ASSERT_EQ(mincore(source.addr, source.size, resident), 0);
for (auto value : resident) EXPECT_EQ(value & 1, 1);
for (size_t i = 0; i < 8; ++i) source.bytes()[i * page] = 21 + i;
alias.addr = mremap(source.addr, 0, alias.size, MREMAP_MAYMOVE);
ASSERT_NE(alias.addr, MAP_FAILED) << strerror(errno);
// The alias has not faulted yet: residency must come from the backing.
ASSERT_EQ(mincore(alias.addr, alias.size, resident), 0);
for (auto value : resident) EXPECT_EQ(value & 1, 1);
ASSERT_EQ(mprotect(source.addr, source.size, PROT_NONE), 0);
for (size_t i = 0; i < 8; ++i) {
EXPECT_EQ(alias.bytes()[i * page], 21 + i);
alias.bytes()[i * page + 1] = 81 + i;
}
ASSERT_EQ(mprotect(source.addr, source.size, PROT_READ | PROT_WRITE), 0);
for (size_t i = 0; i < 8; ++i) {
EXPECT_EQ(source.bytes()[i * page], 21 + i);
EXPECT_EQ(source.bytes()[i * page + 1], 81 + i);
}
}
TEST_P(InternalShmem, ConcurrentFirstFaultsPreserveBothWriters) {
const size_t page = PageSize();
constexpr size_t rounds = 64;
Mapping mapping(rounds * page);
Map(mapping); ASSERT_NE(mapping.addr, MAP_FAILED);
unsigned char resident[rounds] = {};
ASSERT_EQ(mincore(mapping.addr, mapping.size, resident), 0);
for (auto value : resident) ASSERT_EQ(value & 1, 0);
int start[2], done[2];
ASSERT_EQ(pipe(start), 0);
if (pipe(done) != 0) {
close(start[0]); close(start[1]);
FAIL() << "pipe: " << strerror(errno);
}
const pid_t child = fork();
if (child < 0) {
close(start[0]); close(start[1]); close(done[0]); close(done[1]);
FAIL() << "fork: " << strerror(errno);
}
if (child == 0) {
close(start[1]); close(done[0]);
char token = 0;
for (size_t i = 0; i < rounds; ++i) {
if (write(done[1], &token, 1) != 1 || read(start[0], &token, 1) != 1) _exit(1);
mapping.bytes()[i * page + 1] = 101 + i;
if (write(done[1], &token, 1) != 1) _exit(2);
}
_exit(0);
}
close(start[0]); close(done[1]);
bool synchronized = true;
char token = 0;
for (size_t i = 0; i < rounds; ++i) {
// Both processes are ready before either touches this cold page.
if (read(done[0], &token, 1) != 1 || write(start[1], &token, 1) != 1) {
synchronized = false;
break;
}
mapping.bytes()[i * page] = 31 + i;
if (read(done[0], &token, 1) != 1) {
synchronized = false;
break;
}
EXPECT_EQ(mapping.bytes()[i * page], 31 + i);
EXPECT_EQ(mapping.bytes()[i * page + 1], 101 + i);
}
close(start[1]); close(done[0]);
int status = 0;
ASSERT_EQ(waitpid(child, &status, 0), child);
EXPECT_TRUE(synchronized);
ASSERT_TRUE(WIFEXITED(status)); EXPECT_EQ(WEXITSTATUS(status), 0);
}
TEST_P(InternalShmem, SplitAndDuplicateKeepBackingOffsets) {
const size_t page = PageSize();
Mapping mapping(3 * page);
Map(mapping); ASSERT_NE(mapping.addr, MAP_FAILED);
mapping.bytes()[2 * page] = 42;
MapInfo before, after;
ASSERT_TRUE(ReadMap(mapping.addr, &before));
ASSERT_EQ(mprotect(static_cast<char*>(mapping.addr) + page, page, PROT_READ), 0);
ASSERT_TRUE(ReadMap(static_cast<char*>(mapping.addr) + 2 * page, &after));
EXPECT_EQ(before.inode, after.inode); EXPECT_EQ(after.offset, 2 * page);
Mapping alias(page);
alias.addr = mremap(static_cast<char*>(mapping.addr) + 2 * page, 0, page, MREMAP_MAYMOVE);
ASSERT_NE(alias.addr, MAP_FAILED) << strerror(errno);
EXPECT_EQ(alias.bytes()[0], 42);
alias.bytes()[0] = 87;
EXPECT_EQ(mapping.bytes()[2 * page], 87);
ASSERT_TRUE(ReadMap(alias.addr, &after));
EXPECT_EQ(before.inode, after.inode); EXPECT_EQ(after.offset, 2 * page);
ASSERT_EQ(munmap(mapping.addr, mapping.size), 0); mapping.addr = MAP_FAILED;
EXPECT_EQ(alias.bytes()[0], 87);
}
TEST_P(InternalShmem, ShrinkThenMovePreservesBackingSizeAndContents) {
const size_t page = PageSize();
Mapping reservation(4 * page);
reservation.addr = mmap(nullptr, reservation.size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
ASSERT_NE(reservation.addr, MAP_FAILED);
Mapping original(2 * page);
Map(original, reservation.addr); ASSERT_NE(original.addr, MAP_FAILED);
original.bytes()[0] = 11; original.bytes()[page] = 22;
MapInfo initial, moved;
ASSERT_TRUE(ReadMap(original.addr, &initial));
ASSERT_EQ(mremap(original.addr, 2 * page, page, 0), original.addr);
void* destination = static_cast<char*>(reservation.addr) + page;
ASSERT_EQ(mremap(original.addr, page, 3 * page, MREMAP_MAYMOVE | MREMAP_FIXED,
destination), destination) << strerror(errno);
original.addr = MAP_FAILED; // The reservation owns the entire final range.
auto* bytes = static_cast<volatile unsigned char*>(destination);
EXPECT_EQ(bytes[0], 11); EXPECT_EQ(bytes[page], 22);
ASSERT_TRUE(ReadMap(destination, &moved));
EXPECT_EQ(initial.inode, moved.inode);
const int status = ReadInChild(bytes + 2 * page);
ASSERT_GE(status, 0); ASSERT_TRUE(WIFSIGNALED(status)); EXPECT_EQ(WTERMSIG(status), SIGBUS);
}
TEST_P(InternalShmem, FutexWakeUsesBackingIdentityAcrossAliases) {
Mapping source(PageSize()), independent(PageSize()), alias(PageSize());
Map(source); Map(independent);
ASSERT_NE(source.addr, MAP_FAILED); ASSERT_NE(independent.addr, MAP_FAILED);
alias.addr = mremap(source.addr, 0, alias.size, MREMAP_MAYMOVE);
ASSERT_NE(alias.addr, MAP_FAILED);
auto* word = static_cast<int*>(source.addr);
*word = 0;
std::atomic<bool> started{false};
long waited = -2;
int wait_errno = 0;
std::thread waiter([&] {
timespec timeout{2, 0};
started.store(true, std::memory_order_release);
waited = syscall(SYS_futex, word, FUTEX_WAIT, 0, &timeout, nullptr, 0);
wait_errno = errno;
});
long woke = 0;
long wrong_wakes = 0;
for (unsigned i = 0; i < 1000 && woke == 0; ++i) {
if (started.load(std::memory_order_acquire)) {
const long wrong = syscall(SYS_futex, independent.addr, FUTEX_WAKE, 1, nullptr, nullptr, 0);
if (wrong != 0) wrong_wakes = wrong;
woke = syscall(SYS_futex, alias.addr, FUTEX_WAKE, 1, nullptr, nullptr, 0);
}
if (woke == 0) usleep(1000);
}
waiter.join();
EXPECT_EQ(wrong_wakes, 0);
EXPECT_EQ(woke, 1); EXPECT_EQ(waited, 0) << strerror(wait_errno);
}
// /proc/meminfo uses batched global counters on Linux. Use a sizeable mapping
// and tolerate one MiB of batching/background activity, rather than assert an
// unstable exact single-page delta. Dedicated guest runs provide the accounting
// evidence; the content assertion independently checks ownership after unmap.
long long ShmemBytes() {
FILE* file = fopen("/proc/meminfo", "r");
if (!file) return -1;
char line[256];
long long kb = -1;
while (fgets(line, sizeof(line), file)) {
if (sscanf(line, "Shmem: %lld kB", &kb) == 1) break;
}
fclose(file);
return kb < 0 ? -1 : kb * 1024;
}
TEST_P(InternalShmem, LastAliasRetainsPagesThenReleasesShmemAccounting) {
const size_t length = 8 * 1024 * 1024;
const long long tolerance = 1024 * 1024;
const long long before = ShmemBytes();
ASSERT_GE(before, 0);
Mapping source(length), alias(length);
Map(source); ASSERT_NE(source.addr, MAP_FAILED);
for (size_t i = 0; i < length; i += PageSize()) source.bytes()[i] = 53;
const long long allocated = ShmemBytes();
EXPECT_GE(allocated, before + static_cast<long long>(length) - tolerance);
alias.addr = mremap(source.addr, 0, length, MREMAP_MAYMOVE);
ASSERT_NE(alias.addr, MAP_FAILED);
ASSERT_EQ(munmap(source.addr, source.size), 0); source.addr = MAP_FAILED;
for (size_t i = 0; i < length; i += PageSize()) ASSERT_EQ(alias.bytes()[i], 53);
EXPECT_GE(ShmemBytes(), allocated - tolerance);
ASSERT_EQ(munmap(alias.addr, alias.size), 0); alias.addr = MAP_FAILED;
EXPECT_LE(ShmemBytes(), before + tolerance);
}
INSTANTIATE_TEST_SUITE_P(AnonymousAndZero, InternalShmem, testing::Bool(),
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "DevZero" : "Anonymous";
});
TEST(InternalShmemSysV, PrivateSegmentsHaveKeyNamesAndDistinctInodes) {
const int first = shmget(IPC_PRIVATE, PageSize(), 0600);
ASSERT_GE(first, 0) << strerror(errno);
const int second = shmget(IPC_PRIVATE, PageSize(), 0600);
if (second < 0) {
const int error = errno;
shmctl(first, IPC_RMID, nullptr);
FAIL() << strerror(error);
return;
}
void* a = shmat(first, nullptr, 0);
void* b = shmat(second, nullptr, 0);
// Mark both before assertions so a failure cannot leak persistent IPC ids.
EXPECT_EQ(shmctl(first, IPC_RMID, nullptr), 0);
EXPECT_EQ(shmctl(second, IPC_RMID, nullptr), 0);
EXPECT_NE(a, MAP_FAILED); EXPECT_NE(b, MAP_FAILED);
if (a != MAP_FAILED && b != MAP_FAILED) {
MapInfo left, right;
const bool read_left = ReadMap(a, &left), read_right = ReadMap(b, &right);
EXPECT_TRUE(read_left); EXPECT_TRUE(read_right);
if (read_left && read_right) {
// Linux may expose shmid 0 as the first SysV inode number.
EXPECT_NE(left.inode, right.inode);
EXPECT_NE(left.line.find("/SYSV00000000 (deleted)"), std::string::npos) << left.line;
EXPECT_NE(right.line.find("/SYSV00000000 (deleted)"), std::string::npos) << right.line;
}
}
if (a != MAP_FAILED) { EXPECT_EQ(shmdt(a), 0); }
if (b != MAP_FAILED) { EXPECT_EQ(shmdt(b), 0); }
}
TEST(InternalShmemZero, SharedOffsetUsesFixedBackingPrivateAndReadonlyDoNot) {
const size_t page = PageSize();
int fd = open("/dev/zero", O_RDWR); ASSERT_GE(fd, 0);
Mapping shared(2 * page), private_map(2 * page);
shared.addr = mmap(nullptr, shared.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, page);
private_map.addr = mmap(nullptr, private_map.size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, page);
close(fd);
ASSERT_NE(shared.addr, MAP_FAILED); ASSERT_NE(private_map.addr, MAP_FAILED);
EXPECT_EQ(shared.bytes()[0], 0);
const int status = ReadInChild(shared.bytes() + page);
ASSERT_GE(status, 0); ASSERT_TRUE(WIFSIGNALED(status)); EXPECT_EQ(WTERMSIG(status), SIGBUS);
EXPECT_EQ(private_map.bytes()[page], 0); private_map.bytes()[page] = 9;
fd = open("/dev/zero", O_RDONLY); ASSERT_GE(fd, 0);
Mapping readonly(page);
readonly.addr = mmap(nullptr, page, PROT_READ, MAP_SHARED, fd, page);
close(fd); ASSERT_NE(readonly.addr, MAP_FAILED);
const int read_status = ReadInChild(readonly.bytes());
ASSERT_GE(read_status, 0); ASSERT_TRUE(WIFEXITED(read_status)); EXPECT_EQ(WEXITSTATUS(read_status), 0);
MapInfo info; ASSERT_TRUE(ReadMap(readonly.addr, &info));
EXPECT_NE(info.line.find("r--s"), std::string::npos) << info.line;
EXPECT_NE(info.line.find("/dev/zero"), std::string::npos) << info.line;
EXPECT_EQ(info.line.find("(deleted)"), std::string::npos) << info.line;
errno = 0; EXPECT_EQ(mprotect(readonly.addr, page, PROT_READ | PROT_WRITE), -1);
EXPECT_EQ(errno, EACCES);
}
TEST(InternalShmemZero, ReadonlyZeroSharedFutexReturnsEfault) {
const int fd = open("/dev/zero", O_RDONLY);
ASSERT_GE(fd, 0);
for (int flags : {MAP_SHARED, MAP_PRIVATE}) {
SCOPED_TRACE(flags);
Mapping mapping(PageSize());
mapping.addr = mmap(nullptr, mapping.size, PROT_READ, flags, fd, 0);
if (mapping.addr == MAP_FAILED) {
close(fd);
FAIL() << strerror(errno);
}
EXPECT_EQ(mapping.bytes()[0], 0);
const timespec timeout = {0, 0};
errno = 0;
EXPECT_EQ(syscall(SYS_futex, mapping.addr, FUTEX_WAIT, 0,
&timeout, nullptr, 0), -1);
EXPECT_EQ(errno, EFAULT);
errno = 0;
EXPECT_EQ(syscall(SYS_futex, mapping.addr, FUTEX_WAKE, 1,
nullptr, nullptr, 0), -1);
EXPECT_EQ(errno, EFAULT);
// FUTEX_PRIVATE uses the address-space key without anonymous-page
// write admission; a readable word can still time out normally.
errno = 0;
EXPECT_EQ(syscall(SYS_futex, mapping.addr, FUTEX_WAIT_PRIVATE, 0,
&timeout, nullptr, 0), -1);
EXPECT_EQ(errno, ETIMEDOUT);
EXPECT_EQ(syscall(SYS_futex, mapping.addr, FUTEX_WAKE_PRIVATE, 1,
nullptr, nullptr, 0), 0);
}
close(fd);
}
TEST(InternalShmemZero, WriteOnlyFdCannotMapEvenWithProtNone) {
int fd = open("/dev/zero", O_WRONLY); ASSERT_GE(fd, 0);
for (int flags : {MAP_SHARED, MAP_PRIVATE}) {
Mapping mapping(PageSize());
errno = 0; mapping.addr = mmap(nullptr, mapping.size, PROT_NONE, flags, fd, 0);
const int error = errno;
EXPECT_EQ(mapping.addr, MAP_FAILED); EXPECT_EQ(error, EACCES);
}
close(fd);
}
} // namespace
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+2
View File
@@ -131,3 +131,5 @@ normal/epoll_pwait2_semantics
normal/elf_file_backing
normal/exec_write_access
normal/internal_shmem