diff --git a/src/portal/portal_devfs.c b/src/portal/portal_devfs.c index 3fbd1ae..5509f8f 100644 --- a/src/portal/portal_devfs.c +++ b/src/portal/portal_devfs.c @@ -145,54 +145,67 @@ static void igloo_devfs_flush_shm_to_hypervisor(struct file *file, struct portal { void *buffer; loff_t size; - ssize_t bytes; - loff_t pos = 0; + loff_t off; + size_t stage_sz; if (!pe || !pe->shm_file) return; if (!file->f_op || !file->f_op->write) return; // Python didn't provide a write hook mutex_lock(&pe->shm_lock); - + // Get the exact size of the shared memory file size = i_size_read(file_inode(pe->shm_file)); if (size <= 0) goto unlock; + // Flush back in bounded chunks. The mmap length is guest-controlled, so a single + // (k)vzalloc(size) is an unbounded allocation that fails on 32-bit donor kernels for + // large device mmaps (vmalloc space is only ~128-240 MB) and lets a guest force huge + // kernel allocations on demand. A fixed staging buffer keeps this O(1) in kernel memory. + stage_sz = (size < (loff_t)IGLOO_DEVFS_STAGE_SZ) ? (size_t)size : IGLOO_DEVFS_STAGE_SZ; #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 12, 0) - buffer = kvzalloc(size, GFP_KERNEL); + buffer = kvzalloc(stage_sz, GFP_KERNEL); #else - buffer = vzalloc(size); + buffer = vzalloc(stage_sz); #endif + if (!buffer) goto unlock; + + for (off = 0; off < size; off += stage_sz) { + size_t chunk = (size - off < (loff_t)stage_sz) ? (size_t)(size - off) : stage_sz; + loff_t rpos = off; + loff_t wpos = off; + ssize_t bytes; - if (buffer) { - // 1. Read the modified data from the hidden RAM file + // 1. Read this window of modified data from the hidden RAM file #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) - bytes = kernel_read(pe->shm_file, buffer, size, &pos); + bytes = kernel_read(pe->shm_file, buffer, chunk, &rpos); #else - mm_segment_t old_fs = get_fs(); - set_fs(KERNEL_DS); - bytes = vfs_read(pe->shm_file, (char __user *)buffer, size, &pos); - set_fs(old_fs); + { + mm_segment_t old_fs = get_fs(); + set_fs(KERNEL_DS); + bytes = vfs_read(pe->shm_file, (char __user *)buffer, chunk, &rpos); + set_fs(old_fs); + } #endif + if (bytes <= 0) break; - // 2. Push it back to the Python plugin via the trampoline - if (bytes > 0) { - pos = 0; + // 2. Push it back to the Python plugin via the trampoline (at the same offset) #if LINUX_VERSION_CODE < KERNEL_VERSION(5, 10, 0) - old_fs = get_fs(); + { + mm_segment_t old_fs = get_fs(); set_fs(KERNEL_DS); - file->f_op->write(file, (const char __user *)buffer, bytes, &pos); + file->f_op->write(file, (const char __user *)buffer, bytes, &wpos); set_fs(old_fs); + } #else - file->f_op->write(file, (const char __user *)buffer, bytes, &pos); + file->f_op->write(file, (const char __user *)buffer, bytes, &wpos); #endif - } + } #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 12, 0) - kvfree(buffer); + kvfree(buffer); #else - vfree(buffer); + vfree(buffer); #endif - } unlock: mutex_unlock(&pe->shm_lock); @@ -254,23 +267,34 @@ static int igloo_devfs_proxy_mmap(struct file *file, struct vm_area_struct *vma) #endif if (!IS_ERR(shm_file)) { + // Seed the sparse shmem backing in bounded chunks. The mmap length is + // guest-controlled, so a single (k)vzalloc(size) is unbounded and fails on + // 32-bit donor kernels for large device mmaps (issue #79). igloo_fetch_mmap_page() + // honors the offset, so a fixed staging buffer suffices regardless of mmap length. + size_t stage_sz = (size < IGLOO_DEVFS_STAGE_SZ) ? size : (size_t)IGLOO_DEVFS_STAGE_SZ; + void *buffer; #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 12, 0) - void *buffer = kvzalloc(size, GFP_KERNEL); + buffer = kvzalloc(stage_sz, GFP_KERNEL); #else - void *buffer = vzalloc(size); + buffer = vzalloc(stage_sz); #endif if (buffer) { - ssize_t bytes = igloo_fetch_mmap_page(file, buffer, 0, size); - if (bytes > 0) { - loff_t pos = 0; + size_t off; + for (off = 0; off < size; off += stage_sz) { + size_t chunk = (size - off < stage_sz) ? (size - off) : stage_sz; + ssize_t bytes = igloo_fetch_mmap_page(file, buffer, off, chunk); + if (bytes > 0) { + loff_t pos = off; #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) - kernel_write(shm_file, buffer, bytes, &pos); + kernel_write(shm_file, buffer, bytes, &pos); #else - mm_segment_t old_fs = get_fs(); - set_fs(KERNEL_DS); - vfs_write(shm_file, (char __user *)buffer, bytes, &pos); - set_fs(old_fs); + mm_segment_t old_fs = get_fs(); + set_fs(KERNEL_DS); + vfs_write(shm_file, (char __user *)buffer, bytes, &pos); + set_fs(old_fs); #endif + } + if (bytes <= 0) break; // stop on short/failed fetch } #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 12, 0) kvfree(buffer); diff --git a/src/portal/portal_internal.h b/src/portal/portal_internal.h index 568a6d0..9e7cb99 100644 --- a/src/portal/portal_internal.h +++ b/src/portal/portal_internal.h @@ -38,6 +38,12 @@ bool igloo_is_kernel_addr(unsigned long addr); #define CHUNK_SIZE (PAGE_SIZE - sizeof(region_header)) +// Fixed staging-buffer size for seeding/flushing mmap'd pseudofiles. The shmem backing +// is sparse, so the seed/flush trampoline copies in bounded chunks instead of allocating +// a single buffer the size of the (guest-controlled) mmap length. This keeps kernel memory +// O(1) regardless of mmap size and avoids unbounded (k)vzalloc() failures on 32-bit donors. +#define IGLOO_DEVFS_STAGE_SZ (64 * 1024) + // Define handler function type typedef void (*portal_op_handler)(portal_region *mem_region); diff --git a/src/portal/portal_procfs.c b/src/portal/portal_procfs.c index 9bfc6dd..817e83a 100644 --- a/src/portal/portal_procfs.c +++ b/src/portal/portal_procfs.c @@ -121,25 +121,35 @@ int igloo_proxy_mmap(struct file *file, struct vm_area_struct *vma) #endif if (!IS_ERR(shm_file)) { - // kvzalloc introduced in 4.12. Fall back to vzalloc for older kernels. + // Seed the sparse shmem backing in bounded chunks. The mmap length is + // guest-controlled, so a single (k)vzalloc(size) is unbounded and fails on + // 32-bit donor kernels for large device mmaps (issue #79). igloo_fetch_mmap_page() + // honors the offset, so a fixed staging buffer suffices regardless of mmap length. + size_t stage_sz = (size < IGLOO_DEVFS_STAGE_SZ) ? size : (size_t)IGLOO_DEVFS_STAGE_SZ; + void *buffer; #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 12, 0) - void *buffer = kvzalloc(size, GFP_KERNEL); + buffer = kvzalloc(stage_sz, GFP_KERNEL); #else - void *buffer = vzalloc(size); + buffer = vzalloc(stage_sz); #endif if (buffer) { - ssize_t bytes = igloo_fetch_mmap_page(file, buffer, 0, size); - if (bytes > 0) { - loff_t pos = 0; + size_t off; + for (off = 0; off < size; off += stage_sz) { + size_t chunk = (size - off < stage_sz) ? (size - off) : stage_sz; + ssize_t bytes = igloo_fetch_mmap_page(file, buffer, off, chunk); + if (bytes > 0) { + loff_t pos = off; #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) - kernel_write(shm_file, buffer, bytes, &pos); + kernel_write(shm_file, buffer, bytes, &pos); #else - // FIX: Use vfs_write to safely handle the write vs write_iter abstraction - mm_segment_t old_fs = get_fs(); - set_fs(KERNEL_DS); - vfs_write(shm_file, (char __user *)buffer, bytes, &pos); - set_fs(old_fs); + // FIX: Use vfs_write to safely handle the write vs write_iter abstraction + mm_segment_t old_fs = get_fs(); + set_fs(KERNEL_DS); + vfs_write(shm_file, (char __user *)buffer, bytes, &pos); + set_fs(old_fs); #endif + } + if (bytes <= 0) break; // stop on short/failed fetch } #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 12, 0) kvfree(buffer);