Getaddrinfo() on glibc calls getenv(), oh boy
rachelbythebay.com
rachelbythebay.com
I'd say you shouldn't be calling setenv() at all once you've spawned threads.
"But Go compiled it just fine..."
In case you don't know, cross-building with GOOS/GOARCH will imply CGO_ENABLED=0 unless you also specify CC_FOR_${GOOS}_${GOARCH}; I cross-build most of my code for (and test it on) amd64, arm64, linux, openbsd, and darwin.
Go will sometimes link to the local libc for network-related functionality if you don't disable cgo.
But, yes, in general for libc, if the manpage didn't say it's thread-safe, it is unsafe.
Time, date, physical spellings, ... many things are locale dependant, but socket stuff?.
It comes as a Surprise!!, and not the good kind, to many a network programmer with just a few years under their belt to discover threaded networking can segfault because of this.
Once you know, you know and don't forget (until next time), but I suspect this was the motivation behind the blog posting, the principal of potentially most surprise.
We really should have embraced a better primitive than "shared memory execution environment"
The alternative that I have seen be successful, is to achieve parallelism by forking separate processes wherever you would have spawned threads, and then communicate through shared memory regions.
It's a lot like having Rust-style unsafe blocks, in that you know that if you are having a thread-safety issue it will definitely be in one of the code sections where you are touching the shared memory region, and not anywhere else.
Obviously there's a higher startup cost for forking, but this makes it possible to gain parallelism without breaking all the thread-unsafe code that is certainly in an existing large project.
That's why I hate ZeroMQ. They spawn threads and do magics behind your back.
If you're using a library like that it comes with the territory, and you have to decide for yourself the cost-benefit of using non blocking sockets and async io yourself, or trust a library.
Much more fun to write these things yourself sometimes, but I find e.g. libevent a nice somewhere-in-between abstraction level I can be happy with.
Or, you know, get a better C library. https://src.illumos.org/source/xref/illumos-gate/usr/src/lib...
I think the gist of it is that unlike the glibc implementation, the Illumos implementation of getenv() is lock-free and thread-safe.
I imagine that it is meant to convey that the quality is higher in this library than in GNU libc.
Why don’t the OS libraries have some sort of lock around setenv/getenv, so that only one thread can be inside them at a time? I can’t see how it could deadlock. And surely no-one is so dependent on the performance of these calls that the time to lock/unlock would be problematic?
I'm pretty sure the Windows version of the environment calls has locking.
Historically you can access the environment via a global variable too, which would side-step locking schemes. But probably hardly anybody does that anymore.
Can you be more specific than this? I kind of doubt it.
For example, seems to me you could write a setenv() that uses a lock-free algorithm or writes in a strategic way that won't result in a fault if getenv() or reading environ(7) runs concurrently, then say all bets are off for thread safety if you write via environ(7). That's safer than the status quo and I don't foresee it breaking POSIX.
The XSI spec does state that the result of getenv() may be overwritten by putenv() but that's not a strict requirement either.
You still risk programs and libraries expecting getenv to always return a pointer to *environ failing (I believe Go has an issue like that on MUSL).
On the other hand, the POSIX standard explicitly states that getenv() does not need to be reentrant (and therefore doesn't need to be thread safe) so any program relying on thread-safe getenv is already violating the API contract.
The rationale also seems to assume you can't make this stuff thread safe because of the standard implementation:
> The getenv() function is inherently not reentrant because it returns a value pointing to static data.
It's important to note the difference between reentrant and thread safe. The most obvious implementation of getenv(), which would just loop through environ(7) and do a bunch of strncmp, can safely be re-entered, in that you could interrupt it and call it again and it would produce no ill effect. It just can't be overlapped with writes.
[1]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...
[2]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/s...
I can't think of any examples offhand, but I often think it about thread-local storage. Eg. lots of interfaces have an _r() equivalent where you provide the buffer, but many people still call the unsafe one which is broken when there are threads... In my mind, the best way to do this would be to use static thread-local storage in the non-_r() one, and have it call the _r() one ... Sure that has overhead and isn't a perfect solution, but it's better than "bad". But a lot of these old functions don't necessarily get love.
Perhaps this would be a good feature of an assert, or something that breaks in a debugger if it's attached. But I don't think that is reasonable for production.
Edit: In hindsight, a dynamic buffer would require returning ENOMEM errors (which might lead to some unexpected failures), while a static buffer would limit the value length. I think you might be right about the API being broken.
You could also require callers free the returned memory when they're done, but that would be another change of API.
This would break very simple single-threaded programs that e.g. print two env vars in one printf call.
You provide the storage and free it
The problem is these non-direct uses. They each need to switch to •_r and manage the buffer, or offer _r versions themselves and sort of pass through the problem
- return a pointer - the library owns the allocation - the state is global and mutable
Thread safe
There are, however, other problems (discussed elsewhere in this thread) that complicate such an API in the context of getenv().
We need a new API which is not broken like in NetBSD, and a multi-year migration of all core libraries to it. Well a pity it wasn't started years ago though, could've been 95% done by now.
They could easily be made thread safe, but, paraphrasing, most arguments seem to come down to something like:
“setenv and getenv are POSIX functions, and not defined to lock. Just like many POSIX functions, they’ve _never_ been thread safe, and it’s an error to assume they are. Should we really start papering over client errors in use of a supposedly portable API, even though it’s working as specified? And if we make that choice pragmatically for this instance, should we be trying to do it for _all_ of POSIX? That’s impossible for some things, and would add complexity even where it’s not. For all these reasons, it’s better if these just stay dangerous like they’ve always been.”
For example in rust, multiple time libraries were found to be unsound if `std::env::set_env` was ever called from a multi-threaded program. See:
https://github.com/time-rs/time/issues/293 and https://github.com/chronotope/chrono/issues/499
Alas I am not as OS dev, I have not the skills or understanding to not how to build that, or what this would involve, but I do think it’s clear that what we have at the moment isn’t as well suited as it could be. Io_uring / Direct-IO seem to be better suited though.
Even on OSes that happen to use the Linux kernel like Android, those that insist on using the NDK and pretend it is like GNU/Linux, beyond the official supported use cases, end up bumping their heads against the wall.
For the rest of us, I guess this is the best option. We can do wrappers, but, yak!
threaded programs should probably seriously consider retiring libc, but we don’t currently have a common ground replacement.
name related activities are one of the worst areas, contributing significantly to the glibc linkage and abi challenges, but also lacking sufficient standards for alternatives or even consensus to be built quickly.
NetBSD has getenv_r, which copies into a buffer, but few applications use getenv_r, and certainly not all of them. And it doesn't resolve environ.
Solaris never free's env strings or environ arrays, only creating new copies and atomically swapping them. It uses a special allocator for those objects which doubles the backing buffer each time it deep copies the environ array, then argues this strategy is technically asymptotically memory bounded.
EDIT: Glancing at the code I think glibc is similar to Solaris in that it never free's env strings, but it has a heuristic to conditionally free environ arrays which means directly using environ isn't thread-safe.
POSIX explicitly states "The string [returned by getenv] may be overwritten by a subsequent call to getenv(), setenv(), unsetenv(), or putenv()" (https://pubs.opengroup.org/onlinepubs/9699919799.2008edition...).
We inherited our implementation from OpenSolaris, as did Oracle when they created their Solaris 11 fork. I expect at least some of the BSDs have probably fixed this as well.
`setenv` is a nasty beast, especially since the raw `environ` variable is also exposed (and is in fact the only way to enumerate the environment).
That being said, I went to look[0] and it turns out it wasn’t a lie. [0]https://github.com/illumos/illumos-gate/blob/master/usr/src/...
The getenv/setenv/putenv/environ API looks terrible on closer inspection -- it does not appear possible for an implementation to be safe, leak-free, and efficient.
The comment about "this is safe for people walking `environ`" is definitely a lie, for example, though the bug might be hidden on some common architectures with popular compilers in their default configuration.
Kill it all with fire.
I’ve got a small build system and the first thing it does is nuke PATH to empty. It’s glorious. No more grabbing random shit from a big blob with untracked dependencies that varies wildly by system!
I could easily live my entire life without environment variables. They’re just a fundamentally bad idea. Every program that foolishly uses environment variables can be replaced by a better program that takes a config file or arglist.
10 processes can have completely different set of values for the same environment variables, because they are in their own environments, and apparently, that's useful.
There are foot guns, and there are unintentional consequences of implementation and design details. This is why we patch, improve and rewrite our software over time. To iron out these kinks.
Fire is also have a tendency to cause collateral damage. So use both fire and environment variables responsibly, and world will be a better place.
Basically, if you `putenv()`, you commit to never freeing that memory.
Would there be any significant downsides (besides breakage) to mapping `environ(7)` as read-only? That seems like the kind of thing that a Linux distribution (or more realistically OpenBSD) could do as a way to kill off a persistent family of bugs.
There are alternatives, but rewriting every program is not always an option.
These kind of rewriting sounds exactly like what OpenBSD would do ..
Searching in github, other example are:
1) `jq` parsing datetime
2) systemd , timectl printing status
3) Unit tests
4) Some RDP client update its own timezone with server timezoneexecve(3): https://linux.die.net/man/3/execve
A lot of code, for good reasons, assume envvars are constants set before the program started and caches computations based on them, read config files and so on.
The fact that they are essentially global variables should be enough to deter usage of them.
Env var behaviour is much closer to dynamic variables, rather than global variables (which I argue at http://www.chriswarbo.net/blog/2021-04-08-env_vars.html )
Either way, I agree that mutating them is usually a bad idea; though I find them very good for constant (or dynamically-bound) config.
I really think it would be worth creating a new standard API that is built with threading in mind, where functions like mktime, getaddrinfo, localtime, etc. take arguments instead of reading from the environment, that avoid global state as much as possible, and are thread safe if there is global state.
I also wonder about OSX's libc. Newer versions seem to have some sort of locking https://github.com/apple-open-source-mirror/Libc/blob/master... but they free pointers so it's not safe from a potential use-after-free client side.
but older versions (from 10.9) don't even have any locking: https://github.com/apple-oss-distributions/Libc/blob/Libc-99...
Also most set errno, ha!
libc is kinda schizophrenic in this regard. It has mostly obviously low-level functions like string manipulation and memory management, and then unexpectedly a DNS client implementation and a support for arbitrary runtime plugins (for PAM).
It's still a bit worrying, with manual JSON parsing: https://github.com/systemd/systemd/blob/79f487038444646f5bce... But at least it's just ~600 lines of fairly straightforward code.
You also can get most of it with nscd.
(Sigh, I wish BUS1 guys pushed their project to completion)
Or are there other sneaky calls which will do that behind you back?
Ed: actually, that’s even spelled out in the headline.
It is best to avoid calling setenv in a threaded program. Some programs do it to make space for rewriting argv with large strings (freeing space from *environ which tends to be right after the tail of argv). Some programs or libraries use *environ directly to stage variables for exec before forking. Some want to pass variable changes to forks. There are alternatives possible, but in the context of something like go calling libc setenv, it’s to make interop easier- sadly it may make other interop harder, such as this case.
It's easy to think about some complex interactive software where the need to call setenv appears only after you have worker threads doing some other thing. Without a warning, you won't know it's a bad thing to do, and the manpage only says that it and unsetenv are not thread safe, as if this was remotely enough information.
What nobody is telling is that the environment is so big that you need it to compress data or open an IPv6 connection. It's not obvious at all that you can't do those things while editing a variable.
I may think I have control, I may believe that a handful of us are entitled to have that say, but all it takes is someone adding a cross dependency that forces an existing piece of code to jump from 20th position in the load order to 6th and all hell can break loose. Or just as often, set a ticking time bomb that nobody notices until there’s a scaling or peak traffic event or someone adds one more small mistake to the code and foomp! up it goes.
Which is why on Windows there is almost no system APIs (well, almost: there were some weird technical decisions around single-threaded apartments for COM...) that can be safely used only in single-threaded applications.
Maybe in several more decades Linux community will also accept the fact that multi-threaded applications are an entirely normal and inevitable thing, not an aberration of nature that we all best pretend don't exist until we're absolutely forced to deal with their reality.
The man page for OpenBSD indicates the same thing: https://man.openbsd.org/resolv.conf#ENVIRONMENT
Apparently it's not a gnu-specific behaviour.
Other issues I faced :
- Not epoll() friendly. Always forks a process while resolving domain name.
- Valgrind complains of uninitialized memory touches when the function is called and I can't get rid of it.
For example, in a split-VPN situation?
But parsing /etc/resolve.conf and using it is all that you need in your code.
This works as long as you don't need support for mDNS or LDAP host resolution, which depends on libnss/nsswitch on glibc-based systems. Which is fine, but should be a well documented limit of this of approach.
(this is also what the Go runtime does by default, but they automatically fall back to the glibc resolver in any more complex case: https://pkg.go.dev/net#hdr-Name_Resolution)
In a way, I think the fact that many library functions are not thread-safe should be viewed as an encouragement to not use threads, or use them only for the bare minimum necessary.
I say this from a few decades of experience fighting with race conditions and the like, and whereupon several times I rewrote an existing multithreaded process into a single-threaded one and greatly improved performance and reduced memory usage. The architecture astronauts may have moved on to stuff like microservices now, but in the 90s/2000s threads were overused just as much.
Don't believe me? Look at this work of art (the entire file!): https://src.illumos.org/source/xref/illumos-gate/usr/src/lib...
And I'd be interested in your ideas on removing the lock - as I can't see any paths that don't change semantics (e.g. unconditionally doing the init work at process start time when you know there's not multiple threads, for example)
POSIX implements a three-argument `main` function (just look at `exec()`) where the third argument is `char* envp[]`. You can call `setenv` to manipulate it.
But easiest is to just null out the POSIX extern `char* environ` (save a copy if you want to consult it yourself later). Just `man 7 environ`
In particular, setting a previously-undefined variable causes `environ` to be reallocated. Whereas `setenv()` of an extant variable changes just that value in the current `environ` pointer array.
And then wonder why some shared library dependency few layers deep blew up.
That might mean a viable workaround is enabling nscd, oddly enough.
And frankly, maybe libpthread should just overlay thread-safe getenv/setenv like I believe it does for a couple of other libc symbols.
- immediately copy the contents of the buffer the pointer from getenv() points to
- don't use getenv after threads have been started
A library could be written which makes an immutable copy of the whole environment before starting main(). This library then hands out pointers to the environment copy. Or to be even more secure make another copy of the environment variable. This trades some efficiency for security.
In effect, ignore the mutable accessors like setenv from libc.
Or did I miss something? I am not an expert in these things.
And of course it won't solve the problem of two other libraries fighting with each other...
If environment variables are for child processes only, there is no need to use setenv because you can pass a specified environment array through exec.
This is basically what dnsmasq does when you use it as a local DNS cache.
Normally, that's a socket in /run/dbus/system_bus_socket
This looks insane and not what strace(1) tells me
Could you give me more details ?
I thought it was just a general libc thing. Isn't there a spec on this somewhere?
POSIX mandates getaddrinfo to be thread safe.
Once you do unsafe things, the nasal demons can spread to safe code elsewhere.
Glibc's documentation is more explicit about the propogation of the nasal demons:
┌─────────────────────┬───────────────┬─────────────────────┐
│Interface │ Attribute │ Value │
├─────────────────────┼───────────────┼─────────────────────┤
│setenv(), unsetenv() │ Thread safety │ MT-Unsafe const:env │
└─────────────────────┴───────────────┴─────────────────────┘
┌────────────────┬───────────────┬────────────────────┐
│Interface │ Attribute │ Value │
├────────────────┼───────────────┼────────────────────┤
│getaddrinfo() │ Thread safety │ MT-Safe env locale │
├────────────────┼───────────────┼────────────────────┤
│freeaddrinfo(), │ Thread safety │ MT-Safe │
│gai_strerror() │ │ │
└────────────────┴───────────────┴────────────────────┘ const Functions marked with const as an MT-Safety issue non-
atomically modify internal objects that are better
regarded as constant, because a substantial portion of the
GNU C Library accesses them without synchronization.
Unlike race, which causes both readers and writers of
internal objects to be regarded as MT-Unsafe, this mark is
applied to writers only. Writers remain MT-Unsafe to
call, but the then-mandatory constness of objects they
modify enables readers to be regarded as MT-Safe (as long
as no other reasons for them to be unsafe remain), since
the lack of synchronization is not a problem when the
objects are effectively constant.
The identifier that follows the const mark will appear by
itself as a safety note in readers. Programs that wish to
work around this safety issue, so as to call writers, may
use a non-recursive read-write lock associated with the
identifier, and guard all calls to functions marked with
const followed by the identifier with a write lock, and
all calls to functions marked with the identifier by
itself with a read lock.
and env Functions marked with env as an MT-Safety issue access the
environment with getenv(3) or similar, without any guards
to ensure safety in the presence of concurrent
modifications.
We do not mark these functions as MT-Unsafe, however,
because functions that modify the environment are all
marked with const:env and regarded as unsafe. Being
unsafe, the latter are not to be called when multiple
threads are running or asynchronous signals are enabled,
and so the environment can be considered effectively
constant in these contexts, which makes the former safe.Error can spread in your program in funny way, which also break the program in funny way (think about stack overflow or double free).
Unless the manual/document explicitly stated what error can it cause, there is no way to know without actually trigger it.
`errno` is another relic that needs to die yesterday.
https://utcc.utoronto.ca/~cks/space/blog/programming/Go116Op...
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...
https://learn.microsoft.com/en-us/cpp/windows/universal-crt-...
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.
[1] golang official image 1.20.4 to 1.20.5 went from Debian 11 to 12 base. Always use the -(debian version) tags.
Re-implementing system capabilities is fine and all as long as you support common use cases properly, which Golang does not.
ok, i will bite: what is the problem with it ?
Linus Torvalds on errno: https://yarchive.net/comp/linux/errno.html
yes, he argues against its usage in the KERNEL.
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 == ...) { ... }
}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
Linux even shows you a path, yet you reject it for reasons that don't seem compelling to me.
One less register right before and after returns doesn't sound like a big problem, especially with 16+ registers.
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 !
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.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 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).
You should see other libraries. At least glibc does not require meson, cmake and ninja.
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.
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.
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).
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.
> Err no
Not sure if you are trolling
Your program gets its own copy of the environment when the program was launched. Nobody is changing it on you, any contention for that resource is you contending with yourself.
You don't expect an operating system to change things out from under you. Unix doesn't. If there is contention for this resource, it's all you (or whoever wrote the library you are using)
The environment is name-value pairs, as strings. That's it. That's what makes it accessible and useful. You can swallow it up, the whole thing, into whatever data structure you prefer in your language in a few lines of code, and a millisecond (if that much) of runtime. Just learn how things work and you won't feel helpless.
It definitely seems like that syscall is a (multithreaded) footgun that Go just happened to hit.
There is no clear/obvious mention of the fact that setenv could interfere with it. It is a glibc footgun/beartrap that this re-entrancy doesn't actually mean calling it with non-shared memory in the arguments ensures no data races.
But from what I read, it's documented as not threadsafe and this problem is happening in a threaded environment. That still could be called a bug. You find bugs, you fix them;
That's better than hyperventilating about how there is some massive problem with an operating system that you are using because all the people who came before you who knew more than you decided it was the best thing to use. The other OSes sucked more. This OS is simple enough that you can actually learn how it works, it's all laid out in front of you.
Should it do some additional things? sure. Help write the code.
My point (which made reference to the actual OS documentation) was that it takes being bit by such issues before you learn what this "Attributes" section means:
> │Interface │ Attribute │ Value │
> │getaddrinfo() │ Thread safety │ MT-Safe env locale │
For the 1st decade of my career, I just searched for "re-entrant", "reentrant", or "MT" and went on my merry way, without realizing the importance of the other possible values of attributes table: in this case "env".
It's worth highlighting!
Instead of jumping to "It shouldn't work this way" consider "Hmmm, why does it work this way? Is it possible that Thompson, Kernighan and Ritchie knew more about how to make a coherent straightforward system that would last 50 years in 1970 than I know how to now?"
It sounds to me like you're hyperventilating about other people pointing out footguns. Maybe, just maybe, people in the past were capable of making mistakes. Let's not put them on a pedestal.
┌───────────────────────────┬───────────────┬────────────────────┐
│Interface │ Attribute │ Value │
├───────────────────────────┼───────────────┼────────────────────┤
│getaddrinfo() │ Thread safety │ MT-Safe env locale │
├───────────────────────────┼───────────────┼────────────────────┤
│freeaddrinfo(), │ Thread safety │ MT-Safe │
│gai_strerror() │ │ │
└───────────────────────────┴───────────────┴────────────────────┘
MT-Safe
MT-Safe or Thread-Safe functions are safe to call in the
presence of other threads. MT, in MT-Safe, stands for
Multi Thread.
Being MT-Safe does not imply a function is atomic, nor
that it uses any of the memory synchronization mechanisms
POSIX exposes to users. It is even possible that calling
MT-Safe functions in sequence does not yield an MT-Safe
combination. For example, having a thread call two MT-
Safe functions one right after the other does not
guarantee behavior equivalent to atomic execution of a
combination of both functions, since concurrent calls in
other threads may interfere in a destructive way.
Whole-program optimizations that could inline functions
across library interfaces may expose unsafe reordering,
and so performing inlining across the GNU C Library
interface is not recommended. The documented MT-Safety
status is not guaranteed under whole-program optimization.
However, functions defined in user-visible headers are
designed to be safe for inlining.
Other safety remarks
Additional keywords may be attached to functions, indicating
features that do not make a function unsafe to call, but that may
need to be taken into account in certain classes of programs:
locale Functions annotated with locale as an MT-Safety issue read
from the locale object without any form of
synchronization. Functions annotated with locale called
concurrently with locale changes may behave in ways that
do not correspond to any of the locales active during
their execution, but an unpredictable mix thereof.
We do not mark these functions as MT-Unsafe, however,
because functions that modify the locale object are marked
with const:locale and regarded as unsafe. Being unsafe,
the latter are not to be called when multiple threads are
running or asynchronous signals are enabled, and so the
locale can be considered effectively constant in these
contexts, which makes the former safe.
env Functions marked with env as an MT-Safety issue access the
environment with getenv(3) or similar, without any guards
to ensure safety in the presence of concurrent
modifications.
We do not mark these functions as MT-Unsafe, however,
because functions that modify the environment are all
marked with const:env and regarded as unsafe. Being
unsafe, the latter are not to be called when multiple
threads are running or asynchronous signals are enabled,
and so the environment can be considered effectively
constant in these contexts, which makes the former safe.As many things, the behaviour is surprising and can be overlooked. But undocumented is not.
Indeed, surprising and easy to overlook was all I was trying to convey.
People who try to invent the new new, and don't complete the task, blame unix, when unix just does what unix said it would do. It was the people who made bigger claims but did not deliver who should step up and say "I didn't turn the unix environment into what I thought I did."
but people trying to invent the new new are trying to invent it on unix because unix delivers what it promises. It doesn't deliver what you promise. But have some humility, and accept your failure and don't try to pin it on unix. Go implement on Windows.
Unix does allow you to deliver what you promise, that's why you'll still be complaining about unix 10 years from now, but it's up to you to deliver what you promise.
people who say "footgun" are people who try to shed responsibility; "it wasn't my fault, waaaaaah". I'm not saying we should not make computers easier to program, I'm saying that when we fail to: "it's a poor worker who blames his tools."
Why is it so important to you to blame this on unix, when you could blame it on a mistaken implementation of a library that could be fixed?