mmap 주소 선택은 빈 주소가 아니라 충분한 gap을 찾는 일입니다

주소 공간에 여유가 많아 보여도 큰 mapping 하나를 놓을 연속 구간이 없을 수 있습니다. mmap()의 주소 선택은 free RAM 검색과 관계가 없습니다. 이미 존재하는 VMA와 stack guard를 피하면서 길이·정렬·architecture 한계를 만족하는 virtual interval을 찾는 작업입니다.

누가 주소 선택 함수를 고르는가

RB-tree 기반 구현의 get_unmapped_area()는 architecture 검사를 거친 뒤 file의 get_unmapped_area callback을 우선할 수 있습니다. shared anonymous 요청은 shmem 전용 경로를 이용할 수 있으며, 나머지는 process가 선택한 일반 handler로 이어집니다. filesystem callback도 일반 탐색을 확장해 특수 정렬과 huge-page 조건을 반영하는 경우가 많습니다.

이 dispatcher는 먼저 arch_mmap_check()와 len > TASK_SIZE를 검사합니다. x86에서 architecture hook이 아무것도 하지 않는 경우가 있어도 다른 architecture의 계약까지 빈 검사인 것은 아닙니다. file callback이 없을 때 shared anonymous를 shmem_get_unmapped_area()로 보내는 것은 shmem의 huge-page 배치를 고려하기 위해서입니다. 그 내부 객체의 시작을 mapping하므로 offset도 0으로 맞춥니다.

반환 주소가 있다고 곧바로 신뢰하지는 않습니다. task 범위, length를 더했을 때의 끝, page 정렬과 security 조건을 다시 검사합니다. callback의 성공과 최종 mapping 허용이 별도인 이유입니다. FIXED 요청은 탐색할 자유를 없애지만 이런 유효성 검사를 모두 없애지는 않습니다.

구체적으로 error-valued 주소를 먼저 걸러 낸 뒤 addr > TASK_SIZE - len이면 ENOMEM, page 경계에 맞지 않으면 EINVAL로 끝납니다. 마지막 security_mmap_addr()도 통과해야 합니다. 앞의 length 검사가 있어야 뺄셈 형태의 끝 검사도 안전합니다. architecture handler가 MAP_FIXED에 대해 입력 주소를 바로 반환하는 것과 공통 dispatcher가 최종 주소를 검증하는 것은 서로 모순되지 않습니다.

hint를 수용할 수 없는 세 가지 이유

일반 x86-64 top-down 경로에서는 hint를 page 단위로 맞추고, 끝 주소가 허용 범위를 넘는지 확인하며, 기존 VMA와 guard-adjusted gap을 침범하는지 봅니다. 5-level paging에서는 기존 application의 주소 가정 때문에 특정 기본 mapping window 경계를 가로지르는 hint도 주의해야 합니다. hint가 부적합하면 다른 위치를 찾는 것이며, FIXED처럼 같은 주소에서 반드시 실패하는 계약이 아닙니다.

legacy layout과 MAP_32BIT는 bottom-up 선택에 영향을 줄 수 있습니다. 여기서 다루는 x86 mmap_is_legacy()는 ADDR_COMPAT_LAYOUT personality 또는 sysctl_legacy_va_layout을 검사합니다. stack limit 자체가 이 predicate의 조건은 아닙니다. setarch 등으로 process personality를 설정하는 것과 system-wide sysctl을 바꾸는 것도 범위가 다릅니다. 실행 image를 준비하는 setup_new_exec() 계열의 layout 설정이 일반 검색 방향을 고르지만, 개별 요청에 MAP_32BIT가 붙으면 native 64-bit syscall에서도 bottom-up 경로를 선택할 수 있습니다. 32-bit compatibility syscall인지의 판정도 별도로 있습니다.

hint가 채택되지 않은 top-down 검색은 낮은 쪽을 PAGE_SIZE, 높은 쪽을 해당 ABI의 mmap base로 제한합니다. 64-bit process가 기본 window보다 높은 hint를 주었다면 높은 주소를 사용할 의사를 표시한 것으로 보고 5-level 주소 공간 쪽으로 검색 상한을 넓힐 수 있습니다. compatibility ABI에 같은 확대를 무조건 적용하지는 않습니다. file mapping에는 page offset과 architecture의 alignment bits를 반영한 align_mask·align_offset도 전달합니다.

