heap spray를 하기 위한 방법 중 userfaultfd + setxattr를 사용하는 방법이 있다.
이를 알아볼 것이다.
리눅스 커널에서는 메모리를 할당할 때 실제로 해당 물리페이지가 사용되기 전까지 새로 할당하지 않는다.
이 이유는 할당해 놓고 실제로 사용하지 않는 경우의 비용을 없애기 위함임.
그렇기 때문에 할당만 하고 사용하지 않은 경우에는 virtual address 상에서는 페이지가 존재하지만, 실제 physical address는 할당이 되지 않은 상태에 해당한다.
이 상황에서 해당 페이지에 대한 access가 이루어지면, page fault가 발생하고, 커널에서 page fault handler가 동작하여 새로운 페이지를 할당해준다.
그러면 원래 진행되던 프로세스의 입장에서는 아무런 문제 없이 잘 동작이 되는 것으로 인식하고 실행되는 것이다.
위의 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_MISSINGUFFDIO_COPY 혹은 UFFDIO_ZEROPAGE ioctl() 이 호출되기 전까지 스레드는 stop된다.UFFDIO_REGISTER_MODE_MINORUFFDIO_CONTINUE ioctl()이 호출되기 전까지 스레드는 stop된다.UFFDIO_REGISTER_MODE_WPUFFDIO_WRITEPROTECT ioctl()이 호출되기 전까지 스레드는 stop된다.그러면 해당 메모리 영역에 page fault 발생 시 userfaultfd의 fd값으로 event가 발생하게 되고, 이를 처리하는 스레드에서는 UFFDIO_COPY, UFFDIO_ZEROPAGE, UFFDIO_CONTINUE ioctl()을 통해서 처리하게 된다.
필요한 함수만 살펴보자.
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를 하기에 부족해 보인다.
이는 앞서 언급했던 userfaultfd() 를 통해서 해결할 수 있다.
userfaultfd()의 UFFDIO_REGISTER_MODE_MISSING에서 page fault가 handle되기 전까지 스레드가 stop된다는 것에 주목할 필요가 있다.
아래와 같은 상황을 고려해 보자.
userfaultfd()를 통해서 page fault handle을 하게끔 만든 상황.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()을 해당 메모리 영역에 대해서 호출하면 된다.
setxattr() 함수를 호출하면 page fault로 인해 stop 되기 때문에 하나의 kvmalloc() 당 하나의 스레드가 필요하다.setxattr() 함수를 하나의 페이지에 대한 page fault를 발생시키는데 쓰였다면, 할당 해제를 위해 UFFDIO_COPY ioctl() 을 호출하게 되면 해당 페이지를 참조하고 있던 모든 스레드의 page fault가 해결되면서 할당 해제 순서를 제어할 수 없게 된다. -> 그냥 스레드 당 2페이지씩 할당하면 된다.그래서 이 방법이 다른 heap spray보다 더 좋은 이유가 무엇일까?
msg_msg와 비교하면 제일 편할 것 같다.
우선 msg_msg의 경우에도 원하는 데이터만큼을 할당해서 spray가 가능하지만, struct msg_msg, struct msg_msgseg 만큼의 데이터를 할당한 object의 제일 앞부분에 포함해야 한다는 제약이 있다.
그에 반해 userfaultfd + setxattr는 헤더 데이터가 따로 존재하지 않아 첫 바이트부터 원하는 데이터로 채워넣을 수 있다.