7 ms·
In the Rust std, `set_var` and `remove_var` will correctly require using an `unsafe {}` block in the next edition (2024). The documentation does now mention the
by ChrisSD 2y ago
In the Rust std, `set_var` and `remove_var` will correctly require using an `unsafe {}` block in the next edition (2024). The documentation does now mention the safety issue but obviously it was a mistake to make these functions safe originally (albeit a mistake even higher level languages have made).
https://doc.rust-lang.org/stable/std/env/fn.set_var.html https://doc.rust-lang.org/stable/std/env/fn.set_var.html
There is a patch for glibc which makes `getenv` safe in more cases where the environment is modified but C still allows direct access to the environ so it can't be completely safe in the face of modification https://github.com/bminor/glibc/commit/7a61e7f557a97ab597d6fca5e2d1f13f65685c61 https://github.com/bminor/glibc/commit/7a61e7f557a97ab597d6f...
- Thaxll 2y agoWhy requiring unsafe when the std implementation could take care of the synchronisation?
- demurgos 2y agoIt can't ensure synchronization because any code using libc could bypass the sync wrapper. In particular, Rust lets you link C libs which wouldn't use the Rust stdlib.
- msully4321 2y agoBecause it can still race with C code using the standard library. getenv calls are common in C libraries; the call to getenv in this post was inside of strerror.
- ChrisSD 2y agoIt can only synchronize if everything using is Rust's functions. But that's not a given. People can use C libraries (especially libc) which won't be aware of Rust's locks. Or they could even use a high level runtime with its own locking but then they'll be distinct from Rust's locks. The only way to coordinate locking would be to do so in libc itself.
- wahern 2y agolibc does do locking, but it's insufficient. The semantics of getenv/setenv/putenv just aren't safe for multi-threaded mutation, period, because the addresses are exposed. It's not really even a C language issue; were you to design a thread-safe env API, for C or Rust, it would look much different, likely relying on string copying even on reads rather than passing strings by reference (reference counted immutable strings would work, too, but is probably too heavy handed), and definitely not exposing the environ array. The closest libc can get to MT safety is to never deallocate an environment string or an environ array. Solaris does this--if you continually add new variables with setenv it just leaks environ array memory, or if you continually overwrite a key it just leaks the old value. (IIRC, glibc is halfway there.) But even then it still requires the application to abstain from doing crazy stuff, like modifying the strings you get back from getenv. NetBSD tried adding safer interfaces, like getenv_r, but it's ultimately insufficient to meaningfully address the problem. The right answer for safe, portable programs is to not mutate the environment once you go multi-threaded, or even better just treat process environment as immutable once you enter your main loop or otherwise finish with initial process setup. glibc could (and maybe should) fully adopt the Solaris solution (currently, IIRC, glibc leaks env strings but not environ arrays), but if applications are using the environment variable table as a global, shared, mutable key-value store, then leaking memory probably isn't what they want, either. Either way, the best solution is to stop treating it as mutable.
- ChrisSD 2y agoA safe API would look a lot like Windows' GetEnvironmentVariable and SetEnvironmentVariable https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getenvironmentvariable https://learn.microsoft.com/en-us/windows/win32/api/winbase/... https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setenvironmentvariable https://learn.microsoft.com/en-us/windows/win32/api/winbase/...
- wahern 2y agoYep. GetEnvironmentStrings and FreeEnvironmentStrings are probably even more noteworthy as they seem to substitute for an exposed environ array, though they push more effort to the application.
- masklinn 2y agoBecause the std implementation can not force synchronisation on the libc, so any call into a C library which uses getenv will break... which is exactly what happened in TFA: `openssl-probe` called env::set_var on the Rust side, and the Python interpreter called getenv(3) directly.
- miohtama 2y agoIs it possible to skip libc completely or would this introduce too many portability concerns?
- jcotton42 2y agoIt's not just libc, it's any C or C++ library that calls getenv or setenv.
- rerdavies 2y agoSpecifically, any C or C++ library that calls setenv (despite documentation that says that setenv is not threadsafe).
- SAI_Peregrinus 2y agoOr any multithreaded program that uses a C or C++ library that calls setenv somewhere internally, and failed to document that it does so and is thus unsuitable for use by multithreaded programs. No library does that documentation, so you can't use libraries on POSIX systems if writing multithreaded code. Or you do and hope for the best. So everyone just hopes for the best.
- rerdavies 2y agoThe documentation for setenv: Caveats: POSIX.1 does not require setenv() or unsetenv() to be reentrant. ... Interface: setenv(), unsetenv() Attribute: Thread safety Value: MT-Unsafe const:env Libraries that are thread-safe DO provide that documentation. One assumes that libraries that don't provide that documentation are not thread=safe. GnuTLS docs: The GnuTLS library is thread safe by design, meaning that objects of the library such as TLS sessions, can be safely divided across threads as long as a single thread accesses a single object.
- fsckboy 2y agoyou've gotten a lot of answers which say the same thing, but which I don't think answer your question: synchronization methods impose various complexity and performance penalties, and single threaded applications which don't need that would pay those penalties and get no benefit. Unix was designed around a lightweight ethos that allowed simple combining of functions by the user on the command line. See "worse is better", but tl;dr that way of doing things proved better, and that's why you find yourself confronting what it doesn't do.
- sunshowers 2y agoWell it was better in the short term but is worse in the long term. In particular, the error handling situation is generally atrocious, which is fine for interactive/sysadmin use but much worse for serious production use.
- davidt84 2y agoThe real problem is that getenv() and setenv() were created before threads were really a thing.
- kazinator 2y agogetenv can easily be misused in a single threaded program.
- davidt84 2y agoBut it is possible to safely use it in a single threaded program. There's no way to use it safely in a multi threaded application that may use setenv (unless you add your own synchronisation, and ensure everything uses it, even third party libraries).
- kazinator 2y agoActually I don't believe that's the case. The getenv function as described by ISO C cannot be safely used in a program that only uses getenv, if that program uses ISO C threads, and more than one threat calls getenv without synchronizing with the others. I don't think POSIX fixes this: it doesn't specify that the environ array is protected against concurrent access. If two threads call getenv right around the same time, one of them could invalidate the environ array just as the other one has started to traverse it. If you want to be safe, copy the environment to a different data structure on program startup. Then have all your threads refer to that data structure.
- jrmg 2y agoWow, glibc now keep[s] older versions around and adopt[s] an exponential resizing policy. This results in an amortized constant space leak per active environment variable, but there already is such a leak for the variable itself (and that is even length-dependent, and includes no-longer used values). There have got to be pathalogical uses out there where this will cause unbounded memory growth in well-formed (according to the API) programs, no? Interesting to see this _introduce_ a ‘bug’ (unbounded memory growth) for these programs that follow the API in order to ‘fix’ programs that don’t (by using the API in multiple threads). Pragmatism over dogma I guess. Leaves me feeling a bit sketched out though.
- GoblinSlayer 2y agoFWIW you can make a singly linked list with infinite number of nodes too. Memory leaks happen in well formed programs just fine, glibc is just one of many examples.