top-down에서 ENOMEM을 받았다고 반드시 전체 mmap()이 끝나는 것도 아닙니다. 큰 stack limit이나 여러 큰 mapping 때문에 위쪽 검색 공간이 부족하면 bottom-up 검색을 재시도하는 경로가 있습니다. 이 fallback은 기존 VMA를 덮어쓰는 권한을 주지 않으며, 다른 방향의 검색이 유효한 gap을 찾을 기회를 주는 것입니다.

mmap base와 ASLR의 단위

top-down mmap base는 user address space의 위쪽에서 stack을 위한 gap과 random offset을 제외하여 정합니다. v6.18 x86의 mmap_base()는 stack limit에 stack randomization과 guard 여유를 더하되 overflow를 경계하고, gap을 최소 128 MiB와 task-size의 5/6 사이로 제한합니다. “stack unlimited이면 주소 선택도 무제한”이라는 관계가 아닙니다.

randomization bit 수를 byte 수로 바로 읽어서도 안 됩니다. arch_rnd()는 난수의 하위 bit를 고른 뒤 PAGE_SHIFT만큼 이동합니다. 예를 들어 4 KiB page에서 28 bits는 최대 2^28개의 page 단위 선택 폭이며, 단순히 256 MiB라는 뜻이 아닙니다. 실제 bit 설정과 허용 값은 configuration과 process ABI에 영향을 받습니다.

이 예의 offset 간격은 4 KiB이고 전체 선택 폭은 1 TiB입니다. 최대 offset은 그 폭에서 한 page를 뺀 값입니다. 한편 stack randomization 여유는 mmap용 28 bits와 별도 계산입니다. v6.18 native x86-64의 __STACK_RND_MASK(false) 값 0x3fffff에 4 KiB를 곱하면 16 GiB - 4 KiB입니다. header의 오래된 주석이나 1 GiB라는 설명보다 실제 mask·page shift·ABI 조건을 함께 보아야 합니다. PF_RANDOMIZE가 꺼지면 이 stack randomization 여유는 0입니다. Linux v6.18 x86 ELF의 stack mask

mmap_base()는 stack limit에 stack randomization 최대 여유와 guard gap을 더하되 덧셈 overflow를 검사하고, 얻은 gap을 최소 128 MiB와 task size의 5/6 사이로 clamp합니다. 그 뒤 task size에서 gap과 별도의 mmap random offset을 뺀 주소를 page-align합니다. stack limit이 layout 선택 조건이라는 설명과, 여기서 주소 base의 입력이라는 설명을 섞으면 legacy 선택과 top-down 실패를 잘못 진단하게 됩니다.

RB-tree gap 검색을 따라가 봅니다

RB-tree 기반 구현의 unmapped_area_topdown()은 VMA 자체의 크기를 검색하지 않습니다. 각 VMA 앞의 빈 구간과 그 subtree에 존재하는 최대 gap을 요약한 rb_subtree_gap을 사용합니다. root 요약부터 충분한 gap이 없음을 알 수 있으면 전체 tree를 훑지 않고 실패할 수 있습니다.

먼저 mapping들이 끝난 뒤의 최상단 gap이 요청을 수용하는지 확인합니다. 아니면 높은 주소를 선호하므로 오른쪽 subtree를 먼저 탐색합니다. 현재 node 앞의 gap이 부족하면 왼쪽 subtree를 보며, leaf에서 실패하면 아직 검사하지 않은 왼쪽 영역이 있는 ancestor로 되돌아갑니다. subtree의 최대 gap이 부족한 쪽은 건너뜁니다.

gap_start는 앞 VMA의 끝, gap_end는 뒤 VMA의 시작에서 얻되 growth flag에 따라 stack guard를 반영합니다. 요청 범위의 low/high limit도 함께 적용합니다. 충분한 gap을 찾으면 그 위쪽 끝에서 length를 빼 주소를 선택합니다. 따라서 같은 VMA 집합이라도 요청 length와 alignment가 달라지면 결과도 달라집니다.

검색 조건을 interval로 바꾸어 봅니다

