How to force linkage to older libc `fcntl` instead of `fcntl64`?

How to force linkage to older libc fcntl instead of fcntl64?

Compile against an older version of libc. Period.

Because glibc is not forward compatible, it is only backwards-compatible:

The GNU C Library is designed to be a backwards compatible, portable, and high performance ISO C library. It aims to follow all relevant standards including ISO C11, POSIX.1-2008, and IEEE 754-2008.

Without any guarantees of forward compatibility, you don't know what else won't work properly.


What recourse does one have for this?

The fact that GLIBC didn't have a way to #define USE_FCNTL_NOT_FCNTL64 says a lot. Be it right or wrong, most OS+toolchain makers seem to have decided that targeting binaries for older versions of their systems from a newer one is not a high priority.

The path of least resistance is to keep a virtual machine around of the oldest OS+toolchain that builds your project. Use that to make binaries whenever you think that binary will be run on an old system.

But...

  • If you believe your usages are in the subset of fcntl() calls that are not affected by the offset size change (which is to say you don't use byte range locks)
  • OR are willing to vet your code for the offset cases to use a backwards-compatible structure definition
  • AND are not scared of voodoo

...then keep reading.

The name is different, and fcntl is variadic without a vffcntl that takes a va_list. In such situations you cannot forward an invocation of a variadic function.

...then to apply the wrapping trick mentioned, you have to go line-by-line through fcntl()'s interface documentation, unpack the variadic as it would, and then call the wrapped version with a new variadic invocation.

Fortunately it's not that difficult a case (fcntl takes 0 or 1 arguments with documented types). To try saving anyone else some trouble, here's code for that. Be sure to pass --wrap=fcntl64 to the linker (-Wl,--wrap=fcntl64 if not calling ld directly):

asm (".symver fcntl64, fcntl@GLIBC_2.2.5");

extern "C" int __wrap_fcntl64(int fd, int cmd, ...)
{
    int result;
    va_list va;
    va_start(va, cmd);

    switch (cmd) {
      //
      // File descriptor flags
      //
      case F_GETFD: goto takes_void;
      case F_SETFD: goto takes_int;

      // File status flags
      //
      case F_GETFL: goto takes_void;
      case F_SETFL: goto takes_int;

      // File byte range locking, not held across fork() or clone()
      //
      case F_SETLK: goto takes_flock_ptr_INCOMPATIBLE;
      case F_SETLKW: goto takes_flock_ptr_INCOMPATIBLE;
      case F_GETLK: goto takes_flock_ptr_INCOMPATIBLE;

      // File byte range locking, held across fork()/clone() -- Not POSIX
      //
      case F_OFD_SETLK: goto takes_flock_ptr_INCOMPATIBLE;
      case F_OFD_SETLKW: goto takes_flock_ptr_INCOMPATIBLE;
      case F_OFD_GETLK: goto takes_flock_ptr_INCOMPATIBLE;

      // Managing I/O availability signals
      //
      case F_GETOWN: goto takes_void;
      case F_SETOWN: goto takes_int;
      case F_GETOWN_EX: goto takes_f_owner_ex_ptr;
      case F_SETOWN_EX: goto takes_f_owner_ex_ptr;
      case F_GETSIG: goto takes_void;
      case F_SETSIG: goto takes_int;

      // Notified when process tries to open or truncate file (Linux 2.4+)
      //
      case F_SETLEASE: goto takes_int;
      case F_GETLEASE: goto takes_void;

      // File and directory change notification
      //
      case F_NOTIFY: goto takes_int;

      // Changing pipe capacity (Linux 2.6.35+)
      //
      case F_SETPIPE_SZ: goto takes_int;
      case F_GETPIPE_SZ: goto takes_void;

      // File sealing (Linux 3.17+)
      //
      case F_ADD_SEALS: goto takes_int;
      case F_GET_SEALS: goto takes_void;

      // File read/write hints (Linux 4.13+)
      //
      case F_GET_RW_HINT: goto takes_uint64_t_ptr;
      case F_SET_RW_HINT: goto takes_uint64_t_ptr;
      case F_GET_FILE_RW_HINT: goto takes_uint64_t_ptr;
      case F_SET_FILE_RW_HINT: goto takes_uint64_t_ptr;

      default:
        fprintf(stderr, "fcntl64 workaround got unknown F_XXX constant")
    }

  takes_void:
    va_end(va);
    return fcntl64(fd, cmd);

  takes_int:
    result = fcntl64(fd, cmd, va_arg(va, int));
    va_end(va);
    return result;

  takes_flock_ptr_INCOMPATIBLE:
    //
    // !!! This is the breaking case: the size of the flock
    // structure changed to accommodate larger files.  If you
    // need this, you'll have to define a compatibility struct
    // with the older glibc and make your own entry point using it,
    // then call fcntl64() with it directly (bear in mind that has
    // been remapped to the old fcntl())
    // 
    fprintf(stderr, "fcntl64 hack can't use glibc flock directly");
    exit(1);

  takes_f_owner_ex_ptr:
    result = fcntl64(fd, cmd, va_arg(va, struct f_owner_ex*));
    va_end(va);
    return result;

  takes_uint64_t_ptr:
    result = fcntl64(fd, cmd, va_arg(va, uint64_t*));
    va_end(va);
    return result;
}

Note that depending on what version you're actually building on, you might have to #ifdef some of those flag sections out if they're unavailable.

This affects quite a variety of applications...the manual page for fcntl() shows that it's the entry point for a small universe of sub-functions

...and it should probably be a lesson to people: avoid creating such "kitchen sink" functions through variadic abuse.