Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Stuff like this is why I'm supportive of newer languages like Go and Zig that sidestep libc entirely (when not using cgo as in TFA of course). libc is a great achievement and has served us well but, boy, it sure is a product of its time.

`errno` is another relic that needs to die yesterday.



Depending on the operating system, you can't skip libc even in Go. I think it's required on openbsd and illumos/solaris for example.

https://utcc.utoronto.ca/~cks/space/blog/programming/Go116Op...


golang used to break all the time on macOS, because it was using the syscall ABI, which isn't stable, instead of libSystem, which is.


It has been fixed recently-ish.


it was fixed by using-the-system-libs


A long time ago, but AFAIK the fix was “use the system's libc”.


Could you link to the fix or their docs on it? I.e. what do they do today?


The comments on this GitHub issue include links to the changes in the golang code review system: https://github.com/golang/go/issues/17490


I think it's the same in Windows, right? Can't use the syscalls underneath the hood, everything through the standard libraries. Maybe I'm wrong (I know very little about Windows other than how to use it to play games, and WSL)


The standard libraries on Windows don't involve libc. The Windows APIs look rather different, and in general are much more friendly to multi-threading. POSIX on the other hand tends to assume that the program is in control of everything happening inside of it, which is an incorrect assumption due to libraries.

In this particular case, the Windows APIs have neither getaddrinfo() nor getenv(); and the closest equivalent GetEnvironmentVariableW is perfectly thread-safe. Microsoft additionally has a C runtime (msvcrt) providing functions like getenv(), but this is much less fundamental than it is on other system. Every program is supposed to ship its own copy of the C runtime, it's not officially part of Windows! And it's perfectly possible for multiple different copies of the C runtime to be loaded into the same Windows process. And since *environ is a variable defined by the C runtime, there's a different copy for each C runtime...


Almost correct, except that since Windows 10 there is now a C runtime shipped as standard, ironically it is actually written in C++ taking advantage of its safety features over plain C, and exposing the C API via extern "C".

https://learn.microsoft.com/en-us/cpp/windows/universal-crt-...


On windows it’s somewhat possible to avoid most of it by linking to ntdll, which only provides symbols for raw syscall wrappers. But a lot of it is unstable and may change from a windows release to the next.

Doing raw syscalls without ntdll is also possible, but windows syscall numbers change on essentially every release, so you’d end up with something that only works on your windows version.


We've been building everything with CGO_ENABLED=0 for years now, with no nasty side effects. It gets to be a pain using the default, when something as innocuous as a point version of a Docker image breaks compatibility because of a glibc version change[1].

[1] golang official image 1.20.4 to 1.20.5 went from Debian 11 to 12 base. Always use the -(debian version) tags.


Split DNS is broken on macOS when doing that, and for users with VPN that does split DNS it is not just an annoyance it leads to software not actually functioning.

Re-implementing system capabilities is fine and all as long as you support common use cases properly, which Golang does not.


And on the flip side, there have been a number of instances where, in cases where the behavior differs, the Golang documentation describes a function only as it behaves with the Golang-native implementation, rather than the system implementation which ends up being the default - without calling any of this out


Yeah, there's so much misery in the C ecosystem that it's better to eschew it altogether. Even merely packaging anything that depends on C ends up being a hugely painful undertaking since every C library has its own bespoke build system and its own implicit set of dependencies (and implicit versions of those dependencies, and expectations about where on the system those dependencies live).

I mostly like C as a language, but between the security concerns and the tooling concerns (and the community's zealous devotion to ignoring these very real problems) I'm really excited for its increasing marginalization. Unfortunately, it's not being marginalized in favor of "a better C", but rather every ecosystem is rewriting the same stuff from scratch which seems like a bit of a bummer (but still better than depending on C).


> since every C library has its own bespoke build system and its own implicit set of dependencies (and

You should see other libraries. At least glibc does not require meson, cmake and ninja.


yes, it only requires autotools. a build system so friendly that it spawned cmake and meson to replace it.


> `errno` is another relic that needs to die yesterday.

ok, i will bite: what is the problem with it ?


Shared global (well, thread local) mutable state.

As for why shared global mutable state is (generally) bad, see: https://softwareengineering.stackexchange.com/questions/1481...

`man 3 errno` on my Linux system even has a note calling out a common failure pattern. Can you spot the problem?

           if (somecall() == -1) {
               printf("somecall() failed\n");
               if (errno == ...) { ... }
           }


regarding your snippet:

           if (somecall() == -1) {
               printf("somecall() failed\n");
               if (errno == ...) { ... }
           }
sure, the issue is that `somecall(...)` might have altered `errno` through 'acts-of-omission-or-comission' :o)

fwiw, posix has updated its definition to pretty much say that 'value of errno in one thread is not affected by assignments to it by another'. this has been the case since at least a decade-and-a-half (iirc), which in internet years would positively be in the pleistocenic era :o)

