More generically covered by the rule “don’t do anything to change global state in multi-threaded code.
From the end of the post:
None of this is new, but we do re-discover it roughly every five years.
I imagine this is a good example where pledge would be useful? Fork threads and then pledge not to call getenv again?
Unlikely, since getenv()/setenv() aren’t system calls (so the kernel would have no real way of enforcing such a promise).
What does Erlang do about this internally? It must surely use getenv/setenv somewhere in library code.
I could very easily see Erlang simply having one process who’s job it is to manage access to getenv and setenv. I suspect they actually use some kind of locking scheme, but I don’t know enough BEAM to be certain.
Does anyone know a good way to navigate the Erlang source code? I’m a little spoiled by all the code navigation support that golang.org has, and wonder if Erlang has anything similar
The same question for Go. I use setenv() a lot in test code to do various things and have never encountered this behavior. I’m not interacting, though, with a library that uses libc when doing so, at least not in cases I can think of. Curious enough to look into this later.
Also interesting is that environment variables are passed directly to calls to Exec. Does Exec in libc also take an env parameter?
man 2 execve
So, at least in Go, the first call to os.Getenv, or os.Setenv copies the environment into a map. For os.Setenv, there’s an additional call to setenv_c, which I didn’t successfully fund the definition of (just on my phone). Also, it appears that a setenv in another (external) library won’t actually show up in a subsequent os.Getenv call, if the environment was already copied.
Setenv in golang uses a Read-Write mutex, which is how a lot of other racy go constructs (like their regular expressions) add thread safety. (Which is why I think Erlang does something similar). Interestingly enough, golang has its wrapper around setenv/getenv in it’s syscall package.
I noticed that before, but missed this:
// setenv_c and unsetenv_c are provided by the runtime but are no-ops
// if cgo isn't loaded.
func setenv_c(k, v string)
func unsetenv_c(k string)
So, they basically copy the environment on first interaction with it, use a Lock, and don’t try to do anything fancy unless you’re using cgo. With cgo, I bet you end up with the same problems as described in the blog post… something to test out in my “copious” amounts of free time.