요청 길이를 N, 허용 범위를 [L, H), guard를 반영한 gap을 [S, E)라고 하면, 양쪽 구간이 겹친다는 사실만으로는 부족합니다. gap 자체가 양수이고 E - S >= N이어야 하며, 시작을 제한 안에 둘 수 있도록 S <= H - N, E >= L + N도 필요합니다. alignment 여유가 없는 경우에는 먼저 H >= N과 L <= H - N을 확인하여 이후의 shifted limit 계산이 wrap하지 않도록 합니다. 별도 alignment가 있으면 이런 검색 한계에 정렬 여유를 포함한 내부 검색 길이를 사용하므로, 이를 최종 mapping 길이 N과 같은 값이라고 가정해서는 안 됩니다.

실제 RB-tree 검색은 guard를 반영해 보관한 highest_vm_end 뒤의 최상단 gap부터 시도합니다. 거기서 안 되면 root가 없는지와 root의 최대 gap 요약을 확인하고, 충분한 오른쪽 subtree를 우선합니다. 현재 gap의 끝이 shifted low limit보다 낮으면 더 낮은 후보도 불가능합니다. 현재 gap이 안 맞으면 왼쪽 subtree로 내려가며, 올라올 때에는 이미 끝낸 쪽을 다시 방문하지 않습니다. 오른쪽 자식 쪽에서 돌아온 ancestor에서 아직 남은 현재 gap과 왼쪽을 검사하고, root까지 모두 소진하면 실패합니다. traversal을 표시한 숫자는 이런 방문 순서이지 VMA의 실제 주소나 크기가 아닙니다.

독립적인 계산 예로 gap을 [0x10000, 0x18000), 허용 범위를 [0x11000, 0x17000), 길이를 0x3000으로 두겠습니다. 정렬 조건이 page 정렬뿐이라면 가능한 가장 높은 시작은 0x17000 - 0x3000 = 0x14000입니다. 원래 gap의 끝 0x18000에서 바로 빼면 허용 상한을 넘으므로 먼저 끝을 clamp해야 합니다. 일반 alignment가 더해지면 (start & align_mask)가 (align_offset & align_mask)와 같도록 시작을 낮춥니다. 이때도 최종 시작이 low limit과 guard-adjusted gap 시작보다 낮아지지 않아야 합니다. 이는 주소 계산 예이며 실행 측정값이 아닙니다.

이 RB-tree traversal은 RB-tree 알고리즘을 이해하기 위한 설명입니다. v6.18의 주소 index는 Maple Tree이므로 rb_subtree_gap traversal을 그 tag의 내부 구현으로 제시하지 않습니다. 재사용할 수 있는 관점은 “큰 VMA를 찾는 것이 아니라 제한 안의 충분한 빈 interval을 찾는다”는 질문입니다.

고전 구현에서 length와 alignment를 함께 읽습니다

아래는 Linux v5.18의 mm/mmap.c에 있는 unmapped_area_topdown() 전체입니다. 입력 구조체도 같은 tag의 include/linux/mm.h에서 함께 보겠습니다. 이 코드는 kernel 내부 context가 필요한 실제 upstream 함수이며, userspace에서 단독 실행하는 예제나 v6.18 구현은 아닙니다. 함수의 분기·주석을 생략하지 않고 tab만 네 spaces로 표시했습니다. C code는 원 source의 GPL-2.0-only 조건을 따르며, header의 GPL-2.0 표기도 같은 version-only 표기입니다. license 원문은 한국어 해설의 license 지정과 구분합니다.

/* SPDX-License-Identifier: GPL-2.0 */
/* Linux v5.18, include/linux/mm.h */
struct vm_unmapped_area_info {
#define VM_UNMAPPED_AREA_TOPDOWN 1
    unsigned long flags;
    unsigned long length;
    unsigned long low_limit;
    unsigned long high_limit;
    unsigned long align_mask;
    unsigned long align_offset;
};

length는 원하는 mapping 길이, low/high는 허용 검색 범위이며 align_mask와 align_offset은 시작 주소의 나머지 조건입니다. TOPDOWN bit는 높은 주소를 먼저 고르는 dispatcher 선택입니다. 내부 local length는 입력 구조체의 length와 값이 다를 수 있습니다.

