Re: [PATCH] tools/nolibc: rename sys_foo() functions to _sys_foo()

From: Thomas Weißschuh

Date: Sat Apr 04 2026 - 10:29:48 EST


Hi Willy,

On 2026-03-29 07:06:12+0200, Willy Tarreau wrote:
> On Tue, Mar 24, 2026 at 06:01:35PM +0100, Thomas Weißschuh wrote:
> > On 2026-03-22 12:03:30+0100, Willy Tarreau wrote:
> > > On Sun, Mar 22, 2026 at 11:43:56AM +0100, Thomas Weißschuh wrote:
> > > > On 2026-03-22 10:06:42+0100, Willy Tarreau wrote:
> > > > > On Thu, Mar 19, 2026 at 05:20:17PM +0100, Thomas Weißschuh wrote:
> > > > > > The sys_foo() naming scheme used by the syscall wrappers may collide
> > > > > > with application symbols. Especially as 'sys_' is an obvious naming
> > > > > > scheme an application may choose for its own custom systemcall wrappers.
> > > > >
> > > > > Yes but on the other hand it might implement it when missing the one
> > > > > offered by the libc.
> > > >
> > > > I don't really get this sentence. Do you refer to the '#ifdef sys_foo'
> > > > as you mention below?
> > >
> > > Ah no, but rereading my message shows me it was not really parsable :-)
> > > I meant that some applications missing a syscall in nolibc (and detecting
> > > this miss via any method) could decide to implement their own equivalent
> > > for the time it takes to integrate the feature into nolibc, and thus it
> > > can make sense that they call their feature sys_foo like we do, so that
> > > their sys_foo is basically the same as ours (i.e. they really just have
> > > to detect the conflict one way or another but that's all).
> >
> > What is the advantage of switching over to the nolibc-provided
> > implementation? The custom implementation would be good enough anyways.
>
> It's not an "advantage", it's just remaining compatible. When you have
> to locally implement some missing functions, and your build breaks again
> once you update nolibc, it's a real pain, and sometimes depending on the
> environment you have different toolchain versions, yet you don't want to
> be forced to fork your local code which didn't change between the versions.

Using custom symbols which do not collide with the standard ones, looks
like the right choice for that.

> > > In fact it's not about handling different versions, it's really about
> > > working around what's missing. I face this every day as a userland
> > > developer. As soon as you depend on a lib, you figure something is
> > > missing, and you cannot stop your project because of this. So the
> > > right thing to do is to implement what you're missing in a mostly
> > > discoverable way, and the try to upstream your work into the lib that
> > > was missing the feature.
> >
> > That makes sense where you have to work with the external library in the
> > form it is provided to you by somebody else. nolibc should be vendored,
> > so you always know exactly what it supports. Or do you have existing
> > usecases where you detect nolibc features?
>
> I'm not sure to understand what you mean here. Let's take this example
> for illustration, that I have in my preinit code:
>
> static void enable_signals(void)
> {
> struct sigaction act = { 0 };
> sigset_t blk = { 0 };
>
> #ifdef NOLIBC
> #ifdef SA_RESTORER
> act.sa_flags = SA_RESTORER;
> act.sa_restorer = sig_return;
> #endif
> act.sa_handler = sig_handler;
>
> my_syscall4(__NR_rt_sigaction, SIGTERM, &act, NULL, sizeof(sigset_t));
> my_syscall4(__NR_rt_sigaction, SIGINT, &act, NULL, sizeof(sigset_t));
> my_syscall4(__NR_rt_sigprocmask, SIG_SETMASK, &blk, NULL, sizeof(sigset_t));
> #else
> signal(SIGTERM, sig_handler);
> signal(SIGINT, sig_handler);
> sigprocmask(SIG_SETMASK, &blk, NULL);
> #endif
> }
>
> Since this is where nolibc was born, I've always been very careful about
> not redefining standard calls (this is where all the my_* stuff came
> from). A cleaner implementation would have donne this:
>
> #if defined(NOLIBC)
> typedef void (*sighandler_t)(int);
> sighandler_t signal(int signum, sighandler_t handler)
> {
> struct sigaction act = { 0 };
> #ifdef SA_RESTORER
> act.sa_flags = SA_RESTORER;
> act.sa_restorer = sig_return;
> #endif
> act.sa_handler = sig_handler;
>
> my_syscall4(__NR_rt_sigaction, SIGTERM, &act, NULL, sizeof(sigset_t));
> my_syscall4(__NR_rt_sigaction, SIGINT, &act, NULL, sizeof(sigset_t));
> my_syscall4(__NR_rt_sigprocmask, SIG_SETMASK, &blk, NULL, sizeof(sigset_t));
> }
> #endif
>
> static void enable_signals(void)
> {
> sigset_t blk = { 0 };
>
> signal(SIGTERM, sig_handler);
> signal(SIGINT, sig_handler);
> sigprocmask(SIG_SETMASK, &blk, NULL);
> }
>
> With a patch applied to nolibc to include that new signal() definition.
> But the day nolibc is updated, this code would break, with no easy way
> to detect the support of the function. By experience I know pretty well
> how it would end up, I'd look for a #define that appeared in a very
> close version and would detect it as a proxy for the supporting nolibc
> version. But this remains ugly.

My original point was, that such a patch would not come out of the blue.
The users copy of nolibc probably lies right next to their own
applications code. When they update that copy of nolibc, introducing a
conflict, they can choose any resolution they like.

I agree that the version detection would be ugly.

> A cleaner approach would be something like this:
>
> #if defined(NOLIBC) && !defined(signal)
>
> or maybe:
>
> #if defined(NOLIBC) && !defined(_NOLIBC_HAVE_signal)

But will nolibc ever even implement signal()? We don't know yet.
If signal() is available, does it also mean that sigprocmask() is
available? What about SIGTERM/SIGINT? Do we need feature flags for all
of them? Will the users check for all of them, especially given that
they don't even know yet if any of this will every be implemented in
nolibc, or if it is only confusing dead code.

If a user wants to be really defensive, they can use your aproach 1)
if not they can use 2). Both are fine and already work.

