Modifying a process’ environment is generally not thread-safe because it is memory shared between all threads of a process.
System functions like setenv or SetEnvironmentVariable are based on a global variable (environ on unix systems). When a thread updates the environment configuration, it may reallocate the memory. Thus, any other thread which had previously obtained the pointer before the update and is now reading from the old memory, may be left with corrupt data.
We could offer some mitigation by synchronizing access through Crystal’s ENV interface.
However, that would still not be 100% safe because it cannot account for direct access via the system APIs.
But maybe there’s a simpler solution: Disallow environment modifications, i.e. deprecate ENV.[]=.
This idea is based on this hypothesis:
Unless your process is a shell or adjacent, it shouldn’t need to mutate its environment.
We’d need to look a bit more closely at specific use cases, but I think this might be a valid option.
It should be feasible to keep the functionality of ENV.[]= available in a portable interface, but with an explicit warning that it’s potentially unsafe (e.g. ENV.unsafe_set).
Safe usage could only be guaranteed in a single-threaded process. It should be safe to set environment variables at the start of a multi-threaded process, before it spawns any other threads.
A common usage pattern is when you need to adjust the environment for a piece of code that you cannot configure directly but depends on environment variables.
This is primarily relevant for testing code that is supposed to react to environment settings. For example, we test several features in the standard library and compiler using the with_env helper.
This functionality seems reasonable and relevant.
But instead of modifying the process’ environment, we could mock it using a fiber-local overlay in the ENV interface.
In some cases, it might even be sufficient to just pass in configuration as explicit arguments.
I’m not aware of any specific other use cases. But I could expect instances where you need to modify the environment for calling a library methods which are not using Crystal’s ENV interface and thus need to read from the actual process environment.
That generally smells like bad design, but there might be some use cases.
I’m posting this thread to ask for feedback and examples.