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

It seems GLIBC 2.28 (released August 2018) made a fairly aggressive change to fcntl. The definition was changed in <fcntl.h> to no longer be an external function, but a #define to fcntl64.

The upshot is that if you compile your code on a system with this glibc--if it uses fcntl() at all--the resulting binary will not execute on a system from before August 2018. 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:

https://linux.die.net/man/2/fcntl

It would be nice if you could tell the linker what specific version of a GLIBC function you wanted. But the closest I found was this trick described in an answer to another post:

Answer to "Linking against older symbol version in a .so file"

This is a bit more complicated. fcntl is variadic without a vffcntl that takes a va_list. In such situations you cannot forward an invocation of a variadic function. :-(

When one has stable code with purposefully low dependencies, it's a let-down to build it on a current Ubuntu...then and have the executable refuse to run on another Ubuntu released only one year prior (nearly to the day). What recourse does one have for this?

Upvotes: 15

Views: 6665

Answers (3)

Eric Tian
Eric Tian

Reputation: 1

It might be a bit late, but I recently encountered the same issue and did some research. Based on your URL reference to fcntl's definition, I noticed it's actually implemented as:

extern int __REDIRECT (fcntl, (int __fd, int __cmd, ...), fcntl64);

Digging deeper, the __REDIRECT macro uses __asm__ ("xyz"), a technique that modifies symbol names at the assembly level.

Why not leverage this same approach to bypass glibc's behavior? Here's a code snippet I used:

#if defined (__GLIBC__) && defined (__GNUC__) && defined (__USE_FILE_OFFSET64)
extern int fcntl_legacy(int __fd, int __cmd, ...) __asm__("fcntl");
#else
#define fcntl_legacy fcntl
#endif

inline int set_nonblocking(int fd)
{
    int flags = fcntl_legacy(fd, F_GETFL, 0);
    if (flags == -1) {
        return -1;
    }

    return fcntl_legacy(fd, F_SETFL, flags | O_NONBLOCK);
}

Tested with -D_FILE_OFFSET_BITS=64 + GCC 14.2.1 + glibc 2.40 on amd64, this successfully redirects the dependency from fcntl64 to the legacy fcntl.

Upvotes: 0

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.

Upvotes: 13

Andrew Henle
Andrew Henle

Reputation: 1

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.

Upvotes: 5

Related Questions