> Note that it could work both ways. For example we removed support for
> wait4() a while ago, and it could have been nice to be able to detect
> it as well.

There was no way to anticipate the removal of wait4(), so this would
only help users to adapt their code *after* they knew about it.
Instead of guarding their usage of wait4() behind the ifdeffery it would
be simpler to either switch to another wait*() function or copy the old
nolibc definition of wait4() into their own codebase as my_wait4().

> > > Inside the kernel, this problem is less visible because it's the same
> > > repository, so anyone working on their selftest etc can also directly
> > > commit into the nolibc subdir as well. When you're using code outside
> > > it's totally different, as you have less control over the selection of
> > > kernel headers version, hence nolibc version.
> >
> > With the UAPI headers the detection makes slightly more sense.
> > On the other hand we try to be backwards-compatible to fairly old
> > versions.
>
> Not that much actually. I got some breakage a few months ago due to
> upgrading nolibc for a coworker to benefit from something I don't
> remember and that wouldn't build. It just failed to boot because of
> the recent abandon for stat() in favor of statx() that causes silent
> failures when __NR_statx is not defined. It turns out that the toolchain
> was built with support for kernel 4.9 and above and didn't need to be
> upgraded, and since kernel forward compatibility is pretty good, this
> has always been fine. But there sys_stat() was silently implemented as
> "return -ENOSYS" which made the system fail to boot. I could quickly
> bisect and offer him a copy of nolibc from 6.1 which was still
> compatible and had the missing feature I needed. As a comparison, I
> checked, and glibc-2.44 on my local machine was built with support
> for kernel 4.4 and above, and it probably still supports much older
> ones for various reasons.
>
> This made me think that it was not a good idea in the end to report
> -ENOSYS, it should only be left to the kernel (i.e. runtime detect)
> but not build time detection. In other programs I've employed link
> time failure for unsupported features. This is quite efficient. For
> example in sys_statx() we could have done something like this instead
> of return -ENOSYS:
>
> extern int __nolibc_API_failure_statx_requires_NR_statx;
> return __nolibc_API_failure_statx_requires_NR_statx;
>
> This way it's discoverable at build time if the function is used.

We could have an option to make __nolibc_enosys() unlinkable in
general. That will automatically work for all its callers.
Or better yet, turn the logic around have the users opt-in to the
-ENOSYS fallback. __nolibc_enosys() was more relevant when we used it
from the 32-bit time functions.

> But I'm digressing (not that much actually since we're discussing
> features discoverability).
>
> > > I personally don't think that a single define in front of each syscall
> > > definition will make the code harder to read. It can be argued that glibc
> > > doesn't offer this and that the user application will have to compose with
> > > all of this, however glibc offers lots more stuff than we do, and users
> > > rely on its version to guess whether or not something is present, which
> > > is something we don't really have.
> >
> > I don't think it makes the code worse or really have anything against it
> > in general. I just don't really see the advantage. For glibc this makes
> > sense, as an application will have to do with the version that is
> > present during compilation and at runtime. For nolibc this doesn't
> > really happen. Or I might be missing something :-)
>
> It's exactly the same actually. You use a nolibc repository to build your
> code, your kernel headers come from anywhere, very often from the glibc-
> based cross-compiler that includes support for the oldest kernel your
> build system supports, and that's done. If you remember, originally there
> was only nolibc.h to include and nothing else.

But nolibc/ or nolibc.h does not come from the cross-compiler. The user
had to get it from somewhere else, so it shouldn't change without
specific user action.

> > If we adopt that scheme it should not only cover the _sys_*() functions
> > but also the regular ones, no?
>
> I've been wondering about this and probably yes. When I look at my preinit,
> there are my_memmove(), my_strcpy(), my_strlen(), my_strlcpy(), my_getenv(),
> my_atoul(), in addition for the syscall definitions. So yes, this
> illustrates that it would be desirable to have that for the rest as well.
> Maybe it ends up being about making the public API discoverable at build
> time after all.

How should you have known which of these features would be implemented
in nolibc in the future? Preemptively adding a bunch of ifdeffery around
all of them would only clutter the code.
The my_*() variants worked nicely in the past and still do.

> We could possibly also just use a version that applications could check
> against. While the project has no declared version, we could for example
> adopt the kernel's version since it's where it mostly evolves nowadays,
> so maybe we could set a pair of _NOLIBC_KVER_MAJOR and _NOLIBC_KVER_MINOR
> macros reflecting the kernel version this comes form (which is not the
> same as the UAPI version but could be set at install time). At least with
> a single version it would cover everything, and it's not much different
> from what can be done with glibc.

I am not really a fan of version definitions. But it would be easier to
test than adding feature defines for all symbols which could change.


Thomas