NetBSD implements various standard C language interfaces such as the time(3) function in POSIX in libc, which has a prototype like this:

time_t time(time_t *);

However, between NetBSD 5 and NetBSD 6, the definition of the type time_t in NetBSD changed on many architectures from 32-bit to 64-bit to avoid the year 2038 problem. So programs compiled in NetBSD<=5 saw a declaration like

int time(int *);

which on most architectures is 32-bit, while programs compiled in NetBSD>=6 see a declaration like

int64_t time(int64_t *);

These declarations are not compatible -- consider a program with a fragment like:

int before, after;


This would work in NetBSD<=5, but in NetBSD>=6, the calls to time(3) might overwrite adjacent positions on the stack, or crash altogether because the argument is misaligned.

Programs written and compiled on older versions of NetBSD are supposed to continue to work -- with suitable emulators/compatNN packages and compatNN.kmod modules or COMPAT_NN kernel options -- on newer versions of NetBSD.

To make this work, NetBSD's libc provides two symbols:

The declaration in newer NetBSD time.h is actually:

time_t time(time_t *) __RENAME(__time50);

where __RENAME(__time50) is a macro expanding to __asm("__time50"), which has the effect that the compiler will use the symbol __time50 for calls to the C function this declares. Thus, old programs with calls to the symbol time using the 32-bit prototype will continue to work, and new programs will be compiled to call the symbol __time50 using the 64-bit prototype. (Details on how the symbols are implemented in libc.)

dlsym(3) and symbol interposition

Programs that use dlsym(3), such as C foreign function interfaces in dynamic languages like Python, need to know that if they want the legacy 32-bit time() function, they must use the symbol time, and if they want the modern 64-bit time() function, they must use the symbol __time50.

Similarly, programs that use LD_PRELOAD (see ld.elf_so(3)) to interpose their own definitions of symbols, such as rumphijack(3) and torsocks, must know to define __time50 if they want to replace the new semantics in new programs, or time if they want to replace the old semantics in old programs.

The same applies to many other standard C functions, such as clock_gettime(3) (__clock_gettime50) and socket(3) (__socket30), which have all had their prototypes or semantics revised at some point.

Symbol interposition is very difficult to get right, and it is hard to make programs that do it reliably. On NetBSD, it should be reserved for certain standard library functions like malloc and free (and calloc and everything else in that family), and some system call stubs; except for the __...50 pseudo-versioned renames of public functions, you should not try to interpose your own definition of any symbol beginning with ‘_’ (a single underscore), which is reserved to the implementation in C.

Appendix: ELF symbol versions

The renaming scheme of __time50 is informal -- any symbol can be renamed the same way, and NetBSD uses it for some other purposes too, such as exposing a slightly different rename(2) function via the symbol __posix_rename in programs that define _POSIX_C_SOURCE but not _NETBSD_SOURCE.

The GNU ELF toolchain (gcc, ld, &c.) supports a formal concept of ‘symbol versions’ with sections called .gnu.version (associating versions with symbols), .gnu.version_d (versions defined in an object), and .gnu.version_n (versions needed in an object). As of 2020, NetBSD does not use ELF symbol versions, although the linker and loader support them for libraries developed outside NetBSD.

The semantics is:

ELF symbol versions versus __...50 pseudo-versions

ELF symbol versions and NetBSD's __time50 pseudo-version renaming scheme both try to address the same problem: making sure old programs that were built under the assumption of the old semantics continue to run unmodified with new libraries.

Both of them run into problems with dlsym(3) and symbol interposition:

Thus, switching from the pseudo-versions we use to ELF symbol versions doesn't improve the dlsym(3) situation -- in fact, it makes the situation worse, by breaking old programs and providing no way for new programs to bind to the name of the current version.

Perhaps we could create a compiler builtin __builtin_asm_name which would expand to the __asm("...") name by which a C identifier has been declared -- then programs could instead do:

__typeof(time) *timep = dlsym(dso, __builtin_asm_name(time));

This way the text of the program is the same no matter how time(3) is declared in the header file, but it will continue to work across changes to the signature of the time(3) function in newer releases of NetBSD.