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_MISSING
UFFDIO_COPY
혹은 UFFDIO_ZEROPAGE
ioctl()
이 호출되기 전까지 스레드는 stop된다.UFFDIO_REGISTER_MODE_MINOR
UFFDIO_CONTINUE
ioctl()
이 호출되기 전까지 스레드는 stop된다.UFFDIO_REGISTER_MODE_WP
UFFDIO_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는 헤더 데이터가 따로 존재하지 않아 첫 바이트부터 원하는 데이터로 채워넣을 수 있다.