// SPDX-License-Identifier: GPL-2.0-only
// Linux v5.18, mm/mmap.c. Written by obz.
// Address space accounting code: <alan@lxorguk.ukuu.org.uk>
static unsigned long unmapped_area_topdown(struct vm_unmapped_area_info *info)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma;
    unsigned long length, low_limit, high_limit, gap_start, gap_end;

    /* Adjust search length to account for worst case alignment overhead */
    length = info->length + info->align_mask;
    if (length < info->length)
        return -ENOMEM;

    /*
     * Adjust search limits by the desired length.
     * See implementation comment at top of unmapped_area().
     */
    gap_end = info->high_limit;
    if (gap_end < length)
        return -ENOMEM;
    high_limit = gap_end - length;

    if (info->low_limit > high_limit)
        return -ENOMEM;
    low_limit = info->low_limit + length;

    /* Check highest gap, which does not precede any rbtree node */
    gap_start = mm->highest_vm_end;
    if (gap_start <= high_limit)
        goto found_highest;

    /* Check if rbtree root looks promising */
    if (RB_EMPTY_ROOT(&mm->mm_rb))
        return -ENOMEM;
    vma = rb_entry(mm->mm_rb.rb_node, struct vm_area_struct, vm_rb);
    if (vma->rb_subtree_gap < length)
        return -ENOMEM;

    while (true) {
        /* Visit right subtree if it looks promising */
        gap_start = vma->vm_prev ? vm_end_gap(vma->vm_prev) : 0;
        if (gap_start <= high_limit && vma->vm_rb.rb_right) {
            struct vm_area_struct *right =
                rb_entry(vma->vm_rb.rb_right,
                     struct vm_area_struct, vm_rb);
            if (right->rb_subtree_gap >= length) {
                vma = right;
                continue;
            }
        }

check_current:
        /* Check if current node has a suitable gap */
        gap_end = vm_start_gap(vma);
        if (gap_end < low_limit)
            return -ENOMEM;
        if (gap_start <= high_limit &&
            gap_end > gap_start && gap_end - gap_start >= length)
            goto found;

        /* Visit left subtree if it looks promising */
        if (vma->vm_rb.rb_left) {
            struct vm_area_struct *left =
                rb_entry(vma->vm_rb.rb_left,
                     struct vm_area_struct, vm_rb);
            if (left->rb_subtree_gap >= length) {
                vma = left;
                continue;
            }
        }

        /* Go back up the rbtree to find next candidate node */
        while (true) {
            struct rb_node *prev = &vma->vm_rb;
            if (!rb_parent(prev))
                return -ENOMEM;
            vma = rb_entry(rb_parent(prev),
                       struct vm_area_struct, vm_rb);
            if (prev == vma->vm_rb.rb_right) {
                gap_start = vma->vm_prev ?
                    vm_end_gap(vma->vm_prev) : 0;
                goto check_current;
            }
        }
    }

found:
    /* We found a suitable gap. Clip it with the original high_limit. */
    if (gap_end > info->high_limit)
        gap_end = info->high_limit;

found_highest:
    /* Compute highest gap address at the desired alignment */
    gap_end -= info->length;
    gap_end -= (gap_end - info->align_offset) & info->align_mask;

    VM_BUG_ON(gap_end < info->low_limit);
    VM_BUG_ON(gap_end < gap_start);
    return gap_end;
}

local 검색 길이는 info->length + info->align_mask입니다. 최악의 alignment 손실까지 수용할 gap을 먼저 찾으며, 이 덧셈이 wrap하면 ENOMEM으로 끝납니다. shifted low/high 한계와 subtree pruning에도 이 확장 길이를 사용합니다. 마지막에는 clamped gap 끝에서 실제 요청 길이인 info->length를 빼고, (gap_end - info->align_offset) & info->align_mask만큼 더 내립니다. 따라서 alignment 때문에 잃는 공간을 두 번 요청 길이에 더하는 알고리즘이 아닙니다.

예를 들어 별도의 가상 계산에서 gap이 [0x10000, 0x22000), 요청 길이가 0x3000, mask가 0x3fff, offset이 0x1000, 허용 상한이 0x22000이라고 하겠습니다. 최악의 검색 길이는 0x6fff이므로 이 gap에 들어갑니다. 끝에서 실제 길이를 뺀 잠정 시작은 0x1f000이며, (0x1f000 - 0x1000) & 0x3fff = 0x2000만큼 내려 최종 시작 0x1d000을 얻습니다. 시작 주소의 mask 결과는 0x1000이고 mapping의 끝 0x20000도 상한 안에 남습니다. 이 수치는 실행 결과가 아니라 mask 연산을 확인하는 독립 예입니다.