so, i am not sure i really appreciate 'the shared-global-mutable-state' argument above. thanks !


The problem in that snippet is that `printf` could have altered the `errno` set by `somecall`, and that's only thanks to it being shared-global-mutable-state. You not realizing that was possible makes for a great example of why shared-mutable-global-state is hard to reason about.


crap, i definitely meant to write `printf(...)` there !

my typical usage for such scenarios i.e. when i know that callee might alter errno etc. is to

        int save_errno = errno;

        do_foo(...);

        if (errno == ...) {
          ...
        }
wrapping libc into something that (maybe) does better seems like such a sisyphean task to me.


This thread isn't talking about how to fix the errno problem generally. It's talking about the existence of a problem in the first place. Fixing it would be a whole different can of worms, and indeed, sisyphean sounds about right.

Notice how this entire thread was started by someone asking why errno was problematic. This is just about understanding.


> Notice how this entire thread was started by someone asking why errno was problematic. This is just about understanding.

yes, you are absolutely right. this is just about understanding and how easy it is to miss what is hidden just one-level away.


I specifically mentioned thread local. The problems of shared global mutable state aren't limited to multi threaded environments.


There are 2 errno mistakes in that snippet since there is no way to know who set errno.


What's the alternative?

Returning a"Result" struct doubles the size. This is one less register to use.

Exception handling is even more invasive.

They are great for high(-er) level language, but less prefect on lower level where performance is critical.

EDIT: Linux kernel use negative return value for error. It's good and efficient when it work. But it is not always an option when you need the full register width


You are saving one register at the cost of having a thread local variable that is visible to signal handlers, so none of its uses can be optimized away. Which results in things like gcc having to decorate every math instruction with code to set errno on the off chance that someone somewhere might read it (no one ever does).


> Returning a"Result" struct doubles the size. This is one less register to use.

One less register right before and after returns doesn't sound like a big problem, especially with 16+ registers.


Most of the POSIX functions that use errno for error signaling simply return 0 for success and -1 for error. They could have returned errno directly.


Last I checked, the Linux kernel ABI does return -errno for errors. Then libc mangles that all up.


Some newer POSIX APIs, such as pthreads, do return the error this way. But many legacy APIs, such as dup or read, use the positive integer space, thus the negation pattern you often see in syscalls. Notably, POSIX guarantees <errno.h> values to be positive integers.


`dup` and `read` returns -1 on error and set the `errno` variable. If they were redesigned, they should just return `-errno` on failure.


What about the rest?


Put the original return value in a pointer argument, and return errno (or -errno).


There are a lot of alternatives, and it's not clear why the ones you've suggested are inappropriate. You've listed some perceived costs, but I don't see why those costs are greater than the ones paid by the status quo.

Linux even shows you a path, yet you reject it for reasons that don't seem compelling to me.


The performance cost of having otherwise pure functions clobber global mutable memory defeating many optimization passes, is way higher than clobbering another register for the result.


It's global state for a local condition.

Linus Torvalds on errno: https://yarchive.net/comp/linux/errno.html


> ... Linus Torvalds on errno: https://yarchive.net/comp/linux/errno.html

yes, he argues against its usage in the KERNEL.


But his argument stands even outside the kernel. Errno is awful, similar to locale APIs.


The most obvious way it is wrong is that it is archaic. There is simply no reason to ever pass return values in hidden state. Just use return values damnit.


Most of the still working privilege escalation exploits from 2006 are only still there because it is intended behaviour of glibc.

Environment variables like LD_PRELOAD should never ever be available in production.

I totally understand why the muslc developers kinda freaked out and started their own standard library.


Err no thank you. Ld preload and similar mechanism are great to inject code into apps legitimately, i.e. to patch long unsupported systems or to tame current ones.

For example I have vision issue and without reshade filter I would be unable to play a great deal of games.

Now that is also an attack vector, that's for sure, but you cannot go ax features willy nilly just because you don't see value in them.



LD_PRELOAD won't be needed if the OS were built around containers / jails, instead of the weakly isolated processes and process groups.

The Unix kernel (both Linux, BSD, and Solaris) already had much of what's needed, say, 30 years ago, but nobody saw it as such a burning necessity (likely except Solaris which eventually developed Zones).


On a "normal" desktop system, you don't need containers or jails. Your programs must communicate with each other (copy paste, print screen, etc.).

But today every god damn UI program needs an internet connection to phone home and execute remote code. This is the actual problem which must be fixed.


Are you confusing LD_PRELOAD with LD_LIBRARY_PATH? I'm not sure how jails and containers help with the former.


At least it could be additionally guarded by a system setting or something like that.


Yes, for example by setting an environment variable.


> Err no thank you.

> Err no

Not sure if you are trolling




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: