[Linux Kernel] userfaultfd

dandb3·2024년 7월 1일
0

linux kernel

목록 보기
7/21

heap spray를 하기 위한 방법 중 userfaultfd + setxattr를 사용하는 방법이 있다.
이를 알아볼 것이다.

사전 지식

lazy allocation

리눅스 커널에서는 메모리를 할당할 때 실제로 해당 물리페이지가 사용되기 전까지 새로 할당하지 않는다.
이 이유는 할당해 놓고 실제로 사용하지 않는 경우의 비용을 없애기 위함임.

그렇기 때문에 할당만 하고 사용하지 않은 경우에는 virtual address 상에서는 페이지가 존재하지만, 실제 physical address는 할당이 되지 않은 상태에 해당한다.

이 상황에서 해당 페이지에 대한 access가 이루어지면, page fault가 발생하고, 커널에서 page fault handler가 동작하여 새로운 페이지를 할당해준다.
그러면 원래 진행되던 프로세스의 입장에서는 아무런 문제 없이 잘 동작이 되는 것으로 인식하고 실행되는 것이다.

1. userfaultfd

위의 lazy allocation에서 설명할 때에는 커널에서 page fault handler를 동작시킨다고 하였는데, 이를 user space에서 할 수 있게 하는 syscall에 해당한다.

#include <fcntl.h>             /* Definition of O_* constants */
#include <sys/syscall.h>       /* Definition of SYS_* constants */
#include <linux/userfaultfd.h> /* Definition of UFFD_* constants */
#include <unistd.h>

int syscall(SYS_userfaultfd, int flags);

userfaultfd()의 경우 wrapper 함수가 없기 때문에 syscall()을 통해 직접 호출한다.

return되는 값은 생성된 userfaultfd object에 대한 file descriptor값이다.
실패 시 -1리턴.

사용 방법

userfaultfd를 생성한 이후, UFFDIO_API ioctl()을 실행해야 한다.
그 후, UFFDIO_REGISTER ioctl()을 통해 memory address range를 register해야 한다.

register 시 가능한 3가지 모드가 존재한다.
이 모드는 중첩가능하다.

  • UFFDIO_REGISTER_MODE_MISSING
    missing page가 access되었을 때 user-space에 event가 전달된다.
    user-space에서 UFFDIO_COPY 혹은 UFFDIO_ZEROPAGE ioctl() 이 호출되기 전까지 스레드는 stop된다.
  • UFFDIO_REGISTER_MODE_MINOR
    minor page fault가 일어난 경우 (즉, backing page는 page cache에 존재하면서 page table entry는 존재하지 않는 경우) 에 user-space에 event가 전달된다.
    user-space에서 UFFDIO_CONTINUE ioctl()이 호출되기 전까지 스레드는 stop된다.
  • UFFDIO_REGISTER_MODE_WP
    write-protected page가 쓰여진 경우 user-space에 event가 전달된다.
    user-space에서 UFFDIO_WRITEPROTECT ioctl()이 호출되기 전까지 스레드는 stop된다.

그러면 해당 메모리 영역에 page fault 발생 시 userfaultfd의 fd값으로 event가 발생하게 되고, 이를 처리하는 스레드에서는 UFFDIO_COPY, UFFDIO_ZEROPAGE, UFFDIO_CONTINUE ioctl()을 통해서 처리하게 된다.

2. setxattr

필요한 함수만 살펴보자.

static long
setxattr(struct mnt_idmap *idmap, struct dentry *d,
	const char __user *name, const void __user *value, size_t size,
	int flags)
{
	struct xattr_name kname;
	struct xattr_ctx ctx = {
		.cvalue   = value,
		.kvalue   = NULL,
		.size     = size,
		.kname    = &kname,
		.flags    = flags,
	};
	int error;

	error = setxattr_copy(name, &ctx);
	if (error)
		return error;

	error = do_setxattr(idmap, d, &ctx);

	kvfree(ctx.kvalue);
	return error;
}

int setxattr_copy(const char __user *name, struct xattr_ctx *ctx)
{
	int error;

	if (ctx->flags & ~(XATTR_CREATE|XATTR_REPLACE))
		return -EINVAL;

	error = strncpy_from_user(ctx->kname->name, name,
				sizeof(ctx->kname->name));
	if (error == 0 || error == sizeof(ctx->kname->name))
		return  -ERANGE;
	if (error < 0)
		return error;

	error = 0;
	if (ctx->size) {
		if (ctx->size > XATTR_SIZE_MAX)
			return -E2BIG;

		ctx->kvalue = vmemdup_user(ctx->cvalue, ctx->size);
		if (IS_ERR(ctx->kvalue)) {
			error = PTR_ERR(ctx->kvalue);
			ctx->kvalue = NULL;
		}
	}

	return error;
}

