iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🔒

Experimenting with the mlock System Call to Prevent Memory Swap-Out

に公開

What is the mlock system call?

The mlock system call prevents memory data from being swapped out. As far as I know, its use cases include:

  • Performance improvement.
  • Security.
    • When plaintext sensitive data is temporarily stored in memory, this prevents the plaintext data from remaining on the disk if it gets swapped out.
  • IOURING_REGISTER_BUFFERS.
    • This appears to be a feature for registering fixed buffers in io_uring, though I have not used it and am not very familiar with it. (It sounds interesting, so I would like to try it separately.)
    • From a quick look, it seems to relate to "asynchronous I/O operations for storage devices."

The interface is simple, requiring only the memory address to be locked and its size.

#include <sys/mman.h>
int mlock(const void *addr, size_t len);
int munlock(const void *addr, size_t len);

Background for my investigation

I am developing a password manager, and I was looking for ways to prevent plaintext data expanded in memory from being swapped out and saved as sensitive plaintext data on the disk.

Limitations of mlock

Paging is essentially a technique for efficiently utilizing limited memory; if you indiscriminately mlock everything, the OS will be unable to use memory, leading to instability.

Therefore, there is an upper limit on the size that can be mlock'd. In my environment (Ubuntu 24.04 Desktop, 64GB RAM), it was about 8GB.

$ ulimit -l
8179008

Additionally, the process calling mlock must have the CAP_IPC_LOCK capability to use it.

Trying out the mlock system call

The environment is Ubuntu 24.04 desktop LTS.

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("please specify malloc size\n");
        return 0;
    }

    char buf[255];
    const size_t mem_size = atoi(argv[1]);

    printf("memory size: %ld byte\n", mem_size);

    void *mlock_mem = malloc(mem_size);
    int mlock_ret = mlock(mlock_mem, mem_size);
    printf("mlock return: %d\n", mlock_ret);
    if (mlock_ret != 0) {
        perror("mlock");
        return 1;
    }

    printf("wait until input...\n");
    fgets(buf, sizeof(buf), stdin);

    int munlock_ret = munlock(mlock_mem, mem_size);
    printf("munlock return: %d\n", munlock_ret);

    free(mlock_mem);

    return 0;
}

(It has been so long since I wrote C/C++ that I was a bit confused.)

Compile it.

cc mlock.c

Try running it.

./a.out `expr 10 \* 1024`

Verify that it has been locked correctly (<PID> is the process ID of a.out).

$ grep VmLck /proc/<PID>/status
VmLck:	      12 kB

In the execution example above, I specified 10KB, but VmLck shows 12KB. This is likely because pages are allocated in 4KB increments, so it was rounded up to 12KB.

Next, let's set a lower limit using ulimit -l.

$ ulimit -l 64
$ ./a.out `expr 100 \* 1024`
memory size: 102400 byte
mlock return: -1
mlock: Cannot allocate memory

As expected, it resulted in an error.

Conclusion

In this article, I tried out the mlock system call.
As for whether I will use it in my password manager, I think I will not.
The reason is that it has many limitations, making it difficult to use in a standard desktop application.
Furthermore, swap files themselves generally cannot be read without root privileges. However, if an attacker has already obtained root privileges, they could perform a process memory dump anyway...
Therefore, the basic policy will likely be to not hold sensitive data in plaintext in the process unless absolutely necessary, and to zeroize plaintext data in memory after use.

Discussion