Identifying dependencies used via dlopen()
Did you know...? LWN.net is a subscriber-supported publication; we rely on subscribers to keep the entire operation going. Please help out by buying a subscription and keeping LWN on the net. |
The recent XZ backdoor has sparked a lot of discussion about how the open-source community links and packages software. One possible security improvement being discussed is changing how projects like systemd link to dynamic libraries that are only used for optional functionality: using dlopen() to load those libraries only when required. This could shrink the attack surface exposed by dependencies, but the approach is not without downsides — most prominently, it makes discovering which dynamic libraries a program depends on harder. On April 11, Lennart Poettering proposed one way to eliminate that problem in a systemd RFC on GitHub.
The systemd project had actually already been moving away from directly linking
optional dependencies — but not for security reasons. In Poettering's
explanation of his proposal on Mastodon he noted: "The primary reason for
[using dlopen()] was to make it easier to build small disk images
without optional components, in particular for the purpose of initrds or
container deployments.
" Some people have
speculated that this change is what
pushed "Jia Tan" to launch their attack at the beginning of April, instead of
waiting until it was more robust.
There are several problems with using dlopen() for dependencies,
however. One is that, unlike normal dynamic linking, using dlopen()
exposes the functions provided by the dependency as void pointers, which must be
cast to the correct type. If the type in the dependency does not match the type
in the dependent program, this can open a potential avenue for
type-confusion attacks. Several respondants to
Poettering's explanation on Mastodon worried that
promoting the use of dlopen() would be a detriment to security for this
reason. James Henstridge
said:
"I imagine you could hide some interesting bugs via not-quite-compatible function
signatures (e.g. cause an argument to be truncated at 32 bits).
"
Poettering
replied:
In current systemd git we systematically use some typeof() macro magic that ensures we always cast the func ptrs returned by dlopen() to the actual prototype listed in the headers of the library in question. Thus we should get the exact same type safety guarantees as you'd get when doing regular dynamic lib linking. Took us a bit to come up with the idea that typeof() can be used for this, but it's amazing, as we don't have to repeat other libraries' prototypes in our code at all anymore.
Henstridge
agreed after looking at
the code that it was
"quite elegant. It also neatly solves the problem of
assigning a symbol to the wrong function pointer.
"
Not all of the problems are so easily dismissed, however. The real problem,
according to Poettering's
announcement is the fact that using dlopen() removes information
from the program's ELF headers about what its dependencies are.
Now, I think there are many pros of this approach, but there are cons too. I personally think the pros by far outweigh the cons, but the cons *do* exist. The most prominent one is that turning shared library dependencies into dlopen() dependencies somewhat hides them from the user's and tools view, as the aforementioned tools won't show them anymore. Tools that care about this information are package managers such as rpm/dpkg (which like to generate automatic package dependencies based on ELF dependencies), as well initrd generators such as dracut.
His proposed solution is to adopt a new convention for explicitly listing optional dependencies as part of the program itself. In the systemd RFC, he gave an example of a macro that could be used to embed the name of optional dependencies in a special section of the binary called ".note.uapi.dlopen". "UAPI" stands for Userspace API — referring to the Linux Userspace API Group, a relatively recent collaboration between distributions, package managers, and large software projects to define standardized interfaces for user-space software. The initial proposal for what to encode in the note section was fairly bare-bones — just a type field, the string "uapi" denoting the ELF section "vendor", and the name of the dependency in question.
Poettering was also clear that it wouldn't be useful to implement this for systemd on its own; the note would only be useful if other tooling decided to read it, and other projects choose to implement it. Mike Yuan was quick to comment positively about the possibility of adding support to mkinitcpio, Arch Linux's initramfs generation tool, and pacman, the Arch package manager.
Luca Boccassi
agreed that he could "look into adding a debhelper addon for
this
", but wondered if there should be some way to
indicate whether a dependency is truly optional or that the program will
fail if the dependency is missing.
Poettering
responded: "If it is a hard
dep, then it should not be a dlopen() one. The whole reason for using dlopen()
over regular ELF shared library deps is after all that they can be weak
",
although he did point out that the type field means that "the door is open to
extend this later.
"
Antonio Álvarez Feijoo
raised another concern, pointing out: "Some people are very picky about
the size of the initrd and don't like to include things that aren't really
necessary. [...] So yes, it's great to know which libraries are necessary, but
how to know what systemd component requires them?
"
Boccassi
replied that this was an example of a situation where information on
whether a dependency is required or recommended could be useful. Poettering
disagreed, asserting that "which libraries to actually include in an
initrd is up to local configuration or distro policy.
" Ultimately, consumers
of the new note section can do whatever they would like with the information,
including automatically generating dependencies, or merely using them as a
"linter
" to complain about new weak dependencies that are not already
known.
I think all such approaches are better than the status quo though: we'll add a weak dep or turn a regular dep into a weak dep, and unless downstream actually read NEWS closely (which well, they aren't necessarily good at) they'll just rebuild their packages/initrd and now everything is hosed.
This appealed to Feijoo, who agreed that using the information as a sanity-check on package definitions made sense.
Carlos O'Donell asked whether Poettering cared about exposing the specific symbols and symbol versions that systemd uses, pointing out that existing ELF headers include this information. He asserted that RPM uses this information when packaging a program. Poettering said that was a good question, but replied:
To speak for the systemd usecase: even though we dlopen() quite a number of libraries these days (21 actually), and we actually do provide symbol versioning for our own libraries, we do not bother with listing symbol versions for the stuff we dlopen(). We use plain dlsym() for all of them, not dlvsym().
He went on to point out that requiring people to pin down symbol versions would
be "a major additional ask
".
Poettering did seem to think that there was some benefit to integrating this new
proposal into the existing implementation of dynamic linking in the GNU C
library (glibc). He
asked O'Donell and Florian Weimer — who are both involved in the glibc project —
"should we proceed with this being some independent spec somewhere that just
says '.note.uapi.dlopen contains this data, use it or don't, bla bla bla'. Or
did the concept of weak leaking interest you enough that you think this should
be done natively in glibc, binutils and so on?
"
Some other operating systems — notably macOS — have a native concept of "weak
linking" for optional dependencies, so the idea of incorporating this
information into the build system and standard library are not new.
Zbigniew Jędrzejewski-Szmek brought up
an additional question about the formatting of the new section, asking
whether it would make sense to use "a compact JSON representation
".
Jędrzejewski-Szmek said that this could make it easy to add a human-meaningful
description of what the dependency is used for. With that addition, "it
should be fairly easy to integrate this in the rpm build system.
"
Boccassi
agreed that the payload should be JSON.
Poettering
replied: "I have nothing against using JSON for this, but it's key we can
reasonably generate this from a simple no-deps C macro I'd say.
"
Ultimately, the idea of having a standard encoding for optional dependencies seems to have been well-received, with several package managers potentially interested in adding support. With discussion still ongoing and the final format of the added information up in the air, however, it's too soon to say exactly what form the information will take. Anything intended to help ameliorate the pain of removing traditional dynamically linked dependencies seems like a good idea, though, since they reduce the surface open to XZ-backdoor-like attacks.
(Log in to post comments)
Identifying dependencies used via dlopen()
Posted Apr 16, 2024 21:16 UTC (Tue) by andresfreund (subscriber, #69562) [Link]
It seems the medium to long term solution ought to be to properly support "lazily loaded dependencies" at the ELF level. Implementing this via dlopen() in various places doesn't scale all that well and makes it harder to centrally improve performance and security. The dlopen() approach pretty much requires indirect function calls via modifiable pointers, which isn't great. And as the article points out, it makes stuff like symbol versioning much more onerous.
With proper support by the dynamic linker, each lazy-loaded library could get a distinct "range" in an SO's GOT that could separately get remapped. That still would increase exposure over the non-lazy -z now -z relro binaries, but not to the degree that dlopen() ends up doing (and we just have seen -z now has its own risks).
Greetings,
Andres
Identifying dependencies used via dlopen()
Posted Apr 16, 2024 22:06 UTC (Tue) by ibukanov (subscriber, #3942) [Link]
Identifying dependencies used via dlopen()
Posted Apr 22, 2024 15:57 UTC (Mon) by ScottMinster (subscriber, #67541) [Link]
Identifying dependencies used via dlopen()
Posted Apr 22, 2024 16:53 UTC (Mon) by rschroev (subscriber, #4164) [Link]
Identifying dependencies used via dlopen()
Posted Apr 23, 2024 21:10 UTC (Tue) by ScottMinster (subscriber, #67541) [Link]
As a further detail, the code is C++, so the initialization is actually being done through static initializers in the library (constructors on static objects). The registration method is a constructor on a base class that records the pointer to itself (this) in a list of pointers. Later, when asked to exploit the loaded functionality, it can call a pure virtual method (implemented in the derived class in the plugin library) to do the work.
If delaying static initializers until something in the loaded library was explicitly called, I would probably have to change the logic to make some sort of dummy call into each plugin library loaded. That is a bit annoying, though on reflection, if I set it up right, it would provide some kind of check that the library that was loaded really was meant to be loaded. That it really was a plug in to this software and not some mistake. Presumably, if the right function couldn't be found with dlsym(), then the library could be safely unloaded and ignored.
So delaying is an interesting idea, but would probably cause too many problems before software was fixed to expect it. But maybe it could be an opt-in flag to dlpen()?
Identifying dependencies used via dlopen()
Posted Apr 24, 2024 9:29 UTC (Wed) by farnz (subscriber, #17727) [Link]
The code doing the loading can call a void * plugin_init(void) function, which does whatever your deferred dynamic initializers do today; call back to the opener to register a file format reader, a new renderer, a writer, whatever else they currently do at the moment they're loaded. At exit, it can call a void plugin_destroy(void * init_return) function, which does the destruction before the plugin is unloaded.
There's nothing that can be done via code running at load time that can't also be done via calling a function inside the library; the reason most psABIs run code at load time, rather than simply expecting that the start point runs constructors etc, is to make it easy to implement C++ deferred dynamic initialization.
If I were interested in playing with psABIs, with a view to producing an alternative to current ELF mechanisms, I'd be tempted to experiment with indirect symbol resolution as an alternative to running code on load; the idea is that when a symbol is resolved, instead of the loader looking up the symbol value directly (as it does normally), it would run code that provides the symbol value or a reference to another symbol to look up. You can then implement deferred dynamic initialization by having indirect lookup functions that check a flag, run static initializers if needed, and return the "real" symbol to the loader for it to look up - slightly slowing down symbol lookup if static initializers exist, but deferring it to the last moment that C++ permits.
Identifying dependencies used via dlopen()
Posted Apr 25, 2024 14:15 UTC (Thu) by mathstuf (subscriber, #69389) [Link]
Once the library is loaded, you've already lost if it does anything that doesn't allow unloading. Touching thread-local storage on macOS will do this. As will any static initializers that *do* exist. One nice thing on Windows is that you can interrogate a library without loading it. One can use `libelf` for that with ELF platforms, but that is Another Dependency and you're left figuring out what the runtime loader will do with it on your own.
Identifying dependencies used via dlopen()
Posted Apr 22, 2024 18:44 UTC (Mon) by ibukanov (subscriber, #3942) [Link]
With such option all library functions point to a code that first ensures initialization of the library and then calls the function itself. From the library point of view this looks as if the library was loaded with dlopen followed by a call to the function.
Identifying dependencies used via dlopen()
Posted Apr 22, 2024 19:50 UTC (Mon) by Cyberax (✭ supporter ✭, #52523) [Link]
It's really much better to just do initialization explicitly by calling something like "init()" after dlopen(), or just link libraries eagerly and incorporate initialization into the overall application initialization.
Identifying dependencies used via dlopen()
Posted Apr 23, 2024 9:29 UTC (Tue) by farnz (subscriber, #17727) [Link]
This is how iOS and macOS work; the initializer is run on library load, and library load is delayed until the first symbol from that library is referenced. It's also explicitly permitted in the C++ standard - deferred dynamic initialization takes place no later than just before the first use of a symbol from the translation unit that contains the static initializer.
Now, I'd agree with the notion that deferred dynamic initialization (runtime constructors for static objects) is a mistake. But it's a mistake baked into C++, and from there into psABIs.
Identifying dependencies used via dlopen()
Posted Apr 23, 2024 17:56 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link]
It's definitely not _that_ lazy by default. The initializers run during the dlopen() time.
Identifying dependencies used via dlopen()
Posted Apr 24, 2024 9:11 UTC (Wed) by farnz (subscriber, #17727) [Link]
I was referring not to dlopen, but to libraries that are lazily linked in to the binary via Mach-O dynamic linking; at the first use of a symbol from the library, the library's deferred dynamic initialization code is run.
Identifying dependencies used via dlopen()
Posted Apr 24, 2024 11:00 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link]
https://developer.apple.com/forums/thread/131252
> That feature has been removed from the linker. The only work around is to not link with library and instead dlopen() it at runtime and then use dlsym() to find the functions you want from it.
So yes, it's was a horrible feature that was barely used. So Apple did the right thing and removed it.
Identifying dependencies used via dlopen()
Posted Apr 23, 2024 16:36 UTC (Tue) by ibukanov (subscriber, #3942) [Link]
Identifying dependencies used via dlopen()
Posted Apr 23, 2024 18:04 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link]
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 1:49 UTC (Wed) by azumanga (subscriber, #90158) [Link]
There really is no need for something like xz, when used as a library dependancy, to have any more access than "input data goes here, output data goes here".
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 8:46 UTC (Wed) by gmatht (subscriber, #58961) [Link]
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 9:29 UTC (Wed) by smcv (subscriber, #53363) [Link]
(Of course, if your (de)compression library contains a back door, there's no guarantee that it is faithfully decompressing what it was given as input...)
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 10:30 UTC (Wed) by jreiser (subscriber, #11027) [Link]
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 10:32 UTC (Thu) by LtWorf (subscriber, #124958) [Link]
I don't think WASM is so secure as you think it is.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 8:14 UTC (Wed) by farnz (subscriber, #17727) [Link]
There's the Mach-O approach (macOS, iOS): each symbol import identifies both the symbol and the library it comes from. A library is not loaded until the first use of a symbol it supplies; thus, merely dynamically linking a library this way is inert up until you use a symbol from that library.
This then lets you get both behaviours; with -z now, the library is loaded (and thus its .init_array and similar executed) as soon as you start the program. With -z lazy, the library is loaded (and thus its .init_array and similar executed) the first time you reference a symbol from the library.
I suspect that you could do similar in ELF without requiring the symbol imports to identify the library they come from, by making more of the link process lazy; I have not looked in depth, but I believe that you could implement this by tracking symbols against sources inside the dynamic linker instead of having to have the importer tell you which library to import for this symbol.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 10:12 UTC (Wed) by bluca (subscriber, #118303) [Link]
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 11:41 UTC (Wed) by smoogen (subscriber, #97) [Link]
Method 2 would be to come up with a new standard (ORC, ELF, and DWARF are all taken so maybe ENT?) and recompile the universe to make that work. Sometimes it is easier to boil the ocean than deal with academic committees :)
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 19:37 UTC (Wed) by lgerbarg (guest, #57988) [Link]
Identifying dependencies used via dlopen()
Posted Apr 24, 2024 18:00 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link]
100% support that. Lazy loading of libraries is actively bad and leads to crazy designs.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 9:47 UTC (Wed) by Tobu (subscriber, #24111) [Link]
I remember when the --as-needed linker flag was introduced, reducing the dependency footprint of binaries. Now that distributions use it by default, binaries and shared libraries only link against other libraries if they can be used to resolve a non-weak symbol. The limitation of that is that the "object needs library" relationship only describes direct relationships between any symbol of the first object towards any symbol of the second.
I think a more inspectable alternative to lazy loading (although you could do both) would be to compute the transitive closure of needed libraries on executable files at link time. This would be a subset of libraries reachable via DT_NEEDED relationships, but only take into account symbols reachable from the executable. Possibly the analysis would need something like lto to be enabled as well. Then, the dynamic loader would not map libraries that aren't needed by symbols of the top-level executable.
This way, assuming sshd links to libsystemd for one symbol, and libsystemd links to libxz but only for other symbols, the sshd executable will not have libxz included in its manifest of transitively required libraries, and the dynamic loader will not look for it either.
Identifying dependencies used via dlopen()
Posted Apr 16, 2024 22:30 UTC (Tue) by flussence (subscriber, #85566) [Link]
Few ideas:
- A fixed list of dlopen patterns like this needs basic shell globs and a subset of variables if possible (so you can have desktop applications declare intent to load from ~/.local/<foo>/plugins/ or $XDG_RUNTIME_FOOBAR instead of just giving up).
- A capability bounding set stored in the binary would be good for system services instead of leaving that as a sysadmin guessing game. I can count on one hand the number of daemons I've encountered that bother to fill in that part of their systemd .service files, it needs to be ubiquitous and frictionless to get any adoption. This would be complementary to xattr caps since those are about increasing privileges (and also they don't work over NFS).
- It's probably unreasonable to try to cram seccomp or network rules in here, given that understanding either of those seems to be a full time job. Unless it's something *extremely* simple like "disallow all network and socket access" or "give me seccomp mode 1"... but then that'd break "/bin/true --help" on a GNU system.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 2:36 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link]
OK, you screwed up with libsystemd exploding it in size until it became a problem. So live with it, and create another separate library that only contains lean stuff ("liblightsystemd" or whatever). Split the heavy dependencies into a separate "libjournald".
Instead we're now getting hacks upon hacks that further complicate the system.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 6:56 UTC (Wed) by Wol (subscriber, #4433) [Link]
> Instead we're now getting hacks upon hacks that further complicate the system.
Except your solution doesn't solve the underlying problem, which is "if using unsafe techniques is easy, programmers will use them". Splitting the library doesn't get rid of the problem, it just makes it harder to exploit, which for a paid black-hat isn't a problem.
Cheers,
Wol
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 8:52 UTC (Wed) by pbonzini (subscriber, #60935) [Link]
Rather than splitting the library, I'd turn the utilities for daemons into a small copylib, but that's just a spin on the split idea.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 13:21 UTC (Wed) by kazer (subscriber, #134462) [Link]
The automatic linking system is great to reduce workload on programmers, but has the mentioned downsides. Better support for optionals that would not increase bugs or programmer workload significantly sounds great.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 13:34 UTC (Wed) by farnz (subscriber, #17727) [Link]
There's also a deeper issue; anything that is loaded has code run for you by the dynamic linker at load time. regardless of whether symbols from that object are used. As a result, if an unexpected thing is loaded, it can do anything, even if it was supposed to be inert in this situation.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 9:38 UTC (Wed) by donald.buczek (subscriber, #112892) [Link]
However, this strategy becomes problematic when dealing with runtime-loaded plugins and shared objects. The complexity escalates with components like PAM, NSSwitch, and Apache modules, particularly when updates involve critical libraries like glibc. The issue arises from shared objects within the same package that depend on each other but lack backward compatibility due to internal ABI changes. This discrepancy becomes problematic when an updated shared object is dynamically loaded by an older version still in memory.
For instance, updating glibc can lead to conflicts where long-running services, such as sshd, attempt to initiate new sessions with mixed versions of glibc's shared objects, leading to potential segmentation faults or worse. This scenario is exemplified by the interaction between libc, libnsl, and libnss_files and pam_unix within the glibc and Linux PAM packages.
While currently limited to specific use cases involving plugins, the issue represents a significant concern. The adoption of dlopen() for shared libraries, as seen with systemd, amplifies these challenges, raising apprehensions about the sustainability of this approach in critical computing environments.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 10:09 UTC (Wed) by bluca (subscriber, #118303) [Link]
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 10:43 UTC (Wed) by donald.buczek (subscriber, #112892) [Link]
Image-based deployment models wouldn't suit us, because generally, we do profit from the fact, that after an update all systems have the same userspace and all tools are on the same version. While long-running daemons and user jobs might use old versions of shared libraries until they are restarted or the system reboots, these are only few processes compare to the dynamic work. Our users profit from the fact that something, they run on their workstations, runs without any modification on any system, any public compute server or as a cluster job and it eases our centralized management and monitoring. Plus, most of the time updates have a reason, we want the new versions, otherwise we wouldn't do the update. It might be just commands and tools, but if it is important for specific lingering daemons, we can just restart these.
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 11:10 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link]
musl libc manages to do just fine without PAM or NSS. And what "other projects" that are equally fundamental in Linux?
Identifying dependencies used via dlopen()
Posted Apr 17, 2024 16:09 UTC (Wed) by jem (subscriber, #24231) [Link]
Are you proposing to drop PAM because "musl libc manages to do just fine without PAM" (i.e. doesn't support it). Talk about putting the cart before the horse!
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 0:47 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]
NSS can be replaced by a daemon talking the NSCD protocol, and PAM can be segregated into a separate authentication daemon (like SSSD) or replaced by something else (like ephemeral certificates for SSH).
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 7:14 UTC (Thu) by gioele (subscriber, #61675) [Link]
>
> NSS can be replaced by a daemon talking the NSCD protocol, and PAM can be segregated into a separate authentication daemon (like SSSD) or replaced by something else (like ephemeral certificates for SSH).
Are we sure that swapping out dynamic linking for IPC is a net positive move?
(Sincere question from somebody that welcomes every reduction of complexity in the basic stack and TCB.)
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 7:38 UTC (Thu) by donald.buczek (subscriber, #112892) [Link]
Maybe not generally, but it would be w.r.t. the specific problems I have described. Also, if well done, it could be a more secure design. And to me, the pam code and API design doesn't look very nice and a replacement would have a chance to have better coding style.
On the other hand, we've utilized both, PAM and NSS with our own modules in the past and currently consider to create another PAM module for authentication. And while the authentication/authorization part of PAM could well be done with very minimal static code in the client talking to a server, which does the complicated or privileged things, there are several pam modules, which need to run complicated code in the client process, for example pam_limits or pam_env. So if you want to keep this functionality, you won't profit much from a split between processes. These difficulties don't exist for NSS, so I think NCSD might be a better design.
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 7:47 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]
Unquestionably, yes.
PAM and NSS modules have always been awkward, they depend too much on the environment that is not under their control. This can be kind of an issue for PAM modules, especially if you want them to access devices like hardware tokens or write/read files.
You also need to write them in a very careful manner to not interfere with the application that _uses_ them. You can't just add a dependency on libcurl if you're writing an NSS/PAM module, you can't easily launch threads or mess with signal handlers.
Even tasks that _should_ be straightforward like logging and auditing become non-trivial inside PAMs. And this is just not a good situation to be in, for such a critical part of the system.
We now have a very capable daemon management solution (systemd), with socket activation, proper support for logging and so on. It solves nearly all the issues with PAM/NSS.
The only major missing piece is a standard high-level IPC protocol, although even that role is somewhat adequately fulfilled by dbus these days.
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 10:06 UTC (Thu) by farnz (subscriber, #17727) [Link]
Definitely a positive for security; it might be a slight negative for performance in the case where your PAM or NSS stack is very fast in-process, but that should be an extremely rare case - the moment you use a network service for auth, it becomes slow enough that IPC is not the bottleneck.
With IPC to an authentication and authorization daemon (SSSD, for example), you can run the daemon in a known-good environment (so no surprises from LD_PRELOAD, ptrace or similar), with access to files that the process is blocked from accessing (can read /etc/shadow, even if the process isn't authorized to do so). In turn, this would let you lock down access to the configuration for your auth stacks, which you can't do today (/etc/nsswitch.conf has to be readable by any process that wants to ask about authorization, for example).
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 11:29 UTC (Thu) by gioele (subscriber, #61675) [Link]
What I worry about are failure modes. IPC has very different failure modes compared to dynamically loaded libraries (timeouts, partial responses, duplicate responses, DOSes) and security problems (MITM).
Daemonized PAM/NSS will be hot targets for malicious actors. Will users of these PAM/NSS daemons correctly handle these novel failures?
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 11:41 UTC (Thu) by dezgeg (subscriber, #92243) [Link]
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 11:41 UTC (Thu) by farnz (subscriber, #17727) [Link]
Anyone running SSSD, NSCD, or NSLCD has these failure modes already, along with the failure modes of dynamically loaded libraries. Those three all supply nsswitch plugins that simply do IPC to a daemon to get authorization details. Additionally, SSSD supplies a PAM plugin that does authentication over a socket.
And users of NIS, NIS+, LDAP or Kerberos have this problem but worse, since the IPC is done over a network, not over a Unix Domain Socket.
There's also no reason to insist that everything goes via IPC; you can have a built-in handler for "files" and "pam_unix" file-based methods (optionally disabled, of course), with IPC for the cases where the built-in handlers don't work for your use case. Basically, have a hard-wired set of options, with IPC instead of dlopen for cases where the hard-wired set isn't enough.
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 13:06 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]
Others replied about other issues, but I just want to note that MITM doesn't really apply for the local IPC. You can reliably verify the caller's identity using good old Unix sockets (SCM_CREDENTIALS and others).
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 21:03 UTC (Thu) by dkg (subscriber, #55359) [Link]
IPC has very different failure modes compared to dynamically loaded libraries (timeouts, partial responses, duplicate responses, DOSes) and security problems (MITM).
I agree with you about the different kinds of failure modes, but the discussions in this thread suggesting lazy dynamically loaded libraries (whether done via dlopen() or by the dynamic linker itself) also introduce some novel failure modes.
figuring out how to wrap any of these scenarios in reasonable error-handling sections so that arbitrary tooling that relies on them can be updated smoothly is the real trick.
Great idea
Posted Apr 17, 2024 10:09 UTC (Wed) by vadim (subscriber, #35271) [Link]
While we're at it, it'd be nice if there could be a similar feature for binaries. As in, you can easily find out that this program calls something from /usr/libexec.
Great idea
Posted Apr 17, 2024 17:34 UTC (Wed) by matthias (subscriber, #94967) [Link]
Great idea
Posted Apr 17, 2024 17:40 UTC (Wed) by matthias (subscriber, #94967) [Link]
But where do we stop? There are not only libraries and executables, but also data. In theory the application can depend on arbitrary files to be available. But I do not know how much of a problem this is.
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 6:27 UTC (Thu) by guillemj (subscriber, #49706) [Link]
Using dlopen() like this might appear easier (for specific upstreams), than either properly defining a plugin framework (which should not amount to much code anyway), or moving the linking into independent programs that can then be optionally called. This is a hack and a workaround that goes behind the toolchain back, that pretty much just shifts the complexity elsewhere, with reduced safety nets and functionality (for example with dpkg we have dependency granularity up to versioned symbols). If the proposal was to improve the toolchain to support optional linking, then that would be a welcome change, but this is rather disappointing.
I've covered that in the past (with further references):
https://lists.debian.org/debian-mentors/2017/11/msg00196....
https://github.com/systemd/systemd/pull/17416#issuecommen...
https://github.com/systemd/systemd/pull/17416#issuecommen...
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 10:15 UTC (Thu) by bluca (subscriber, #118303) [Link]
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 11:05 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]
dlopen() to load optional dependencies, on the other hand, is great.
Right?
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 14:42 UTC (Thu) by bluca (subscriber, #118303) [Link]
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 14:54 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]
When your feature requires reinventing something fundamental as ELF, then you're probably on a wrong track.
Identifying dependencies used via dlopen()
Posted Apr 18, 2024 18:55 UTC (Thu) by bluca (subscriber, #118303) [Link]
Identifying dependencies used via dlopen()
Posted Apr 19, 2024 1:02 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]
Identifying dependencies used via dlopen()
Posted Apr 19, 2024 8:45 UTC (Fri) by bluca (subscriber, #118303) [Link]
Identifying dependencies used via dlopen()
Posted Apr 19, 2024 8:58 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]
First, "optional dependency" is kind of an oxymoron. It's either a dependency or not.
A more correct way to put it: it papers over a bloated dependency set, by moving dependency resolution from compile-time to runtime. You can compare it with dynamic languages and static languages. Static languages check stuff at compile time, but dynamic languages allow users to just write whatever and delay checking until the code is run.
In this particular case, this also very much applies. dlopen() will interfere with sandboxing, or with mseal()/mimmutable(). It will also introduce failure modes if your code enters a namespace that doesn't have the dependencies.
And all of that, what, to avoid admitting the mistake and splitting libsystemd into libjournald and a lighter libsystemd?
Identifying dependencies used via dlopen()
Posted Apr 19, 2024 10:09 UTC (Fri) by bluca (subscriber, #118303) [Link]
Optional features are obviously a thing, as the dependencies needed for an optional feature are obviously and clearly optional, this is just basic logic. Not only that, you are literally arguing against reality, given this is how it actually works, right now, there's nothing theoretical about this.
It's quite plainly obvious given this rant that you have a gigantic chip on your shoulder, so there's not much point in going on. It's very simple: we will use dlopen for most of our dependencies, libsystemd will stay exactly as it is other than that as it has the right design for a number of reasons, and we will work around outdated package managers with something like the elf note discussed in the article. If one day glibc+gcc give us the same feature mach-o implements with on-demand-resolved weak dt_needed, we'll switch to that.
Identifying dependencies used via dlopen()
Posted Apr 19, 2024 11:25 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]
What "actually works"? I can have a completely static system with musl, or with iOS where apps are statically linked.
dlopen() hacks and "optional dependencies" are just sloppiness, nothing more. In your case it's to cover up the screwups that happened with journald.
> It's quite plainly obvious given this rant that you have a gigantic chip on your shoulder, so there's not much point in going on. It's very simple: we will use dlopen for most of our dependencies, libsystemd will stay exactly as it is other than that as it has the right design for a number of reasons, and we will work around outdated package managers with something like the elf note discussed in the article
Are you going to support mseal()/mimmutable() or are you going to give a middle finger to people who want security?
Identifying dependencies used via dlopen()
Posted Apr 19, 2024 12:40 UTC (Fri) by pizza (subscriber, #46) [Link]
One of my printer drivers has an _optional_ dependency on an image processing library.
It is optional unless you have one of two specific models, then that library becomes mandatory if you want the printer to produce anything considered passable.
That library started out as proprietary, non-redistributable, and x86-only. So I reverse-engineered it and wrote an F/OSS replacement. Problem is that due to patent concerns, the F/OSS library isn't distributed in binary form -- or even as source in the upstream project.
Doing things this way lets distributions handle most of the support burden -- ie a single binary covering everyone, and that works for everyone without those two specific models. Folks with those models who get a license to use the proprietary library (that's guaranteed to work) can plop it in /usr/local/lib; or they can do a 'git pull && make && sudo make install` and get (so far) bit-for-bit identical results.
A dlopen() based approach is by far the simplest [1] way to accomplish the goals in a portable [2] manner.
[1] ie least burdensome to *me*
[2] Linux, MacOS, and even Windows. (ie probably every platform with libusb support)
Identifying dependencies used via dlopen()
Posted Apr 19, 2024 13:02 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]