Question

Intercept open syscall in C, when called via fopen

I'm trying to code up a (limited) in-memory file system redirects via libc/syscall interception and returning memfd_create-produced file descriptors for the files that are virtualized. As a preparation step, I have a following test.c file:

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <dlfcn.h>
#include <sys/stat.h>

/*
FILE* fopen(const char *path, const char *mode)
{
    typedef FILE* (*orig_fopen_func_type)(const char *path, const char *mode);
    fprintf(stderr, "log_file_access_preload: fopen(\"%s\", \"%s\")\n", path, mode);
    orig_fopen_func_type orig_func = (orig_fopen_func_type)dlsym(RTLD_NEXT, "fopen");
    return orig_func(path, mode);
}
*/

int open(const char *path, int flags)
{
    typedef int (*orig_func_type)(const char *pathname, int flags);
    fprintf(stderr, "log_file_access_preload: open(\"%s\", %d)\n", path, flags);
    orig_func_type orig_func = (orig_func_type)dlsym(RTLD_NEXT, "open");
    return orig_func(path, flags);
}
int open64(const char *path, int flags)
{
    typedef int (*orig_func_type)(const char *pathname, int flags);
    fprintf(stderr, "log_file_access_preload: open64(\"%s\", %d)\n", path, flags);
    orig_func_type orig_func = (orig_func_type)dlsym(RTLD_NEXT, "open64");
    return orig_func(path, flags);
}
//TODO: int openat(int dirfd, const char *path, int flags, mode_t mode)
int openat(int dirfd, const char *path, int flags)
{
    typedef int (*orig_func_type)(int dirfd, const char *pathname, int flags);
    fprintf(stderr, "log_file_access_preload: openat(%d, \"%s\", %d)\n", dirfd, path, flags);
    orig_func_type orig_func = (orig_func_type)dlsym(RTLD_NEXT, "openat");
    return orig_func(dirfd, path, flags);
}

int main()
{
    (void)fopen("test.txt", "r");
}

Compiling as gcc test.c (on Ubuntu 22.04) and calling it as ./a.out does not print anything (if I uncomment fopen interception, then it works but unfortunately fmemopen does not allow creating a corresponding fd/fileno with it, as opposed to shm_open and memfd_create so I prefer to only override open calls)

If I put similar interception in LD_PRELOADed shared library, it still does not get called for an open call (again, it works if I intercept fopen).

If I strace -f ./a.out, then I do get openat(AT_FDCWD, "test.txt", O_RDONLY) = -1 ENOENT (No such file or directory) on the output, so open does get called?

