stack 바로 아래의 주소를 접근했는데 segmentation fault 대신 실행이 계속될 수 있습니다. 위의 VMA가 성장 가능한 stack이고 확장이 허용되면 kernel이 VMA 시작을 낮추기 때문입니다. 그러나 모든 hole이 stack이며 모든 thread stack이 자동 성장하는 것은 아닙니다.
가까운 VMA를 찾는 것과 포함 여부 확인
아래 v6.0 구현은 새 시작을 PAGE_MASK로 내린 뒤 mmap_min_addr보다 낮으면 EPERM으로 거부합니다. guard 검사는 직전 VMA가 존재하고, 그 VMA가 별도의 GROWSDOWN segment가 아니며, READ·WRITE·EXEC 중 접근 가능한 flag가 있을 때 적용합니다. 따라서 “언제나 모든 이웃과 고정 1 MiB를 비운다”는 규칙은 아닙니다. stack_guard_gap은 boot command-line에서 정할 수 있는 당시 정책값이며, 여기서는 변경하지 않습니다.
주소 lookup은 fault 주소보다 뒤에서 끝나는 VMA를 찾을 수 있습니다. fault가 그 VMA 시작보다 아래이면 우선 hole입니다. VM_GROWSDOWN과 적절한 성장 조건이 있을 때에만 expand_stack()·expand_downwards() 계열로 확장을 검토합니다. runtime이 미리 고정 크기로 만든 thread stack이나 단순 MAP_STACK 요청을 모두 같은 경우로 보아서는 안 됩니다.
새 시작 주소는 page 단위로 맞추고, 이웃 mapping과 guard gap을 비교합니다. 기본 256 base-page gap은 4 KiB page에서 1 MiB이지만 architecture·설정과 인접 VMA 속성의 조건을 함께 봐야 합니다. 주소 사이에 물리적으로 아무 page도 없다는 뜻의 guard allocation은 아닙니다.
limit과 commit은 순서를 가집니다
growth 양이 vm_pgoff의 논리 시작을 underflow시키지 않는지 확인합니다. acct_stack_growth()는 전체 virtual space 한계, 확장 후 stack의 RLIMIT_STACK, locked-memory 증가와 hugetlb-only 영역 등의 조건을 검사합니다. 마지막에 commit과 security accounting을 청구하면 앞선 실패 때문에 청구를 복구하는 경우를 줄일 수 있습니다.
RLIMIT_STACK이 unlimited여도 다른 limit과 guard, commit 제약은 남습니다. RLIMIT_DATA는 data-mapping 판정에 따른 검사이지 독립적인 stack byte 상한으로 단순화할 수 없습니다. 확장 허용은 physical allocation 성공의 보장도 아닙니다.
rmap index도 함께 이동합니다
고전 구현에서 anon_vma write lock은 같은 anonymous 계보의 동시 확장을 직렬화하지만, 한 mm의 모든 growable VMA가 같은 root anon_vma를 공유하지는 않습니다. 그래서 서로 다른 VMA의 RB-tree gap 갱신까지 보호하려고 mm->page_table_lock을 별도로 사용합니다. 이 lock 아래 locked_vm, total_vm·stack_vm accounting, anon interval tree의 범위, vm_start·vm_pgoff와 gap 요약을 갱신합니다. lock 이름이 page_table_lock이라고 해서 이 구간이 data PTE를 설치하는 단계라는 뜻은 아닙니다.
grow <= vm_pgoff는 새 virtual address가 음수가 되는지 검사한다기보다 unsigned logical page offset을 뺄 때 underflow하지 않게 하는 직접 조건입니다. boundary와 rmap index를 함께 이동시켜야 이전부터 mapped된 page의 linear_page_index() 관계가 유지됩니다. 마지막의 perf event·khugepaged 등록·debug validation도 전체 정의에는 남아 있습니다.
mlock은 reclaim 방지의 정책이며 GUP의 physical pin과 같은 계약이 아닙니다. VM_LOCKED accounting 증가를 “migration할 수 없는 physical page를 즉시 모두 pin한다”로 해석해서는 안 됩니다. VMA growth 승인과 실제 population·backing 공급의 실패도 따로 추적해야 합니다.
성장하는 VMA의 anonymous 관계를 준비하고 concurrent expansion을 직렬화한 뒤 경계가 이미 바뀌지 않았는지 다시 확인합니다. VA 시작이 낮아지면 새로 포함한 page 수만큼 vm_pgoff도 조정해야 기존 folio의 논리 index가 유지됩니다. anon_vma interval node를 빼고 새 범위로 다시 넣는 이유입니다.
RB-tree 기반 구현에서는 gap 요약과 이를 보호하는 lock도 관련됩니다. 현재 VMA index와 locking은 이 구현과 다르므로 예전의 mmap read-lock 아래 경계 변경을 모든 tag에 그대로 적용할 수 없습니다. v6.18 mm/vma.c의 expand_downwards()는 mmap write lock을 요구하고 vma_start_write()로 concurrent page fault를 배제합니다. Maple Tree iterator preallocation과 entry 갱신도 필요하므로 예전 gap update 코드의 lock 이름만 바꿔서 설명할 수 없습니다.
확장 뒤에도 fault가 남습니다
VMA를 넓힌 뒤에는 새 주소의 PTE와 backing을 준비하는 일반 fault 처리가 필요합니다. locked VMA이면 population과 locked accounting도 관계합니다. 이 단계의 실패를 stack 경계 확장 실패와 같은 사건으로 합치면 진단이 어려워집니다.
반대로 stack 확장이 허용되지 않으면 bad-area 경로에서 signal을 전달할 수 있습니다. signal handler 자체가 stack을 필요로 한다는 점 때문에 application의 signal 처리 설계도 별도 문제입니다. 여기서는 limit을 바꾸거나 guard를 침범하는 실험을 하지 않고, 정상 성장 판정과 translation 준비의 두 단계를 구별합니다.
Linux v6.0의 stack 확장 전체 정의
RB-tree gap과 mmap read-lock을 사용하던 구현을 전체로 읽습니다. 아래 정의는 v6.18의 Maple Tree 또는 mmap write-lock 경로를 대체하는 코드가 아닙니다.
아래는 Linux v6.0의 전체 함수·자료형 정의입니다. kernel의 header·configuration·호출 규약에 의존하는 코드이며 독립 실행 프로그램은 아닙니다. 본문 해설과는 별도로 각 source의 SPDX license를 적용합니다. 원래 copyright notice는 source notice에, GPL version 2 전문은 license 파일에 보존합니다. 표시상의 tab을 space로 바꾼 것 외에 함수 내부의 문장이나 configuration 분기는 제거하지 않았습니다.
expand_downwards()
Linux v6.0 mm/mmap.c, SPDX GPL-2.0-only입니다.
int expand_downwards(struct vm_area_struct *vma,
unsigned long address)
{
struct mm_struct *mm = vma->vm_mm;
struct vm_area_struct *prev;
int error = 0;
address &= PAGE_MASK;
if (address < mmap_min_addr)
return -EPERM;
/* Enforce stack_guard_gap */
prev = vma->vm_prev;
/* Check that both stack segments have the same anon_vma? */
if (prev && !(prev->vm_flags & VM_GROWSDOWN) &&
vma_is_accessible(prev)) {
if (address - prev->vm_end < stack_guard_gap)
return -ENOMEM;
}
/* We must make sure the anon_vma is allocated. */
if (unlikely(anon_vma_prepare(vma)))
return -ENOMEM;
/*
* vma->vm_start/vm_end cannot change under us because the caller
* is required to hold the mmap_lock in read mode. We need the
* anon_vma lock to serialize against concurrent expand_stacks.
*/
anon_vma_lock_write(vma->anon_vma);
/* Somebody else might have raced and expanded it already */
if (address < vma->vm_start) {
unsigned long size, grow;
size = vma->vm_end - address;
grow = (vma->vm_start - address) >> PAGE_SHIFT;
error = -ENOMEM;
if (grow <= vma->vm_pgoff) {
error = acct_stack_growth(vma, size, grow);
if (!error) {
/*
* vma_gap_update() doesn't support concurrent
* updates, but we only hold a shared mmap_lock
* lock here, so we need to protect against
* concurrent vma expansions.
* anon_vma_lock_write() doesn't help here, as
* we don't guarantee that all growable vmas
* in a mm share the same root anon vma.
* So, we reuse mm->page_table_lock to guard
* against concurrent vma expansions.
*/
spin_lock(&mm->page_table_lock);
if (vma->vm_flags & VM_LOCKED)
mm->locked_vm += grow;
vm_stat_account(mm, vma->vm_flags, grow);
anon_vma_interval_tree_pre_update_vma(vma);
vma->vm_start = address;
vma->vm_pgoff -= grow;
anon_vma_interval_tree_post_update_vma(vma);
vma_gap_update(vma);
spin_unlock(&mm->page_table_lock);
perf_event_mmap(vma);
}
}
}
anon_vma_unlock_write(vma->anon_vma);
khugepaged_enter_vma(vma, vma->vm_flags);
validate_mm(mm);
return error;
}
acct_stack_growth()
Linux v6.0 mm/mmap.c, SPDX GPL-2.0-only입니다.
static int acct_stack_growth(struct vm_area_struct *vma,
unsigned long size, unsigned long grow)
{
struct mm_struct *mm = vma->vm_mm;
unsigned long new_start;
/* address space limit tests */
if (!may_expand_vm(mm, vma->vm_flags, grow))
return -ENOMEM;
/* Stack limit test */
if (size > rlimit(RLIMIT_STACK))
return -ENOMEM;
/* mlock limit tests */
if (mlock_future_check(mm, vma->vm_flags, grow << PAGE_SHIFT))
return -ENOMEM;
/* Check to ensure the stack will not grow into a hugetlb-only region */
new_start = (vma->vm_flags & VM_GROWSUP) ? vma->vm_start :
vma->vm_end - size;
if (is_hugepage_only_range(vma->vm_mm, new_start, size))
return -EFAULT;
/*
* Overcommit.. This must be the final test, as it will
* update security statistics.
*/
if (security_vm_enough_memory_mm(mm, grow))
return -ENOMEM;
return 0;
}
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 6 §6.10. 현재 확장은 Linux v6.18 mm/vma.c, limit 의미는 getrlimit(2), lock 규칙은 Process Addresses에 있습니다.