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;
time(&before);
...
time(&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:
time
, which still implements the legacy prototype as before; and__time50
(yes, this is not a typo for__time60
), which implements the new 64-bit prototype.
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:
When creating a library, a version map may be specified like so:
NetBSD_BASE { global: __time50; free; malloc; time; local: *; }; NetBSD_6 { global: time; };
The library can specify what versioned symbol each definition in the library is exposed with:
__asm(".symver time_legacy,time@NetBSD_BASE"); int time_legacy(int *t) { ... } __asm(".symver time64,time@@NetBSD_6"); /* default version */ int64_t time64(int64_t *t) { ... } __asm(".symver __time50,__time50@NetBSD_BASE"); __typeof(time) __time50 __attribute__((__alias__("time64")));
Versions marked with
@@
are default versions; versions marked with@
are non-default.When running a program that was linked without ELF symbol versions, from before the library had ELF symbol versions (like
libc
today), the first version in the map is used to resolve symbols:Old programs calling the legacy
time
symbol will gettime@NetBSD_BASE
, which is defined viatime_legacy
above.Programs calling
__time50
will get__time50@NetBSD_BASE
, which is defined viatime64
above.
When linking a program against a library with symbol versions, the linker will record what the default version was; when later running the program, the stored symbol version will be used. If there is no default version, and the program did not request a specific version with
.symver
, then the linker refuses to link, so obsolete symbols can be ‘removed’ by giving them only non-default versions -- thus old programs continue to work but new programs can't be made that use the obsolete symbols.For example, if time(3) is declared in a header file as simply
typedef int64_t time_t; time_t time(time_t *);
then new programs will be linked against
time@NetBSD_6
, which is the default version for the symbol nametime
. If NetBSD ever changed the prototype of time(3) again, and defined atime@NetBSD_11
as the new default version, existing programs compiled withtime@NetBSD_6
would continue to get the semantics they were built against.Unfortunately, setting only a non-default version doesn't work to compatibly remove obsolete symbols that never had versions in the first place. If a program was already linked with a reference to an unversioned symbol, the runtime loader will refuse to resolve that reference by the non-default version.
When a program uses dlsym(3), it always gets the default version, if any. Programs can request specific versions with dlvsym(3).
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:
A program written today that expects to find the function time() in
libc
, such as a C foreign function interface for a dynamic language like Python, needs to know to calldlsym("__time50")
; otherwise it will get an obsolete definition that does not match the semantics of the current definition oftime_t
, possibly leading to data corruption, crashes, or worse.If
libc
used used ELF symbol versions, thendlsym("time")
would return the modern symbol.But any old programs that used
dlsym("time")
assuming it returned the legacy definition (which was the ‘modern’ definition at the time the programs were written and built) will break if it instead returns the 64-bit definition.And if we ever modified time(3) again (hypothetically, to extend it to 128-bit galactic-scale times), programs written assuming that
dlsym("time")
returns the 64-bit definition will break if it begins to return the 128-bit definition. Programs could future-proof themselves by usingdlsym("time", "NetBSD_6")
explicitly, but this is no better than writingdlsym("__time50")
explicitly.
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.
References
Jörg Sonnenberger, How to break long-term compatibility in NetBSD, AsiaBSDcon 2016.
Ulrich Drepper, How To Write Shared Libraries, 2011-12-10.
Ulrich Drepper, ELF Symbol Versioning