execve("./a.out", ["./a.out"], 0x7ffff9b89518 /* 22 vars */) = 0
brk(NULL)                               = 0x7fffde75e000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffe5547b50) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcee5410000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=103195, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 103195, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fcee53b6000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\244;\374\204(\337f#\315I\214\234\f\256\271\32"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2216304, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2260560, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fcee5180000
mmap(0x7fcee51a8000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7fcee51a8000
mmap(0x7fcee533d000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7fcee533d000
mmap(0x7fcee5395000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x214000) = 0x7fcee5395000
mmap(0x7fcee539b000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fcee539b000
close(3)                                = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcee53b0000
arch_prctl(ARCH_SET_FS, 0x7fcee53b0740) = 0
set_tid_address(0x7fcee53b0a10)         = 5270
set_robust_list(0x7fcee53b0a20, 24)     = 0
rseq(0x7fcee53b10e0, 0x20, 0, 0x53053053) = -1 ENOSYS (Function not implemented)
mprotect(0x7fcee5395000, 16384, PROT_READ) = 0
mprotect(0x7fcee5419000, 4096, PROT_READ) = 0
mprotect(0x7fcee5408000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=8192*1024}) = 0
munmap(0x7fcee53b6000, 103195)          = 0
getrandom("\x05\x04\x5c\x57\xcc\x25\x9c\x8e", 8, GRND_NONBLOCK) = 8
brk(NULL)                               = 0x7fffde75e000
brk(0x7fffde77f000)                     = 0x7fffde77f000
openat(AT_FDCWD, "test.txt", O_RDONLY)  = -1 ENOENT (No such file or directory)
exit_group(0)                           = ?
+++ exited with 0 +++

Why can strace see this call and my interception cannot? Is there a way to make this interception work?

Thanks!


P.S. At the end of the day, I'm looking to create some fd for an in-memory buffer (maybe by memfd_create and then writing to it my buffer and then seeking to 0).

 5  77  5
1 Jan 1970

Solution

 3

Why can strace see this call and my interception cannot?

The strace doesn't see the call to openat in libc, it sees the actual system call, by using an entirely different mechanism (ptrace).

In contrast, what you are trying to do is interpose calls into libc.so using dynamic linker symbol resolution.

This program doesn't call libc at all, but strace will still show openat in it:

        section .text
        global _start
_start:
        mov     rax,257
        mov     rdi,0
        mov     rsi,0
        syscall

        mov     rax,231
        mov     rdi,0
        syscall
nasm -f elf64 x.s && ld x.o && strace ./a.out

execve("./a.out", ["./a.out"], 0x7fff89dee840 /* 30 vars */) = 0
openat(0, NULL, O_RDONLY)               = -1 EFAULT (Bad address)
exit_group(0)                           = ?
+++ exited with 0 +++

Is there a way to make this interception work?

First, let's see how openat is actually invoked. On Fedora 40, using int main() { fopen("test.txt", "r"); }, running to main and using catch syscall openat in GDB:

Catchpoint 1 (call to syscall openat), 0x00007ffff7ed4403 in __libc_open64 (file=0x402012 "test.txt", oflag=0)
    at ../sysdeps/unix/sysv/linux/open64.c:41
Downloading source file /usr/src/debug/glibc-2.39-15.fc40.x86_64/io/../sysdeps/unix/sysv/linux/open64.c
41        return SYSCALL_CANCEL (openat, AT_FDCWD, file, oflag | O_LARGEFILE,
(gdb) bt
#0  0x00007ffff7ed4403 in __libc_open64 (file=0x402012 "test.txt", oflag=0)
    at ../sysdeps/unix/sysv/linux/open64.c:41
#1  0x00007ffff7e56adf in __GI__IO_file_open (fp=fp@entry=0x4052a0, filename=<optimized out>,
    posix_mode=<optimized out>, prot=prot@entry=438, read_write=8, is32not64=<optimized out>) at fileops.c:188
#2  0x00007ffff7e56c95 in _IO_new_file_fopen (fp=fp@entry=0x4052a0,
    filename=filename@entry=0x402012 "test.txt", mode=<optimized out>, mode@entry=0x402010 "r",
    is32not64=is32not64@entry=1) at fileops.c:281
#3  0x00007ffff7e4b046 in __fopen_internal (filename=0x402012 "test.txt", mode=0x402010 "r", is32=1)
    at iofopen.c:75
#4  0x0000000000401139 in main ()

Here you can see that the __libc_open64 is where the system call is executed, and that it is called from __GI__IO_file_open. What does the disassembly at the call site look?

(gdb) x/i $pc-5
   0x7ffff7e56ada <__GI__IO_file_open+42>:      call   0x7ffff7ed43b0 <__libc_open64>

You can see that this call doesn't go through PLT, and thus doesn't participate in the dynamic symbol resolution.

So what can be done about this?

If you control the target environment, you could possibly modify GLIBC source to call open instead of __open in fileops.c and rebuild libc.so.6.

If you need to make this work with an existing libc.so.6, the only option I know if is (very hacky) runtime patching.

Basically you would have to scan the instructions inside __IO_file_open, find a call to __open64, and patch it to call your interposer code instead (with an additional complication is that your interposer can't be more than 2GiB away from the call site).

2024-07-03
Employed Russian