이 함수의 two-limit 검사와 마지막 두 VM_BUG_ON()을 함께 읽어야 정렬 뒤에도 low limit과 gap 시작을 침범하지 않는다는 불변식이 연결됩니다. align_mask가 0인 경로에서는 확장 길이와 요청 길이가 같고 마지막 alignment 차감도 0이 됩니다. x86라는 이름만으로 모든 file mapping의 alignment mask가 항상 0이라고 가정해서는 안 됩니다.

v6.18의 공통 검색은 Maple Tree 기반입니다. 따라서 위의 mm_rb, vm_prev, rb_subtree_gap을 현대 tree의 field로 옮겨 읽지 않습니다. 고전 구현과 비교할 때 유지되는 계약은 허용 interval 안에서 요청 길이와 alignment를 동시에 만족하는 시작 주소를 찾는다는 점입니다.

v5.18 dispatcher와 x86 layout의 전체 경로

앞의 gap 검색에 어떤 입력이 도달하는지 공통 dispatcher와 x86의 선택 경로를 함께 보겠습니다. 다음 네 함수도 v5.18 전체 정의입니다. arch/x86/mm/mmap.c의 원 license는 GPL-2.0-or-later이며, mm/mmap.c의 GPL-2.0-only와 구분합니다. 전체 source 파일은 v5.18 코드 묶음에서 받으실 수 있습니다. 각 파일의 원 header와 license를 보존했으며, 한국어 해설과 upstream code의 license는 구분합니다. 이 묶음은 kernel 전체 build tree나 userspace 실행 예제가 아닙니다.

get_unmapped_area()

공식 v5.18 mm/mmap.c, L2224–L2264 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code    <alan@lxorguk.ukuu.org.uk>
 */
/* Linux v5.18: mm/mmap.c */
unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
        unsigned long pgoff, unsigned long flags)
{
    unsigned long (*get_area)(struct file *, unsigned long,
                  unsigned long, unsigned long, unsigned long);

    unsigned long error = arch_mmap_check(addr, len, flags);
    if (error)
        return error;

    /* Careful about overflows.. */
    if (len > TASK_SIZE)
        return -ENOMEM;

    get_area = current->mm->get_unmapped_area;
    if (file) {
        if (file->f_op->get_unmapped_area)
            get_area = file->f_op->get_unmapped_area;
    } else if (flags & MAP_SHARED) {
        /*
         * mmap_region() will call shmem_zero_setup() to create a file,
         * so use shmem's get_unmapped_area in case it can be huge.
         * do_mmap() will clear pgoff, so match alignment.
         */
        pgoff = 0;
        get_area = shmem_get_unmapped_area;
    }

    addr = get_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr;

    if (addr > TASK_SIZE - len)
        return -ENOMEM;
    if (offset_in_page(addr))
        return -EINVAL;

    error = security_mmap_addr(addr);
    return error ? error : addr;
}

file callback 또는 shmem 경로가 골라 준 주소에도 공통 범위·정렬·security 검사가 남습니다. 특정 callback을 사용한다는 사실이 이 최종 검사를 우회한다는 뜻은 아닙니다.

mmap_is_legacy()

공식 v5.18 arch/x86/mm/mmap.c, L62–L68 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Flexible mmap layout support
 *
 * Based on code by Ingo Molnar and Andi Kleen, copyrighted
 * as follows:
 *
 * Copyright 2003-2009 Red Hat Inc.
 * All Rights Reserved.
 * Copyright 2005 Andi Kleen, SUSE Labs.
 * Copyright 2007 Jiri Kosina, SUSE Labs.
 */
/* Linux v5.18: arch/x86/mm/mmap.c */
static int mmap_is_legacy(void)
{
    if (current->personality & ADDR_COMPAT_LAYOUT)
        return 1;

    return sysctl_legacy_va_layout;
}

이 predicate에는 stack limit 검사가 없습니다. personality와 system 설정으로 방향을 고르는 질문, stack limit을 mmap base 계산의 입력으로 사용하는 질문을 분리해야 합니다.

arch_get_unmapped_area_topdown()

