7 page를 요청했는데 GUP가 4를 반환했다면 실패한 요청 전체가 되돌려진 것일까요. 그렇지 않습니다. 반환한 네 page는 실제로 reference나 pin을 획득한 결과이며 caller가 정리해야 합니다. 뒤의 세 entry를 읽거나 일곱 개를 모두 unpin하면 잘못된 object를 만질 수 있습니다.
VMA를 찾고 권한을 검사합니다
slow GUP core는 범위를 순회하며 현재 주소를 포함한 VMA를 확인합니다. 없는 range나 금지된 접근은 error입니다. stack growth가 가능한 경로는 별도 helper와 lock 계약을 거칩니다. VM_IO·VM_PFNMAP, anonymous-only 요청과 backing 종류, read/write 권한과 MAY 권한 등의 조건을 검사합니다.
FORCE가 있어도 허용되는 private CoW 접근과 shared file 접근을 같은 방식으로 우회하지 않습니다. DAX·secret memory 같은 특수 backing도 별도 제약을 가집니다. page-table bit가 write 가능해 보인다는 사실만으로 VMA 검사를 생략할 수 없습니다.
follow 결과가 다음 동작을 정합니다
page table을 따라 ordinary page를 얻으면 pages array에 넣습니다. 아직 fault되지 않았으면 faultin_page()로 backing을 준비하고 다시 follow합니다. unshare가 필요하다는 결과도 fault 계열로 이어집니다. special entry가 유효해도 반환할 struct page가 없다면 page array를 요구한 요청은 같은 방식으로 성공할 수 없습니다.
fault가 mmap lock을 놓고 RETRY 또는 COMPLETED 계약으로 돌아오면 core는 이를 바깥 retry loop에 전달합니다. locked state, 아직 남은 page 수와 이미 진행한 count를 함께 갱신합니다. fatal signal이 pending이면 계속 진행하지 않으며 긴 순회에는 scheduler 양보도 필요합니다.
7 page를 요청하고 4를 반환받은 경우라면 유효한 output index는 0부터 3까지입니다. 나머지 4부터 6까지는 조회하거나 unpin하지 않습니다. 다음 요청을 시도한다면 VA, output 위치, remaining count를 모두 4 page만큼 진행한 상태로 맞춥니다.
retry는 offset과 output 위치를 함께 옮깁니다
4개를 처리했다면 다음 start는 네 base page 뒤이며 output pointer도 네 칸 뒤로 갑니다. 남은 nr_pages를 줄이지 않거나 output만 옮기면 같은 주소를 중복 pin할 수 있습니다. lock을 다시 얻은 뒤에는 현재 VMA를 다시 확인해야 합니다.
한 번의 내부 fault retry가 끝나도 바깥 범위 loop나 동시 mapping 변경으로 추가 시도가 생길 수 있습니다. TRIED라는 이름을 GUP 호출 전체의 고정 반복 횟수로 읽으면 안 됩니다. partial success는 동시성과 error를 함께 표현하는 API의 일부입니다.
positive return과 error의 우선순위
core는 이미 처리한 page가 있으면 보통 그 개수를 우선하여 caller에게 소유권을 전달합니다. 아직 하나도 처리하지 못했을 때 error를 돌려줄 수 있습니다. 이를 “양수니까 요청 전체가 성공했다”로 처리하면 뒤의 buffer를 잘못 사용하게 됩니다.
caller는 정확히 반환 개수만 작업에 사용하고, 실패로 끝내기로 결정했어도 그 개수의 reference를 반환해야 합니다. device write가 있었다면 dirty-accounting contract도 맞춰야 합니다. DMA가 아직 진행 중인데 error 경로라는 이유로 먼저 unpin해서는 안 됩니다. partial GUP의 cleanup은 count, data 사용 종료, dirty 처리, put/unpin을 모두 포함합니다.
Lock이 다시 잡혀도 이전 critical section은 되살아나지 않습니다
아래 v6.0 전체 함수는 책에서 non-retry와 retry로 나뉜 부분을 연결합니다. locked pointer를 제공하면 초기값은 1이어야 하고 VMA output은 함께 요청할 수 없습니다. 진행한 count를 먼저 축적한 뒤 현재 주소에서 한 page를 FOLL_TRIED로 다시 시도합니다. Fatal signal과 killable lock 획득 실패도 정상 종료 경로로 다룹니다.
이 version의 core 주석은 lock 해제를 허용하는 호출에서 mmap read lock을 보유하도록 요구합니다. 하위 fault가 up_read()로 반환하기 때문입니다. writer를 잡아 놓고 locked = 1만 전달한다고 호환되는 것이 아닙니다. lock을 넘기지 않는 호출은 read 또는 write 보호 아래 실행할 수 있지만, 그 경우에는 retry-aware read-unlock protocol을 사용하지 않는다는 차이가 있습니다.
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:1387-1513
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static __always_inline long __get_user_pages_locked(struct mm_struct *mm,
unsigned long start,
unsigned long nr_pages,
struct page **pages,
struct vm_area_struct **vmas,
int *locked,
unsigned int flags)
{
long ret, pages_done;
bool lock_dropped;
if (locked) {
/* if VM_FAULT_RETRY can be returned, vmas become invalid */
BUG_ON(vmas);
/* check caller initialized locked */
BUG_ON(*locked != 1);
}
if (flags & FOLL_PIN)
mm_set_has_pinned_flag(&mm->flags);
/*
* FOLL_PIN and FOLL_GET are mutually exclusive. Traditional behavior
* is to set FOLL_GET if the caller wants pages[] filled in (but has
* carelessly failed to specify FOLL_GET), so keep doing that, but only
* for FOLL_GET, not for the newer FOLL_PIN.
*
* FOLL_PIN always expects pages to be non-null, but no need to assert
* that here, as any failures will be obvious enough.
*/
if (pages && !(flags & FOLL_PIN))
flags |= FOLL_GET;
pages_done = 0;
lock_dropped = false;
for (;;) {
ret = __get_user_pages(mm, start, nr_pages, flags, pages,
vmas, locked);
if (!locked)
/* VM_FAULT_RETRY couldn't trigger, bypass */
return ret;
/* VM_FAULT_RETRY or VM_FAULT_COMPLETED cannot return errors */
if (!*locked) {
BUG_ON(ret < 0);
BUG_ON(ret >= nr_pages);
}
if (ret > 0) {
nr_pages -= ret;
pages_done += ret;
if (!nr_pages)
break;
}
if (*locked) {
/*
* VM_FAULT_RETRY didn't trigger or it was a
* FOLL_NOWAIT.
*/
if (!pages_done)
pages_done = ret;
break;
}
/*
* VM_FAULT_RETRY triggered, so seek to the faulting offset.
* For the prefault case (!pages) we only update counts.
*/
if (likely(pages))
pages += ret;
start += ret << PAGE_SHIFT;
lock_dropped = true;
retry:
/*
* Repeat on the address that fired VM_FAULT_RETRY
* with both FAULT_FLAG_ALLOW_RETRY and
* FAULT_FLAG_TRIED. Note that GUP can be interrupted
* by fatal signals, so we need to check it before we
* start trying again otherwise it can loop forever.
*/
if (fatal_signal_pending(current)) {
if (!pages_done)
pages_done = -EINTR;
break;
}
ret = mmap_read_lock_killable(mm);
if (ret) {
BUG_ON(ret > 0);
if (!pages_done)
pages_done = ret;
break;
}
*locked = 1;
ret = __get_user_pages(mm, start, 1, flags | FOLL_TRIED,
pages, NULL, locked);
if (!*locked) {
/* Continue to retry until we succeeded */
BUG_ON(ret != 0);
goto retry;
}
if (ret != 1) {
BUG_ON(ret > 1);
if (!pages_done)
pages_done = ret;
break;
}
nr_pages--;
pages_done++;
if (!nr_pages)
break;
if (likely(pages))
pages++;
start += PAGE_SIZE;
}
if (lock_dropped && *locked) {
/*
* We must let the caller know we temporarily dropped the lock
* and so the critical section protected by it was lost.
*/
mmap_read_unlock(mm);
*locked = 0;
}
return pages_done;
}
핵심은 lock_dropped입니다. 중간에 한 번이라도 mmap lock을 놓았다면 마지막에 다시 보유하고 있어도 이를 내려놓고 *locked = 0으로 caller에게 알립니다. Caller가 처음부터 끝까지 VMA를 보호했다고 착각하지 않도록 하는 protocol입니다. 이 state를 무시한 무조건 unlock은 double unlock이 되고, 다시 잡았다는 사실만 보고 옛 VMA pointer를 쓰면 stale 접근이 됩니다.
Core의 반환 우선순위와 VMA 검사를 연결합니다
전체 core에서는 gate·huge·devmap cleanup과 output의 page 단위 증가를 생략하지 않았습니다. Base-page 수를 세는 API이므로 하나의 large folio를 만났다고 항상 count가 1만 증가하는 것은 아닙니다. page_increm은 mask와 남은 길이의 제한도 함께 적용받습니다.
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:1140-1273
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static long __get_user_pages(struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *locked)
{
long ret = 0, i = 0;
struct vm_area_struct *vma = NULL;
struct follow_page_context ctx = { NULL };
if (!nr_pages)
return 0;
start = untagged_addr(start);
VM_BUG_ON(!!pages != !!(gup_flags & (FOLL_GET | FOLL_PIN)));
/*
* If FOLL_FORCE is set then do not force a full fault as the hinting
* fault information is unrelated to the reference behaviour of a task
* using the address space
*/
if (!(gup_flags & FOLL_FORCE))
gup_flags |= FOLL_NUMA;
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start);
if (!vma && in_gate_area(mm, start)) {
ret = get_gate_page(mm, start & PAGE_MASK,
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
goto out;
ctx.page_mask = 0;
goto next_page;
}
if (!vma) {
ret = -EFAULT;
goto out;
}
ret = check_vma_flags(vma, gup_flags);
if (ret)
goto out;
if (is_vm_hugetlb_page(vma)) {
i = follow_hugetlb_page(mm, vma, pages, vmas,
&start, &nr_pages, i,
gup_flags, locked);
if (locked && *locked == 0) {
/*
* We've got a VM_FAULT_RETRY
* and we've lost mmap_lock.
* We must stop here.
*/
BUG_ON(gup_flags & FOLL_NOWAIT);
goto out;
}
continue;
}
}
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (fatal_signal_pending(current)) {
ret = -EINTR;
goto out;
}
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &ctx);
if (!page || PTR_ERR(page) == -EMLINK) {
ret = faultin_page(vma, start, &foll_flags,
PTR_ERR(page) == -EMLINK, locked);
switch (ret) {
case 0:
goto retry;
case -EBUSY:
case -EAGAIN:
ret = 0;
fallthrough;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
goto out;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
/*
* Proper page table entry exists, but no corresponding
* struct page. If the caller expects **pages to be
* filled in, bail out now, because that can't be done
* for this page.
*/
if (pages) {
ret = PTR_ERR(page);
goto out;
}
goto next_page;
} else if (IS_ERR(page)) {
ret = PTR_ERR(page);
goto out;
}
if (pages) {
pages[i] = page;
flush_anon_page(vma, page, start);
flush_dcache_page(page);
ctx.page_mask = 0;
}
next_page:
if (vmas) {
vmas[i] = vma;
ctx.page_mask = 0;
}
page_increm = 1 + (~(start >> PAGE_SHIFT) & ctx.page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
i += page_increm;
start += page_increm * PAGE_SIZE;
nr_pages -= page_increm;
} while (nr_pages);
out:
if (ctx.pgmap)
put_dev_pagemap(ctx.pgmap);
return i ? i : ret;
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:1027-1078
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
{
vm_flags_t vm_flags = vma->vm_flags;
int write = (gup_flags & FOLL_WRITE);
int foreign = (gup_flags & FOLL_REMOTE);
if (vm_flags & (VM_IO | VM_PFNMAP))
return -EFAULT;
if (gup_flags & FOLL_ANON && !vma_is_anonymous(vma))
return -EFAULT;
if ((gup_flags & FOLL_LONGTERM) && vma_is_fsdax(vma))
return -EOPNOTSUPP;
if (vma_is_secretmem(vma))
return -EFAULT;
if (write) {
if (!(vm_flags & VM_WRITE)) {
if (!(gup_flags & FOLL_FORCE))
return -EFAULT;
/*
* We used to let the write,force case do COW in a
* VM_MAYWRITE VM_SHARED !VM_WRITE vma, so ptrace could
* set a breakpoint in a read-only mapping of an
* executable, without corrupting the file (yet only
* when that file had been opened for writing!).
* Anon pages in shared mappings are surprising: now
* just reject it.
*/
if (!is_cow_mapping(vm_flags))
return -EFAULT;
}
} else if (!(vm_flags & VM_READ)) {
if (!(gup_flags & FOLL_FORCE))
return -EFAULT;
/*
* Is there actually any vma we can reach here which does not
* have VM_MAYREAD set?
*/
if (!(vm_flags & VM_MAYREAD))
return -EFAULT;
}
/*
* gups are always data accesses, not instruction
* fetches, so execute=false here
*/
if (!arch_vma_access_permitted(vma, write, false, foreign))
return -EFAULT;
return 0;
}
FOLL_FORCE라도 read 접근에는 MAYREAD, write 접근에는 private CoW 성질과 architecture access 조건이 필요합니다. is_cow_mapping()을 “potentially writable shared”로 읽으면 반대가 됩니다. VM_IO·VM_PFNMAP은 일반 page 반환 경로에서 거절되며 PFNMAP을 단순히 모든 kernel memory의 mapping이라는 뜻으로 사용하지 않겠습니다. DAX·secret-memory 검사도 전체 정의에 보존했습니다.
arch_vma_access_permitted()에는 execute = false가 전달됩니다. GUP는 instruction fetch가 아니라 data 접근을 검사하므로 실행 permission이 있다는 사실만으로 read 요청이 허용되는 것은 아닙니다. 앞의 untagged_addr()도 architecture별 address tag 처리를 위한 준비이지 VMA 존재나 접근 permission을 증명하는 검사가 아닙니다.
Source: Linux v6.0 mm/gup.c. Historical source이고 book exact tag는 입증하지 않았습니다. COPYING, GPL-2.0와 원래 notice를 보존했습니다. v6.18은 별도 비교 tag이며 이 글에서 kernel build·runtime 검증은 하지 않았습니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 8 §8.1.5.2. 현재 slow core, VMA 검사와 반환 우선순위는 Linux v6.18 mm/gup.c, pin 해제 계약은 pin_user_pages 문서 원본에 있습니다.