void *vmemdup_user(const void __user *src, size_t len)
{
	void *p;

	p = kvmalloc(len, GFP_USER);
	if (!p)
		return ERR_PTR(-ENOMEM);

	if (copy_from_user(p, src, len)) {
		kvfree(p);
		return ERR_PTR(-EFAULT);
	}

	return p;
}

위 코드를 보면, setxattr() -> setxattr_copy() -> vmemdup_user() 순으로 호출되는 것을 알 수 있다.

결국 setxattr()는 원하는 크기의 메모리를 kvmalloc() 을 통해서 할당하고, 원하는 내용으로 채워넣을 수 있는 함수이다.
-> heap spray에 쓸 수 있을 것 같다.

하지만 setxattr() 함수의 마지막 부분을 보면, kvfree() 함수를 통해서 할당했던 메모리를 해제하는 것을 확인할 수 있다.
원할 때까지 할당 상태를 유지할 수 없다는 점에서 heap spray를 하기에 부족해 보인다.

3. 해결 방법?

이는 앞서 언급했던 userfaultfd() 를 통해서 해결할 수 있다.
userfaultfd()UFFDIO_REGISTER_MODE_MISSING에서 page fault가 handle되기 전까지 스레드가 stop된다는 것에 주목할 필요가 있다.

아래와 같은 상황을 고려해 보자.

  • 두 페이지가 연속으로 메모리에 할당되어 있는 상황.
  • 첫 번째 페이지는 실제 물리 메모리에 매핑이 되어 있지만, 두 번째 페이지는 물리 메모리에 매핑이 되어 있지 않은 상태이다.
  • 이 때, 두 번째 페이지를 userfaultfd()를 통해서 page fault handle을 하게끔 만든 상황.
  • 두 페이지에 걸친 어떤 data가 존재한다. 예를 들어, 1페이지 마지막 8bytes, 2페이지 처음 8bytes에 걸친 어떠한 struct를 생각해보자.
  • 이 struct를 setxattr()의 호출 인자로 집어넣는다.

이런 상황이 발생한 경우, 함수가 vmemdup_user()까지 도달할 것이고, kvmalloc()을 통해 할당한 후 copy_from_user()가 호출될 것이다.
이 때, 첫 8바이트만큼은 잘 복사가 될 것이다. (앞서서 첫 번째 페이지는 이미 물리 메모리에 매핑이 된 상태라고 했으므로)
그런데 뒷 8바이트를 복사를 하려고 하면, 이 때 page fault가 발생하고, userfaultfd()를 통해 처리되기 전까지 스레드는 stop된다.

여기서 중요한 점은, 만약 user-space에서 handler를 실행하지 않는다면, page fault를 일으킨 스레드는 계속 stop 상태로 존재한다는 점이다.

원래 함수 흐름대로라면 copy_from_user()가 종료되고, 이후 kvfree()를 호출하는데, 중간에 멈춰서 결국 kvfree()가 호출되지 않고 할당된 메모리는 그대로 남아있게 된다..!

만약 할당 해제를 하고 싶을 때는 명시적으로 UFFDIO_COPY ioctl()을 해당 메모리 영역에 대해서 호출하면 된다.

4. 주의해야 할 점

  • 하나의 스레드가 setxattr() 함수를 호출하면 page fault로 인해 stop 되기 때문에 하나의 kvmalloc() 당 하나의 스레드가 필요하다.
  • 모든 setxattr() 함수를 하나의 페이지에 대한 page fault를 발생시키는데 쓰였다면, 할당 해제를 위해 UFFDIO_COPY ioctl() 을 호출하게 되면 해당 페이지를 참조하고 있던 모든 스레드의 page fault가 해결되면서 할당 해제 순서를 제어할 수 없게 된다. -> 그냥 스레드 당 2페이지씩 할당하면 된다.

5. 필요성

그래서 이 방법이 다른 heap spray보다 더 좋은 이유가 무엇일까?

msg_msg와 비교하면 제일 편할 것 같다.
우선 msg_msg의 경우에도 원하는 데이터만큼을 할당해서 spray가 가능하지만, struct msg_msg, struct msg_msgseg 만큼의 데이터를 할당한 object의 제일 앞부분에 포함해야 한다는 제약이 있다.

그에 반해 userfaultfd + setxattr는 헤더 데이터가 따로 존재하지 않아 첫 바이트부터 원하는 데이터로 채워넣을 수 있다.

reference

profile
공부 내용 저장소

0개의 댓글

관련 채용 정보