공식 v5.18 arch/x86/kernel/sys_x86_64.c, L163–L231 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0
/* Linux v5.18: arch/x86/kernel/sys_x86_64.c */
unsigned long
arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
              const unsigned long len, const unsigned long pgoff,
              const unsigned long flags)
{
    struct vm_area_struct *vma;
    struct mm_struct *mm = current->mm;
    unsigned long addr = addr0;
    struct vm_unmapped_area_info info;

    /* requested length too big for entire address space */
    if (len > TASK_SIZE)
        return -ENOMEM;

    /* No address checking. See comment at mmap_address_hint_valid() */
    if (flags & MAP_FIXED)
        return addr;

    /* for MAP_32BIT mappings we force the legacy mmap base */
    if (!in_32bit_syscall() && (flags & MAP_32BIT))
        goto bottomup;

    /* requesting a specific address */
    if (addr) {
        addr &= PAGE_MASK;
        if (!mmap_address_hint_valid(addr, len))
            goto get_unmapped_area;

        vma = find_vma(mm, addr);
        if (!vma || addr + len <= vm_start_gap(vma))
            return addr;
    }
get_unmapped_area:

    info.flags = VM_UNMAPPED_AREA_TOPDOWN;
    info.length = len;
    info.low_limit = PAGE_SIZE;
    info.high_limit = get_mmap_base(0);

    /*
     * If hint address is above DEFAULT_MAP_WINDOW, look for unmapped area
     * in the full address space.
     *
     * !in_32bit_syscall() check to avoid high addresses for x32
     * (and make it no op on native i386).
     */
    if (addr > DEFAULT_MAP_WINDOW && !in_32bit_syscall())
        info.high_limit += TASK_SIZE_MAX - DEFAULT_MAP_WINDOW;

    info.align_mask = 0;
    info.align_offset = pgoff << PAGE_SHIFT;
    if (filp) {
        info.align_mask = get_align_mask();
        info.align_offset += get_align_bits();
    }
    addr = vm_unmapped_area(&info);
    if (!(addr & ~PAGE_MASK))
        return addr;
    VM_BUG_ON(addr != -ENOMEM);

bottomup:
    /*
     * A failed mmap() very likely causes application failure,
     * so fall back to the bottom-up function here. This scenario
     * can happen with large stack limits and large mmap()
     * allocations.
     */
    return arch_get_unmapped_area(filp, addr0, len, pgoff, flags);
}

MAP_32BIT의 bottom-up 선택, hint 검증 실패, 기본 window보다 높은 hint에 대한 검색 상한 확장, top-down ENOMEM 뒤의 fallback을 하나의 전체 정의에서 볼 수 있습니다. 높은 hint의 의사표시와 FIXED의 주소 고정은 서로 다른 분기입니다.

mmap_base()

공식 v5.18 arch/x86/mm/mmap.c, L82–L106 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Flexible mmap layout support
 *
 * Based on code by Ingo Molnar and Andi Kleen, copyrighted
 * as follows:
 *
 * Copyright 2003-2009 Red Hat Inc.
 * All Rights Reserved.
 * Copyright 2005 Andi Kleen, SUSE Labs.
 * Copyright 2007 Jiri Kosina, SUSE Labs.
 */
/* Linux v5.18: arch/x86/mm/mmap.c */
static unsigned long mmap_base(unsigned long rnd, unsigned long task_size,
                   struct rlimit *rlim_stack)
{
    unsigned long gap = rlim_stack->rlim_cur;
    unsigned long pad = stack_maxrandom_size(task_size) + stack_guard_gap;
    unsigned long gap_min, gap_max;

    /* Values close to RLIM_INFINITY can overflow. */
    if (gap + pad > gap)
        gap += pad;

    /*
     * Top of mmap area (just below the process stack).
     * Leave an at least ~128 MB hole with possible stack randomization.
     */
    gap_min = SIZE_128M;
    gap_max = (task_size / 6) * 5;

    if (gap < gap_min)
        gap = gap_min;
    else if (gap > gap_max)
        gap = gap_max;

    return PAGE_ALIGN(task_size - gap - rnd);
}

입력 rnd는 이미 page 단위 randomization을 반영한 byte offset입니다. 여기서는 stack limit과 pad의 덧셈이 wrap하지 않을 때만 더하고 gap을 clamp한 뒤 이를 사용합니다. (task_size / 6) * 5라는 계산 순서도 그대로 보존했습니다. 이 함수만으로 실제 random bit 설정을 읽을 수는 없으며, 같은 source 파일의 arch_rnd()와 ABI 선택도 확인해야 합니다.

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 5 §5.0.5. 현재 layout·randomization 계산은 Linux v6.18 arch/x86/mm/mmap.c, 공통 주소 선택은 mm/mmap.c를 참고하실 수 있습니다.