I am trying to write an interceptor for fcntl. What differentiates it from a “normal” intercepted function, is that it requires a variadic parameter in it’s third argument:
int fcntl(int fildes, int cmd, ...);
The problem is that based on the value of cmd, the variadic parameter can be empty, or have one argument (which often has different types).
A naive approach may do something like:
case F_GETFL:
case F_GETFD:
case F_GETOWN:
// no arguments
return REAL(fcntl)(filedes, cmd);
case F_SETFL:
// one argument that is an int
// unpack va_args and pass it in to the real function
va_list args;
va_start(args, cmd);
int flags = va_arg ...
va_end(args);
return REAL(fcntl)(filedes, cmd, flags);
This seems like unsustainable insanity. It seems like the cmds differ system by system, and would be a general maintenance nightmare.
Is there any way to safely and maintainably intercept fcntl? Or am I better off ditching this interceptor altogether?
Is it possible to do in a way that’s actually portable? No.
But is it possible in practice? Yes. Just fetch the argument unconditionally via arg = va_arg(args, unsigned long) and make the call with REAL(fcntl)(filedes, cmd, arg); – regardless of what cmd is.
This works on common platforms (all the platforms that I know of), because the calling convention for vararg functions either specifies that the argument is passed in a GPR (where int is passed the same way as long), or that values will be passed in a stack slot the size of a GPR (so, again, int is passed the same way as long).
You might find the discussion on this FreeBSD review informative.
If you’re going to make a blind va_arg() call, I’d suggest intptr_t vs long as being closest to correct. (It still won’t work on CHERI systems due to va_list being a bounded array which has size=0 when no argument is passed. We generally solve this in libc by enumerating all the commands which we can do for fcntl and open, but with ioctl there’s no avoiding UB except by peeking into the implementation of va_list (cheribsd/lib/libsys/ioctl.c at dev · CTSRD-CHERI/cheribsd · GitHub))
All of this is certainly UB. Calling va_arg with the wrong type, or with an argument where none exists, is wrong and bad.
I kinda hate saying this, because the “I know what the underlying machine implementation I expect the compiler to generate will be, so my UB is totally fine” thinking causes so many problems in C and C++ ecosystem…but: Can you get away with this? I do believe you can. (except on CHERI, as noted above.)
FWIW, see also the implementation in glibc and musl, which do this, too. That they also find the need to makes sense, as on Linux, libc itself is effectively intercepting arbitrary fcntl calls, from user code to the independently-maintained Linux kernel.