Index: sys/compat/linux/linux_misc.c =================================================================== --- sys/compat/linux/linux_misc.c +++ sys/compat/linux/linux_misc.c @@ -586,11 +586,23 @@ return (error); } +/* + * Expand or shrink an existing memory mapping, potentially moving it as well + * + * Without a direct analog in FreeBSD, model this as mmap with a bcopy if the + * mapping moves. + */ int linux_mremap(struct thread *td, struct linux_mremap_args *args) { + vm_map_t map; + vm_map_entry_t entry; + vm_prot_t prot, maxprot; + vm_object_t obj = NULL; + int eflags; + boolean_t found; uintptr_t addr; - size_t len; + uintptr_t new_addr = 0; int error = 0; #ifdef DEBUG @@ -602,35 +614,171 @@ (unsigned long)args->flags); #endif - if (args->flags & ~(LINUX_MREMAP_FIXED | LINUX_MREMAP_MAYMOVE)) { - td->td_retval[0] = 0; + /* + * If anything goes wrong, the return value is MAP_FAILED. Set it + * here to simplify the error handling code. If the mapping succeeds, + * the return value will also be set at the end. + */ + td->td_retval[0] = (uintptr_t)MAP_FAILED; + + if (args->flags & ~(LINUX_MREMAP_FIXED | LINUX_MREMAP_MAYMOVE)) return (EINVAL); - } + + if ((args->flags & (LINUX_MREMAP_FIXED | LINUX_MREMAP_MAYMOVE)) == + LINUX_MREMAP_FIXED) + return (EINVAL); + + addr = args->addr; /* * Check for the page alignment. * Linux defines PAGE_MASK to be FreeBSD ~PAGE_MASK. */ - if (args->addr & PAGE_MASK) { - td->td_retval[0] = 0; + if (addr & PAGE_MASK) + return (EINVAL); + + if (args->new_len == 0) return (EINVAL); + + if (args->new_len == args->old_len && !(args->flags & LINUX_MREMAP_FIXED)) { + td->td_retval[0] = (uintptr_t)addr; + return (0); } args->new_len = round_page(args->new_len); args->old_len = round_page(args->old_len); - if (args->new_len > args->old_len) { - td->td_retval[0] = 0; + if (args->flags & LINUX_MREMAP_FIXED) { + new_addr = args->new_addr; + + if (new_addr & PAGE_MASK) + return (EINVAL); + + /* Check for overlapped addresses */ + if ((new_addr >= addr) && + (new_addr < (addr + args->new_len))) { + return (EINVAL); + } + + if ((addr > new_addr) && + (addr < (new_addr + args->new_len))) { + return (EINVAL); + } + } + + map = &td->td_proc->p_vmspace->vm_map; + + /* Range check the address and length */ + if (addr + args->old_len > vm_map_max(map)) + return (EFAULT); + + if (addr + args->old_len < addr) + return (EFAULT); + + vm_map_lock_read(map); + found = vm_map_lookup_entry(map, addr, &entry); + if (found) { + prot = entry->protection; + maxprot = entry->max_protection; + eflags = entry->eflags; + obj = entry->object.vm_object; + } + vm_map_unlock_read(map); + + if (!found) + return (EFAULT); + + /* + * If the requested new length is smaller than the current and the + * new address, if any, matches the old address, free the tail of + * this allocation. + */ + if ((args->new_len < args->old_len) && + (!new_addr || (addr == new_addr))) { + error = kern_munmap(td, + addr + args->new_len, + args->old_len - args->new_len); + goto out; + } + + /* + * If the allocation isn't moving, try to extend the existing mapping + */ + if (!new_addr) { + uintptr_t req_addr; + size_t delta_size; + + addr += args->old_len; + + req_addr = addr; + + delta_size = args->new_len - args->old_len; + + /* + * The new mapping protection / flags should match the current + * mapping + */ + error = vm_mmap_object(map, &addr, + delta_size, + prot, + maxprot, + eflags, + obj, 0, FALSE, td); + if (error) + return (error); + + if (addr == req_addr) { + /* + * The VM system was able to extend the existing map + * entry. Reset addr to reflect the original address + */ + addr = args->addr; + //TODO might need a merge here ... + goto out; + } else { + /* No room to extend. Free this and retry elsewhere */ + error = kern_munmap(td, addr, delta_size); + if (error) + return (error); + } + } + + /* + * Allocate a new region either because the user requested a specific + * address or the existing map entry could not be extended. + * + * Unless, of course, the map is not allowed to move. + */ + if (!(args->flags & LINUX_MREMAP_MAYMOVE)) { return (ENOMEM); } - if (args->new_len < args->old_len) { - addr = args->addr + args->new_len; - len = args->old_len - args->new_len; - error = kern_munmap(td, addr, len); + error = vm_mmap_object(map, &new_addr, args->new_len, + prot, + maxprot, + eflags | (args->flags & LINUX_MREMAP_FIXED ? + MAP_FIXED : 0), + obj, 0, FALSE, td); + if (error) + return (error); + + /* + * If the VM system was not able to allocate a map entry at the + * requested address, free the memory and indicate the error. + */ + if ((args->flags & LINUX_MREMAP_FIXED) && (new_addr != args->new_addr)) { + kern_munmap(td, new_addr, args->new_len); + return (ENOMEM); } - td->td_retval[0] = error ? 0 : (uintptr_t)args->addr; + /* Copy the data from the old address to the new one */ + pmap_copy(map->pmap, map->pmap, new_addr, args->old_len, args->addr); + + addr = new_addr; + + kern_munmap(td, args->addr, args->old_len); +out: + td->td_retval[0] = error ? (uintptr_t)MAP_FAILED : (uintptr_t)addr; return (error); }