Preferences

The major takeaway from this is that Rust will be making environment setters unsafe in the next edition. With luck, this will filter down into crates that trigger these crashes (https://github.com/alexcrichton/openssl-probe/issues/30 filed upstream in the meantime).

But that won't actually fix the underlying problem, namely that getenv and setenv (or unsetenv, probably) cannot safely be called from different threads.

It seems like the only reliable way to fix this is to change these functions so that they exclusively acquire a mutex.

I have a different perspective: the underlying problem is calling setenv(). As far as I'm concerned, the environment is a read-only input parameter set on process creation like argv. It's not a mechanism for exchanging information within a process, as used here with SSL_CERT_FILE.

And remember that the exec* family of calls has a version with an envp argument, which is what should be used if a child process is to be started with a different environment — build a completely new structure, don't touch the existing one. Same for posix_spawn.

And, lastly, compatibility with ancient systems strikes again: the environment is also accessible through this:

   extern char **environ;
Which is, of course, best described as bullshit.
Indeed, environment variables should be used to configure child processes, not to configure the current process, for non-shell programs, IMHO.

Note that Java, and the JVM, doesn't allow changing environment variables. It was the right choice, even if painful at times.

I think there's a narrow window, at least in some programming languages, when environment variables can be set at the start of a process. But since it's global shared state, it needs to be write (0,1) and read many. No libraries should set them. No frameworks should set them, only application authors and it should be dead obvious to the entire team what the last responsible moment is to write an environment variable.

I am fairly certain that somewhere inside the polyhedron that satisfies those constraints, is a large subset that could be statically analyzed and proven sound. But I'm less certain if Rust could express it cleanly.

Your process can be started in a paused state by a debugger, have new libraries and threads injected into it, and then resumed before a single instruction of your own binary has been executed... and debuggers are far from the only thing that will inject code into your processes. If you're willing to handwave that, pre-main constructors, etc. away, you can write something like this easily enough:

    struct BeforeEnvFreeze(());
    struct AfterEnvFreeze(());

    impl BeforeEnvFreeze {
        pub fn new() -> Self { /* singleton check using a static AtomicBool or something */ Self(()) }
        pub fn freeze(self) -> AfterEnvFreeze { AfterEnvFreeze(()) }
        pub fn set_env(&self, ...) { ... }
    }

    impl AfterEnvFreeze {
        pub fn spawn_thread(&self, ...) { ... }
    }

    fn main() {
        let a = BeforeEnvFreeze::new();
        a.set_env(...);
        a.set_env(...);
        //b.spawn_thread(...); // not available

        let b = a.freeze(); // consumes `a`

        b.spawn_thread(...);
        //a.set_env(...); // not available
    }
Exercises left to the reader:

• Banning access to the relevant bits of Rust's stdlib, libc, etc. as a means of escaping this "safe" abstraction

• Conning your lead developer into accepting your handwave

• Setting up the appropriate VCS alerts so you have a chance to NAK "helpful" "utility" pull requests that undermine your "protections"

And of course, this all remains a hackaround for POSIX design flaws - your engineering time might be better spent ensuring or enforcing your libc is "fixed" via intentional memory leaks per e.g. https://github.com/bminor/glibc/commit/7a61e7f557a97ab597d6f... , which may ≈fix more than your Rust programs.

I agree that libraries certainly should not. But why would writing be the right choice ever, even for applications? Doesn't it make far more sense to use env to create in some better-typed global configuration object, filling any gaps with defaults, then use that?

I'd go further and say env should always be read-only and libraries should never even read env vars.

> I think there's a narrow window, at least in some programming languages, when environment variables can be set at the start of a process.

I mean, based on this issue I would say the only safe time is "at the start of the program, before any new threads may have been created".

But again, as others have said, there's no good reason I'm aware of to set environment variables in your own process, and when you spawn a new process you can give it its own environment with any changes you want.

Which programming languages?

When using C++ I wanted programs to have a function that was called before main() and set up things that got sealed afterwards, like parsing command-line-arguments, the environment variables, loading runtime libraries, and maybe look at the local directory, but I'm not sure if it'll be a useful and meaningful distinction unless you restructure way too many things.

I remember that on the Fuchsia kernel programs needed to drop capabilities at some point, but the shift needed might be a hard sell given things already "work fine".

>Note that Java, and the JVM, doesn't allow changing environment variables. It was the right choice, even if painful at times.

Not sure why would it be considered painful. Imo, use of setenv to modify your own variable, the definition of setenv is thread unsafe. So unless running a single threaded application it'd never make sense to call it.

Java does support running child processes with a designated env space (ProcessBuilder.environment is a modifiable map, copied from the current process), so inability to modify its own doesn't matter.

Personally I have never needed to change env variables. I consider them the same as the command line parameters.

Java doesn't even allow to change the working directory also due to potential multi-threading problems.

Another reason why Java isn't the greatest language to create CLI tools with.

> Java doesn't even allow to change the working directory also due to potential multi-threading problems.

Linux and macOS both support per-thread working directory, although sadly through incompatible APIs.

Also, AFAIK, the Linux API can't restore the link between the process CWD and thread CWD once broken – you can change your thread's CWD back to the process CWD, but that thread won't pick up any future changes to the process CWD. By contrast, macOS has an API call to restore that link.

It is interesting that they do not allow ability to change env and working dir via security policy or a command line arg (--allow-setenv, etc.).
That would be so much wasted engineering effort. The actual solution is simple: read what you need from env, and pass it as parameters to the functions you want to. The values of what you have read can be changed... and if you really, really want start a child process with a modified env.
Sure is painful (mostly when writing tests where the environment variables aren't abstracted in some way).

But I think it was actually possible to hack around up until Java 17.

if you really wish - you can change the bootstrap path and allow changing env() for whatever reason you want to (likely via copy on write). If you don't wish to do that feel free to spawn a child process with whatever env you desire, then redirect/join sys in/our/err (0/1/2)

Those are trivial things in around 100 lines of code and have been available since System.getenv() got back (it used to be deprecated and non-functional prior Java 1.5 or 2004)

A lot of the Java I'm writing is in AWS Lambda so my options are a bit more limited.
You can’t convince me that there is EVER a reason to call setenv() after program init as part of a regular program, outside needing to hack around something specific.

Environmental variables are not a replacement for your config. It’s not a place to store your variables.

Even if the env var API is fully concurrent, it is not convention to write code that expects an env var to change. There isn’t even a mechanism for it. You’d have to write something to poll for changes and that should feel wrong.

> You can’t convince me that there is EVER a reason to call setenv() after program init as part of a regular program, outside needing to hack around something specific.

The most common use I see for this is people setting an env in the current process before forking off a separate process; presumably because they don't realize that you can pass a new environment to new processes.

I wonder what bugs you'd find if you injected a library to override setenv() with a crash or error message into various programs. Might be a way to track down these kind of random irreproducable bugs.

Given how old most UNIX APIs are, and that when I do man fork I get information to look into execve(), which provices the feature, I guess not knowing is a typical case from google-copy-paste programming.
As a really old school UNIX guy I'd agree with this. Programmatic manipulation of the environment is an 'attractive nuisance' in that I feel anything you might be trying to achieve by using the environment as a string scratch pad of things that are different for different threads, can be coded in a much safer way.

I'd be happy to have you copy the immutable read-only environment vector of strings into your space and then treat that as the source of such things.

I think it would be interesting to build all the packages with a stdlib that dumps core on any call to setenv() or unsetenv(). That would give one an idea of the scope of the problem.

Environment variables are a gigantic, decades-old hack that nobody should be using... but instead everyone has rejected file-based configuration management and everyone is abusing environment variables to inject config into "immutable" docker containers...
> instead everyone has rejected file-based configuration management

With good reason. Files are surprisingly hard: https://danluu.com/deconstruct-files/

Rejecting one hard problem and replacing it with another method that is officially documented to be worse isn't really a solution.

Note the standard:

https://pubs.opengroup.org/onlinepubs/009604499/functions/se...

> The setenv() function need not be reentrant. A function that is not required to be reentrant is not required to be thread-safe.

With the increased use of PIE, thunks for both security and due to ARM + the difference between glibc and musl, plus busybox and you have a huge mess.

I would encourage you to play around with ghidra, just to see what return oriented programming and ARM limits does.

Compilers have been good at hiding those changes from us, but the non-reentrant nature will cause you issues even without threads.

Hint, these thunks can get inserted in the MI lowering stage or in the linker.

But setenv() is owned by posix, with only getenv() being differed to cppr.

Perhaps someone could submit a proposal on how to make it reentrant to the Open Group. But it wasn't really intended for maintaining mutable state so it may be a hard sell.

> replacing it with another method that is officially documented to be worse isn't really a solution

Agreed, 150%. My comment had more to do with rejecting files than it did with embracing environ as a suitable alternative.

SQLite is likely the most trouble-free option at the moment.

With that being said, it would be nice to see Android's sys/system_properties.h ported to GNU/Linux proper and, from there, other Unixen.

> I would encourage you to play around with ghidra, just to see what return oriented programming and ARM limits does.

Having worked professionally in reverse engineering at DoD, I can assure you that this is something I'm intimately familiar with.

Files and environ are bad.

That applies mostly to databases using the filesystem.

For configuration files, the write-fsync-move strategy works fine. Generally you don't need fsync, since most people don't use the file system settings that allow data writes to be reordered with the metadata rename.

We use env vars on cloud machines to hold various metadata information about the machines. They can be queried by any program and is extremely useful. It's too useful to be considered a hack. People just misuse them.
It's funny how any hack, no matter how big, somehow becomes a commonplace everyday "solution" once it's needed to work around some quirk of whatever technology is fashionable at the time.
Everything already supports environment variables, and everyone and their dog have their own favorite yaml-based configuration management.
That (managing the env "from the outside) is and always has been the "supposed" way of using it.

Modifying _your own_ environment _at runtime_ is not. The corresponding functions - setenv/getenv - and state - envp/environ - have in the UNIX standards "always" (since threads exist, really) been marked non-MT. "way back when" people were happy to accept that stated restrictions on use don't make bugs. Today, general sense of overentitlement makes (some) people say "but since whatever-trickery can remove this restriction... you're wrong and I'm entitled to my bugfix". I agree the damage is done, though.

> As far as I'm concerned, the environment is a read-only input parameter set on process creation like argv.

This holds for a lot of programs, but what if you're writing a shell?

Even then, you could maintain a separate copy of the environment that you control and freely mutate. Basically, during startup, you create a copy of the env you received. Any setenv primitive you expose to users will modify this copy (that you can sync properly yourself). When you want to launch a process, you explicitly provide the internal copy of the env to that process, you don't rely on libc providing its own copy.

Of course, this means you won't see any changes to env vars from libraries you may use that call setenv(), but you also shouldn't need, or want, that in a shell.

I still think having a proper synchronous thread safe setenv()/getenv() in libc is the better choice.

It doesn't look like there's any incentive to change it, e.g. getenv_r is an unpopular function.
If you're writing a shell, you can spend the 15 minutes to write a custom mutable data structure for your envvars; no need to significantly worsen the entire ecosystem to reduce the size of shells by a couple dozen lines (or, rather, move those lines into libc..)
I’ve written a lot of subprocess runners and environmental variables passed to a sub-process is just data at that point and you store it in your own variable like you would store someone’s name or someone’s age.
The underlying problem isn't just setenv, because the string returned by getenv can be invalidated by another call to getenv. ISO C says:

"The getenv function returns a pointer to a string associated with the matched list member. The string pointed to shall not be modified by the program, but can be overwritten by a subsequent call to the getenv function."

In a single threaded virtual machine, you can immediately duplicate the string returned by getenv and stop using it, right there.

Under threads, getenv is not required to be safe.

I think that with some care, it may be; an environment implementation could guarantee that a non-mutating operation like getenv doesn't invalidate any previously returned strings.

I think POSIX does that. It allows getenv to reallocate the environ array, but not the strings themselves:

"Applications can change the entire environment in a single operation by assigning the environ variable to point to an array of character pointers to the new environment strings. After assigning a new value to environ, applications should not rely on the new environment strings remaining part of the environment, as a call to getenv(), secure_getenv(), [XSI] [Option Start] putenv(), [Option End] setenv(), unsetenv(), or any function that is dependent on an environment variable may, on noticing that environ has changed, copy the environment strings to a new array and assign environ to point to it."

environ is documented together with the exec family of functions; that's where this is found.

So whereas there are things not to like about environ, it can be the basis for thread safety of getenv in an application that doesn't mutate the environment.

> As far as I'm concerned, the environment is a read-only input parameter set on process creation like argv.

Mutating argv is actually quite popular, or at least it used to be.

Mutating argv is fine for how it is usually done. That is, to permute the arguments in a getopt() call so that all nonoptions are at the end.

It is fine because it is usually done during the initialization phase, before starting any other thread. setenv() can be used here too, though I prefer to avoid doing that in any case. I also prefer not to touch argv, but since that's how GNU getopt() works, I just go with it.

Once the program is running and has started its threads, I consider setenv() is a big no no. The Rust documentation agrees with me: "In multi-threaded programs on other operating systems, the only safe option is to not use set_var or remove_var at all.". Note: here, "other operating systems" means "not Windows".

A big reason to mutate argv is to change the process's name for tools like top.
Yes, and if there were "setargv()" or "getargv()" functions, they'd have the same issues ;) … but argv is a function parameter to main()¹, and only that.

¹ or technically whatever your ELF entry point is, _start in crt0 or your poison of choice.

> but argv is a function parameter to main()¹, and only that.

> ¹ or technically whatever your ELF entry point is, _start in crt0 or your poison of choice.

Once you include the footnote, at least on linux/macos (not sure about Windows), you could take the same perspective with regards to envp and the auxiliary array. It's libc that decided to store a pointer to these before calling your `main`, not the abi. At the time of the ELF entry point these are all effectively stack local variables.

On Linux, a privileged process can change the memory address which the kernel (/proc filesystem) reads argv/etc from... prctl(PR_SET_MM) with the PR_SET_MM_ARG_START/PR_SET_MM_ARG_END arguments. Likewise, with PR_SET_MM_ENV_START/PR_SET_MM_ENV_END.

The API is ugly, and since it needs CAP_SYS_RESOURCE many programs can't use it... but systemd does: https://github.com/systemd/systemd/blob/2635b5dc4a96157c2575...

This shouldn't cause the kind of race conditions we are talking about here, since it isn't changing a single arg, it is changing the whole argv all at once. However, the fact that PR_SET_MM_ARG_START/PR_SET_MM_ARG_END are two separate prctl syscalls potentially introduces a different race condition. If Linux would only provide a prctl to set both at once, that would fix that. The reason it was done this way, is the API was originally designed for checkpoint-restore, in which case the process will be effectively suspended while these calls are made.

No amount of locking can make the getenv API thread-safe, because it returns a pointer which gets invalidated by setenv, but lacks a way to release ownership over it and unblock setenv safely (or to free a returned copy).

So setenv's existence makes getenv inherently unsafe unless you can ensure the entire application is at a safe point to use them.

This is actually not that hard to fix.

Getenv() could keep several copies of the value around: one internal copy protected by a mutex, that it never returns, and one copy per thread that it stores in thread local storage. When you call getenv(), it locks the mutex, checks if the current thread's value exists, populates it from the internal copy if not, and returns it. It will also install a new setenv-specific signal handler on this thread and store info about this thread having a copy.

Setenv() will then take the same mutex as getenv(), check if the internal copy is different from the new value; if it is, it will modify the internal copy, modify the local thread's copy if that has one, and then signal each other thread in the process that has a copy in TLS. The setenv signal handler will modify the local copy that thread holds.

It's gonna be slow for a large multi-threaded program, but since setenv() used to corrupt memory for such programs, they probably don't care. And for single-threaded programs, or even for programs that don't access getenv()/setenv() on multiple threads, there should be no extra overhead other than the mutex and the bookkeeping.

The only issues that would remain are programs which send the pointer they get from getenv() to other threads without ensuring locking access, and programs which rely on modifying the pointer from getenv() directly as a way to set an env var, and expect this to be visible across threads. Those are just hopelessly broken and can't use the same API - but aren't more broken then they are today.

Of course, in addition to this complex work to make the old API (mostly) thread safe, it should also offer a new API that simply returns a copy every time, doesn't promise to show modifications to your copy when setenv() gets called (you need to call getenv() again), and puts the onus on you to free that copy explicitly.

> it should also offer a new API that simply returns a copy every time

Returning a copy isn't great (memory allocation!), the API should probably be something like:

    int getenv(const char *varName, char *buf, size_t bufSize, size_t *varSize);
Where the caller manages the buffer and getenv writes into it (so it can e.g. be stack or statically allocated), the third argument is the size of the caller-managed buffer, then the last variable is an "out parameter" that returns the "true" length of the environment variable. Then afterwards, you can check if `*varSize > bufSize`, and if so, you need to make your buffer larger. The return value is an error code.

Doing it like this, you can easily implement the "return a malloced copy" if you want to, but it also gives you the option to avoid allocation entirely. This is important for e.g. embedded or real-time applications, or anything that just likes to avoid `malloc()/free()`.

If you only consider `getenv`/`setenv` there are indeed many solutions, but it's not that simple. You also need to consider `putenv` (not that nasty, you just need to treat it like initial environment, which means you can't use a single range check) and accessing the `environ` variable directly (nasty).

Your particular solution doesn't work because people expect `getenv` to be async-signal-safe, which means you shouldn't be allocating memory.

Hmm ... doing an incref-like operation during `getenv` for a previously `setenv`ed variable that hasn't yet been accessed in this thread would be fine ... clear those refs during calls we know indicate knowledge refreshes ...

>`putenv` (not that nasty,

It's equally nasty. POSIX requires that the argument to `putenv()' not be copied, so it's not very different from assigning to `environ' directly.

> accessing the `environ` variable directly (nasty).

"easy": protect the page containing environ and handle the mutation from the signal handler.

/s of course.

"mutating" there involves the need to (re)allocate memory. To do so in a signal handler is hard ... because memory allocators are, while threadsafe, not async-signal-safe. You can't make a hard problem easy by asserting dependence on another (unsolved) hard problem.

Btw, you can _also_ substitute libc's setenv/getenv/putenv with your own (locking) implementations, courtesy preload and all the funky features of ELF symbol resolution. Actually easy. But impossible if you link against static code using it (go ... away). Hmm. easy ? impossible ? damn this grey world. Gimme some color.

Someone above mentioned getenv_r(). I needed to Google about it. It is not impl'd by GNU GLibC (that I know). I do see it on NetBSD: https://man.netbsd.org/getenv_r.3

It looks useful.

There has to be some sort of nuance regarding why this seemingly simple fix hasn't been made yet. Changing from crashing to blocking doesn't seem like a big breaking change.
Because it doesn't actually solve anything: You're still replacing whatever getenv returned from under the nose the program code - if that happens in another thread or in a signal handler in the same thread doesn't make any difference.

And that's before you even get to the `extern char *environ` global.

B/c you never need setenv outside a single threaded command line utilities, and even then it's questionable.
According to ISO C, getenv returns a pointer to storage that can be overwritten by another call to getenv! Only POSIX slightly fixes it: the string comes from the environ array, and operations on environ by the library preserve the strings themselves (when not replacing or deleting them), just not the array. A program that calls nothing but getenv is okay on POSIX, not necessarily on ISO C.
C could provide functions to lock/unlock a mutex and require that any attempt to access the environment has to be done holding the mutex. This would still leave the correctness in the hands of the user, but at least it would provide a standard API to secure the environment in a multi threaded application that library and application developers could adopt.
That is basically "what it means" if an interface is non-MT: you can call this no-problem if you know you're singlethreaded, and if you're not, find your own way to serialize (meaning: have your own locking prinitive you acquire/release where you make calls to these functions).

One could "dream of" a func that tells libc "acquire/drop this mutex of mine around get/set/putenv calls" but that'd simply move the problem - because the nifty "frameworks" would do that (independently of each other, we're sovereign and entitled frameworks around here) and race each other's state nonetheless.

> because the nifty "frameworks"

Malicious software exists, does that mean we should remove all threading primitives from the standard?

Obviously not, but _threading_ primitives are not the subject of this post at all. Declared-as Non-threadsafe interfaces are. And of course one (as is happening here) one can argue whether all "system runtimes" shall be threadsafe. Right now though, they are not, and agreed/sanctioned standards don't require them to be. Again (also as happening here) opinions may differ whether changes-to-make-threadsafe would be bugfixes, enhancements, or (require) new interfaces. I have expressed my views on this. Happy to agree to disagree, though.
They can have copy on write of course.
Is that a problem? I feel like calling getenv and setenv from different threads is a design antipattern anyway. Any environment setting and loading should happen in the one and only main thread right after process init.
The underlying problem is that setenv is mutable global state and should never have existed
The process's current directory is mutable global state as well, and yet chdir(2) is thread-safe.
It's threadsafe in the memory sense. It's not threadsafe in the having an idea what files you are accessing sense.
The latter is always true even when you don't use chdir(2) and/or always use absolute file paths since, you know, there are other processes that can re-arrange the file system whatsoever way the like. The file system is one example of the unavoidable global mutable shared state (another example is network) which one simply has to deal with.
If files end up in a different directory because the user rearranged the filesystem under your nose, that's on the user. Most applications would deal with that by telling the user not to do that.

If your sensitive logs end up in the webserver root because one thread used chdir to temporarily change the working directory it's on the application writer.

Or to put it another way, the filesystem as a whole being shared mutable state does not make the current working directory being shared mutable state between threads any less of an issue.

chdir is thread-safe, but interacting with the current directory in any context other than parsing command-line arguments is still nearly always a mistake. Everything past a program's entry point should be working exclusively in absolute paths.
Yeah if you chdir() in a multithreaded program, all cwd-relative file accesses in other threads are fucked.

As well as absolute paths, it’s ok to work with descriptor-relative paths using openat() and friends.

chdir is only thread safe to the extent that corruption won't occur.

If one thread is using relative paths, and another is doing a chdir-based traversal (as using the nftw function, for instance), that first thread's accesses are messed up.

This is why POSIX now has various -at functions; the provide stable relative access.

The current working directory is kernel state. getcwd() is a system call. This doesn't compare.
Welcome to the C standard library, the application of mutable global state to literally everything in it has to be the most consistent and predictable feature of the language standard.
I used to think this was bad too. But when C was designed an entire single threaded program was considered the unit of encapsulation for functionality. Now it’s mostly libraries.

The former allows you to design a coherent system. a lot of design questions which are annoying (“how do I access config data consistently, etc) become very clear.

It also makes C more productive. If global vars and static locals are unbanned, features like closures become less important.

I mean, I'm sure it was an okay solution on PDP-11.
I mean, it's only threadsafe in the sense that opening a file in cwd without being able to actually know what cwd is is "safe"
The mutex would have to be held by the caller until it no longer needs the string returned from the environment, or makes a copy:

   stdenvlock();    // imaginary function added to ISO C or POSIX
   char *home = getenv("HOME");
   char *home_copy = strdup(home);
   stdenvunlock();  // only here can we unlock
   // home pointer is now indeterminate
Other solutions:

1. Put the above sequence into a function, and don't expose the mutex. Thread-safe code must use:

   char *home = dupenv("HOME"); // imaginary function; caller responsible for freeing.

2. Provide environment lookup into a buffer:

   getenvbuf("HOME", mybuf, sizeof mybuf);  // returns some value that helps to resize the buffer

   
All functions that retain pointers out of the classic getenv remain unsafe.

A mutex can be provided to those applications that want to manipulate the environ array directly, or use getenv and setenv, or any combinations of these.

The main problem is all the code out there using getenv.

Please no.

If your program wants to use the environment as an out-of-band global var for cross thread communication, you can make your own mutex.

That will break if any code which is not aware of the mutex calls getenv, even for a variable not related to the communication.
Of course. It’s a bad idea. If you modify it the environemnt becomes a global var.
It's the same problem with global vars, but at a machine scope. The real solution here would be for the OS to have a better interface to read and write env vars, more like a file where you have to get rw permission (whether that's implemented as a mutex or what).
This is neither an OS nor a machine scope problem. The environment is provided by the OS at startup. What the process does with it from there on is its own concern.
> The environment is provided by the OS at startup.

That's part of the design of the OS. How the OS implements this is primitive, and so it leaves it up to every language to handle. The blog mentions the issue is with getenv, setenv, and realloc, all system calls. To me, that sounds like bad OS design is causing issues downstream with languages, leaving it up to individual programmers to deal with the fallout.

> getenv, setenv, and realloc, all system calls

None of these 3 functions is a system call. open(), mmap(), sbrk(), poll(), etc. are system calls. What you're referring to is C library API, which as Go has shown (both to its benefit and its detriment) is optional on almost all operating systems (a major exception being OpenBSD.)

If you really want to lose some sanity I would recommend reading the man page for getauxval(), and then look up how that works on the machine level when the process is started. Especially on some of the older architectures. (No liability accepted for any grey hair induced by this.)

ed.: https://lwn.net/Articles/631631/

Neither getenv, setenv nor realloc are system calls, they all are functions from C stdandard library, some parts of which for historical reasons are required to be almost impossible to use safely/reliably.
> It seems like the only reliable way to fix this is to change these functions so that they exclusively acquire a mutex.

A mutex can ensure thread safety but risks deadlocks if not used carefully and will hurt performance...

Agree about performance, but wouldn't there need to be >1 mutex to risk a deadlock?
Imagine you get a signal during getenv itself with the mutex held. Then your signal handler calls getenv. (On the other hand -- getenv is not marked async-signal-safe, so this use is already illegal.)
If it's not a "recursive mutex" (where you can call lock within the same thread on the same mutex more than once consecutively and it handled that), it's possible to lock on itself again (say in code which is recursive)...
The problem (with get/set/putenv as they are) was isn't the non-use of a mutex. It's the "meaning" of the pointer returned to by getenv(). It returns a char*. Nevermind the persistance of that value - you can work around that by deliberately leaking memory - but it's writeable. Whether it's a good idea to do so ... well. But simply locking "inside" these funcs doesn't solve all the / your issues.
setenv and getenv have never been thread safe, why the concern with it now?
The concern now is that, unlike when Posix was set in stone, threads exist.
The p in pthreads stands for Posix. I.e., uh, Posix is neither set in stone, nor entirely predates threads.
I am old enough to remeber when UNIX only had processes, and several thread designs were being discussed until eventually pthreads one design won.

POSIX predates adoption of threads in the UNIX world.

And when can we expect the version of Posix that fixes setenv to be MT-safe?
Is that the underlying problem, or is the underlying problem that libraries are using thread-unsafe setenv in threaded contexts when they could just do something else?
But it would force Rust programs to add their own synchronization mechanism around them. As long as no two threads can call getenv/setenv at the same time then it’s fine.
The problem isn't something that Rust can solve.

The Rust stdlib is already using synchronization on the versions of these functions that are exposed from the Rust stdlib. That's why those functions were allowed to be marked as safe in the first place.

The problem is that people are calling C code from Rust (which already requires an unsafe annotation), and then that C code is doing silly thread-unsafe shenanigans for regrettable historical reasons.

It's beyond Rust's power to fix without cooperation from the underlying C code, which happens to be provided by the OS, which is just being compliant with Posix. Rust can only do so much when the platform itself is hell-bent on sabotaging you.

Ah, that’s a detail that I either forgot or did not know. Thank you.

It certainly would be nice if the C library had fewer built–in footguns. And if we could write programs in other languages without ever depending on it (which wouldn’t but much use when you’re relying on a C library anyway, but it still would be nice).

In particular, it doesn't help if you call a c function that indirectly modifies the environment with FFI.
> Nowadays the best solution to this issue is "stop using this crate" with libraries like rustls.

Nice to see that the author of the library has a sensible take. Unfortunately the ecosystem does not: https://github.com/seanmonstar/reqwest/blob/master/Cargo.tom...

People get trained to ignore the ____UNSAFE_payattention__nevermindthatthisappears50timesinthisfile___ blocks and prefixes

This also shows up in web frameworks where Vue has the v-html directive and react has dangerouslySetInnerHTML. Vue definitely has it better.

In the React world, the only times I've seen dangerouslySetInnerHTML consistently used is for outputting string literal CSS content (and this one is increasingly rare as build tools need less handholding), string literal JSON content (for JSON+LD), and string literal premade scripts (i.e. pixel tags from the marketing content). That's not to say there's no danger surface there, but it's not broadly used as a tool outside of code that's either really bad or really exhaustively hand-tuned.
I've only really seen dangerouslySetInnerHTML used while transitioning from certain kinds of server side rendering to React. There is still lots of really old internal tools in ancient html out there.
Code syntax highlighting libraries for react use dangerouslySetInnerHTML.
React doesn't have a tag and attribute sanitizer built in, so having non-js-programmers edit JSX isn't especially safe anyways, as an img or a href could exfiltrate data. If it were they could just block out an innerHTML attribute. A js programmer can get around it by setting up a ref and then using the reference to set innerHTML without the word dangerously appearing.
> A js programmer can get around it by setting up a ref and then using the reference to set innerHTML without the word dangerously appearing.

If DOM nodes during the next render differ from what react-dom expects (i.e. the DOM nodes from the previous render), then react-dom may throw a DOMException. Mutating innerHTML via a ref may violate React's invariants, and the library correctly throws an error when programmers, browser extensions, etc. mutate the DOM such that a node's parent unexpectedly changes.

There are workarounds[1] to mutate DOM nodes managed by React and avoid DOMExceptions, but I haven't worked on a codebase where anything like this was necessary.

[1] https://github.com/facebook/react/issues/11538#issuecomment-...

The reference is used to operate on the subtree when wrapping libraries like CodeMirror https://github.com/uiwjs/react-codemirror/blob/master/core/s... React leaves it alone if the children doesn't change.

innerHTML is useful when there is a trusted HTML source, which is becoming more popular with stuff like HTMX and FastHTML.

This item has no comments currently.

Keyboard Shortcuts

Story Lists

j
Next story
k
Previous story
Shift+j
Last story
Shift+k
First story
o Enter
Go to story URL
c
Go to comments
u
Go to author

Navigation

Shift+t
Go to top stories
Shift+n
Go to new stories
Shift+b
Go to best stories
Shift+a
Go to Ask HN
Shift+s
Go to Show HN

Miscellaneous

?
Show this modal