Xz: Can you spot the single character that disabled Linux landlock?
git.tukaani.org
git.tukaani.org
Description of Linux's Landlock access control system if you are not familiar with it: https://docs.kernel.org/userspace-api/landlock.html
xz official (maybe...) incident response page: https://tukaani.org/xz-backdoor/
The stray dot isn’t valid C, so it will never compile. By ensuring it can never compile, Landlock will never be enabled.
But I still think reading boring logs is a great way to understand a system. Turning on all the intermediates for a compiler (llvm ghc etc) is very educational too.
That's my point, you're going to look at autoconf's output if a feature you're expecting is missing.
But would you think to have a test or expectation for landlock in xz? How would you even check if landlock is enabled for a process?
This is quite common in build systems. I had such a test produce incorrect results in the kernel build system recently. The problem is that the tests should really look carefully for an expected error message. If the compilation fails with the expected message the test result is false. If the compilation fails for some other reason the build should fail so the developer is forced to investigate.
Disclaimer: I have not studied the xz build system in detail, just what I saw from the diff plus a bit of extrapolation.
It doesn’t fail the overall build.
While I like the idea, I fear this would go the same route as UserAgent strings, with everybody giving carefully crafted lies. The main issue is that bugs are only known in retrospect.
As an example, suppose Compiler A and Compiler B both implement Feature X. Therefore, they both produce JSON output indicating that they support Feature X. A year later, developers for Compiler A find and fix a bug in its implementation of Feature X. Now, how do we handle that case?
* Update the version for Feature X as by Compiler A? Now Compiler B is publishing the same version as the buggy Compiler A did, and would get marked as not having a usable implementation.
* Update the spec, forcing a version update for Feature X in both Compiler A and Compiler B? Now a bugfix in one compiler requires code updates in others.
* In the build system, override the JSON output from Compiler A, such that Feature X is marked as unavailable for the buggy versions? Now the JSON is no longer trustable, as it requires maintaining a list of exceptions.
* Try to trigger the bug that Compiler A just fixed, so that the build system can recognize the buggy versions, regardless of the JSON output? Now we're back to the starting point, with features determined based on attempted compilation.
Except that lone period, hard to recognize because of its small size and proximity to the left edge of the diff, caused the C code to become always invalid, hence keeping the landlock always disabled?
That's both vilely impressive and impressively vile. I didn't even spot it on my first read-through.
EDIT: come to think of it, even that might not have done much here, where well-formedness is the issue :(
Edit, sorry, shoulda read your whole commebt before replying
This issue is no worse that having multiple different variables that look the same and only in their obscure same-looking unicode symbols.
A linter can handle this. As can a decent font, or editor settings.
How?
if(some condition)
do something; {
do something else;
}
do another thing;
You can stretch 'some condition' out over multiple lines and have some more parents inside of it, to make it more confusing.See also https://softwareengineering.stackexchange.com/a/16530
Yes
I class that as a bug in the original C specification
That would be Python. I do not like it, which does not mean it is bad, it is a matter of taste
The alternative is you say "blocks are delimited by {}"
`if(foo) bar` should be invalid
`if(foo){bar}` is OK by me. Worik's tick of approval
(Neither is valid C I think)
Not necessarily. In practice, in C-as-actually-used people (should) set up linters and formatters, so that you can rely on indentation.
When programming, this means that you can behave as if both curly braces and indentation are significant, and you get an error when they are out-of-sync.
Like what? As you note, mixing tabs and spaces is now an error.
I've never understood the objection to semantic whitespace. Complaining about having to indent code correctly strikes me as being akin to complaining about having to pee in the toilet instead of the bathtub.
Bonus, if there's a good reason to change the linter format, you can do so. Rust handles this rather well I think.
Whitespace is a delimiter in (almost?) all languages humans use.
Whitespace determining which scope you’re in is one of the many problems of making whitespace significant, which might be what you meant.
Yes, I agree that Python should just forbid tabs. As a second best, you can tell your linter (and/or formatter) to forbid tabs (and any weird unicode whitespace). That's basically the equivalent of compiling your C with -Wall.
Well, there's also the opposite: Goldman Sachs's Slang allows space as part of identifiers.
Many SQL implementations permit whitespace in identifiers, but then you need to use quoted identifiers.
> eg Python could do something similar, because they don't have any existing syntax where you just put two identifiers next to each other with only a space in between
The interaction with keywords would cause some issues. For example, right now, “if” is not a valid identifier (keyword), but “if_” and “_if” are. However, with this proposal “x y” could be a valid identifier, but “x if” would introduce ambiguity
> But C could not, because variable declaration is just two identifiers, one for the type and one for the variable name, next to each other with a space in between
This is one aspect of C syntax I have never liked. I always wish it had been Pascal-style `x:int;` instead of `int x;`
Sorry, I don't remember, and I can't seem to find it out online. I could ask my friends who still work there, if it's important. (For what it's worth, I never remember anyone complaining about mixing up a different number of spaces in their variable names. So either all number of spaces were treated the same, or perhaps multiple spaces in a row in an identifier were just banned (via linter or via language?).)
> The interaction with keywords would cause some issues. For example, right now, “if” is not a valid identifier (keyword), but “if_” and “_if” are. However, with this proposal “x y” could be a valid identifier, but “x if” would introduce ambiguity
Yes, you would need to sort out these details, if you wanted to add this 'feature' to Python.
> This is one aspect of C syntax I have never liked. I always wish it had been Pascal-style `x:int;` instead of `int x;`
I'm glad Rust made the same decision.
I do like using the 'space operator' to denote functions calls, at least for a language like Haskell or OCaml.
Tabs still have syntactical meaning and are still invisible
Python will never be a success, I predict, because of this
Trust me
People are really quick to add optionality like this without understanding the maintenance cost. (Every boolean feature flag increases the number of variants you need to test by 2!) Either make a decision, or check that both sides work. Don't let people check in dead code.
Edit: oof, though the stray character in question is inside a perfectly legitimate C string, so to catch this, any such diffs would need to Matroyshka down and that seems unsolvable / intractable.
The problem here wasn’t the concept of feature detection but that it was sabotaged by someone trusted and easily missed in the noise of other work. What was needed was someone very carefully reviewing that commit or build output to notice that the lockdown feature was being disabled on systems where it was fully enabled, and we already know that maintainer time was in short supply. Any other approach would likely have failed because the attacker would have used the same plausible sounding language to explain why it needed to be a complicated dynamic check.
That the sabotage worked at all relied on the fact that nobody was testing those features.
How secure is a sandbox that may not even exist? Apparently its good enough for most.
Not sure, you could also just forbid code that's too complex to analyse without going down a rabbit hole. Instead of trying to go down the rabbit hole.
In general, it's hard to analyse arbitrary code. But we don't have to allow arbitrarily complex code when doing code review.
(void)SYS_landlock_create_ruleset;
with this: (void)SYS_landloсk_create_ruleset;A utf character from a language that has zero chance of being mapped to your programmer's keyboard in the middle of a code line, that would be obvious intentional tampering or at the very least raise some eyebrows.
I accidentally get Cyrillic "с" in my code few times a year. It's on the same key as Latin "c", so if I switch keyboard layout, I don't notice until the compiler warns me. Easy to do if I switch between chats and coding. Now my habit is to always type a few characters in addition to "c" to check what layout I'm _really_ using.
Granted, it's easier with a one-letter variable called "c", but with longer names I can easily see myself not typing "c" the first time (e.g. not pressing hard enough), starting build, chatting in some windows, getting back, facepalming, "fixing" the error by adding "c" or "с" depending on my keyboard layout.
and since it's the only Cyrillic character that's placed on the same key as the same-looking english character, I don't even see the problem with my eyes when the autoreplacement fires
You left out a very important qualifier: in the middle of a code line
The chances of someone accidentally fat-fingering a keyboard layout switch, typing one character, then switching back again without noticing while typing a line of code are very slight indeed.
Plus the fact that you'd need to speak a language written in that alphabet to have a plausible reason for having the keyboard layout enabled in the first place.
That said, perhaps something like disabling the default -fextended-identifiers [0], and enabling the -Wbidi-chars [1] warning.
[0] https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html...
[1] https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#inde...
Expanding macros on different OSes could complicate things though, and determining what flags to build the feature check code with — so perhaps filtering based on the type of error would be best done as part of the build system functionality for doing the feature checking.
A syntax error might be exactly what they’re looking for e.g. they’re feature testing a new bit of syntax or a compiler extension.
> so perhaps filtering based on the type of error would be best done as part of the build system functionality for doing the feature checking.
Which would require every compiler to have detailed, consistent, and machine-readable failure reporting.
Handling the output from several of the most recent GCC and Clang versions would probably cover the majority of cases, and add in MSVC for Windows. If the output isn’t from a recognized compiler or doesn’t match expectations, the only option is falling back to current behavior. Not ideal, but better than the current status quo…
A mitigation here would be to make result of autoconf only provide instructions for humans to change their build config, instead of doing it for them silently. The latter is an anti-pattern.
FWIW, the approach you propose is how the UI tests for rustc work, with checks for specific annotations on specific lines, but those have the benefit of being tied to a single implementation/version and modified in tandem with the app. Unless all compilers could be made to provide reasonable machine readable output for errors, doing that for this use case isn't workable.
Here is my bad variant of the feature, via confusables: https://github.com/rurban/gcc/tree/homoglyph-pr103027
The better variant would be to use my libu8ident, following UTR 39. I only did that for binutils.
So what? Source code was fine with ASCII for a long time, this push for unicode symbols is a recent endeavor and IMO a huge mistake not just because of the security impliciations.
Btw, the -fno-extended-identifiers compiler parameter gives an error if UTF-8 identifiers are used in the code: <source>:3:11: error: stray '\317' in program float <U+03C9><U+2083> = 0.5f;
Maybe in the future more languages/ tools will have the concept of per-project character sets, as opposed to trying to wrangle all possible Unicode ambiguity problems.
I suppose then the problem is how to permit exceptions when integrating with some library written in another (human) language.
Impressive.
So stuff like that is increasingly more reliable caught.
Including by 3rd parties doing any ad-hoc scanning.
Through adding `compiles check` fail with syntax errors definitely should be added tot he list of auto scan checks, at least in C/C++ (it's not something you would do in most languages tbh. even in C it seems to be an antipatters).
To me, that makes the . more evil.
warning: the usage of Script Group `Cyrillic` in this crate consists solely of mixed script confusables
--> src/lib.rs:1:4
|
1 | fn SYS_landloсk_create_ruleset() {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: the usage includes 'с' (U+0441)
= note: please recheck to make sure their usages are indeed what you want
= note: `#[warn(mixed_script_confusables)]` on by default
https://play.rust-lang.org/?version=stable&mode=debug&editio...From what I understand, this landlock disabling wasn't relevant to the sshd attack. It appears it was setting up for something else.
- doesn't force users to differentiate between syntax errors and other errors (e.g. symbols not found). Partially you can't even make it work properly if you want due to C macros.
- it seems sensible, tbh. if "does compile" checks for compatibility seems sensible then there is so much wrong with the ecosystem in so many ways
That is because a human should have to Look at the build and manually make the decision to switch off a security feature from the build.
It seems like it is infinitely better to have a default set of features and explicitly require the user to say `--enable-x --disable-y` as needed. Then error out if a required library isn't available. (Preferably saying which feature requires it.)
Most of us are humans after all, and being social creatures we tend to take violations of trust quite deeply.
His commits on these links look legit to me. It's a sad situation for him if he wasn't involved.
The Linux ecosystem will either figure shit out and maintain it or move into a different archive library.
I'd be nuking everything, possibly even disposing of hardware and setting up a new verifiable online identity on a clean system/hardware.
A side by side in something like "meld" will highlight changes but also means you're reading the full context as it will exist in the code base.
My number one complaint about "code review" is the number of people who simply run their eyes over the diff (in private orgs) because management says "code review" and that's as far as they ever take that in terms of defining outcomes.
Note that this is C code inside if a cmake string, even if your diff can do highlighting the odds it would highlight that are low, and if it did highlighting is generally just lexing so there wouldn’t be much to show.
There's better ways to hide by swapping in Unicode lookalike characters. Some of them even pixel match depending on the font.
Maybe I'm out of the loop but is intentionality settled here?
:wq!
If you are using vi and your left shift key is dodgy, that can easily happen by accident.
There would be plausible deniability in a different situation, but this is the same author who implemented the backdoor and several similar changes that disable security features. I don't think the benefit of doubt is deserved here.
How can we ensure say, that Microsoft doesn't pay someone to throw a wrench in libre office development or Adobe to sabotage Gimp?
There's lots of deception strategies for bad faith actors and given the paucity of people who actually do the work, it's really really hard to be picky.
Especially with the complexity of library dependencies. Defect-at-a-distance may actually be a common strategy and this is the first that was caught
Microsoft and Adobe have reputations to uphold long into the future.
Is that infallible? Hell no it isn't, but consider that Jia Tan only needed to uphold his reputation insofar as getting his backdoor onto everyone's systems. Once that is done, his reputation or the lack thereof becomes a non-issue because his work is done. We're lucky we caught him just before the finish line.
The likelihood of demonstrably reputable parties like Microsoft and Adobe poisoning the well is practically nil because they don't have a finish line.
They can secure the dominance of their offering against the open source competition for well under 500k a year. It's a no brainer.
What this might look like would be say, poorly discernable icons, clumsy UI design, or an unstable API that makes plugins constantly break. Large volumes of documentation that are inadequate or inaccurate in critical places, etc.
If I was malicious I'd pay someone to write otherwise functional code and bug fixes but make really user hostile decisions whenever possible.
We should be diligent for this in a few key projects. Tech companies could easily be doing this already.
There are levels of skull duggery. Hiring someone to pretend to work for a competitor while secretly sabotaging them is a whole other level of skullduggery with a lot of liability attached. I don't think that would be worth it to them.
Like shooting a whistle blower?
It is probably prudent for you (and other like-minded individuals) to be more careful who you think your enemies really are. Infighting against friends and allies, or even neutral parties, is exactly what your real enemies would want you to do.
2. Make them contribute to a competing open source project.
3. Profit!
Even paying a cutout consulting company to do it would be iffy since so many finance employees would be involved in paying them, it would leak sooner than later - or at least raise a lot of questions that would be hard to answer legitimately. Being a public company, also hard to avoid auditor scrutiny.
Even a private company would have an easier time.
Nation state actors though? No such issues - that’s literally why security clearances, compartmentalization, and ‘you never really leave’ are done the way they are.
And they don’t have to worry about market blowback unless they get really excessively heavy handed (like France did in the late 80’s-90’s). They are setup to do shady stuff, it’s their raison d'etre.
[https://en.m.wikipedia.org/wiki/Industrial_espionage#:~:text....]
Try pasting any of these into your IDE and see if it catches it. https://gist.github.com/StevenACoffman/a5f6f682d94e38ed80418...
Yes. The exploit used a payload, that was stored in the tests directory as a binary compression test case, but which is very clearly a very intentional exploit payload.
All the sneaky stuff was about deploying that payload without it being obvious.
+ # A compile check is done here because some systems have
+ # linux/landlock.h, but do not have the syscalls defined
+ # in order to actually use Linux Landlock.
Fix those headers on those systems to explicitly opt-out. What's the point of headers if they don't declare their capabilities?Also why isn't there a single test after a binary blob (even when compiled from open source) is made to ensure security is in-tact?
I wouldn't even ship a website checkout without end-to-end tests for core capabilities. There must be a priority misalignment of adding features > stability.
Edit: I hope the 'fix' isn't to remove the '.'--I just saw the other post on HN that shows removing the '.'
Fixing headers or extra tests would not have prevented this, as there is no indication the headers were broken in the first place, and extra tests could have been compromised (or ignored for release tarball) some other way.
[1] https://git.tukaani.org/?p=xz.git;a=commit;h=328c52da8a2bbb8...
Since users could be trying to compile it on GCC 9.4.0, and as the functionality dependent on the header isn't critical to the application, if the build detects the header is broken, it just disables the functionality and emits a warning.
Back to xz: If security isn't your top priority, it seems reasonable to just ignore the failure of an optional feature and just move along. Of course, in hindsight, it's easy to point fingers, but without that, it doesn't sound completely unreasonable to me.
“LGTM!”
If you have commit access to it, and nobody is there to see, nothing stops you.
Can you prove it? Where's the evidence? ;)
Our industry is a laughingstock.
One more thing that I don't see enough people talking about here: Writing good software is as much about human relationships (trust!) as it is about the computer code itself. When you have a deeply trusted, highly competent teammate, it is normal to put less effort into the review. And to be clear, I am talking about the 99% scenarios of writing the same old CRUD corporate code that no one gets excited about here. Please don't reply to this post with something like, "Well, I work on Dragon at SpaceX and all branches have 2x code coverage and the QA team has 600 years of combined experience... Yada..."
In general, the most highly skilled developers who are most capable of doing a thorough code review are also the ones who are most likely to be genuinely over capacity as is.
Edit: yes for FAANG-ish companies this is usually a bit different, for this reason. And licenses..
Maybe at FAANGs, but I work at a massive company and code review is non-existent.
Very easy claim to make. Difficult to verify.
I guess you cannot really use it in a generic library like compression/decompression. The library has no clue what the program is supposed to do and what should be restricted.
For a program it might be clearer.
The sshd attack was using liblzma as a library. So disabling landlock seems unrelated? A sign that there is more bad code waiting to be detected / had been planned to be inserted???
Its very confusing. Parts of this sega seem incredibly sophisticated while other parts seem kind of sloppy.
Removing the landlock means the daemon doesn’t lock itself down and will allow for better payload execution when they get to the exploitation stage.
I don’t think these two things are unrelated. I think they already had payloads in mind and realized this would be a hurdle.
The idea is that the library starts a separate thread to do any complex operation and then waits for it in the API call the user code made. The thread landlocks itself and then begins the operation. If something goes wrong the code is confined.
Of course in this case it doesn't work because the sandbox is controlled by the attacker, so they can just turn it off or make it weaker than it should be. But you can also do things other ways.
But why put your backdoor eggs into one basket (library)?
Plus, you do not backdoor the library itself, but the tools using it. "Reflections on trusting trust" style.
Sounds like a perfect plan, until it isn't.
One could probably sneak a buffer overflow or use-after-free into a C project they maintain without being noticed. Actually shipping a trojan is much harder, as observed with the xz-to-sshd backdoor.
More plausibly deniable too.
There was another example where this team submitted a patch that swapped safe_fprintf for fprintf while adding some additional behavior. It was later pointed out that this allows printing invisible characters to the stream, which allows hiding some of the files that are placed when decompressing.
Yeah, just have a default set of features and allow `--enable-a --disable-b` as needed.
The fact that it silently swallows bugs in the check is just another problem of it.
At least for a time, this was a horrible problem in Yocto.
1. Force enable 2. Enable if available 3. Force disable
Like,
--enable_landlock=always
--enable_landlock
--disable_landlockLikewise, I'd rather distros figure out the hard way when a new release has a new feature and needs a new dependency rather than their users complain that a new feature is missing.
Principle of least surprises.
It's much simpler to say "enable this if we can compile it" rather than detecting the compiler and target platform and throwing a mess of conditional compilation into an autoconf script.
If you explicitly set the enable-landlock flag, configure will fail when the feature doesn't compile.
See: https://cmake.org/cmake/help/latest/command/option.html
https://git.tukaani.org/?p=xz.git;a=commit;h=328c52da8a2bbb8...
Of course in practice you shouldn’t be writing each test program by hand. Autoconf has macros that generate them; you would only need to supply an identifier or at most a single line of code and the rest would be created correctly. I’m sure CMake is similar in that regard, so the first red flag is that they wrote a whole test program.
Then compile your test program against your fake header files (with -Imy-fake-includes). It should compile without errors even if landlock is missing from your actual system.
Then build your test program a second time, this time against the real system headers, to test whether landlock is supported on your system.
But it would have been enough here to have a test just to see that Landlock works in general. That test would have broken with this commit, because that's what the commit actually does - break all Landlock support.
Based on that it sounds like there wasn't a test for Landlock integration, if I've understood things correctly.
But yeah, to test it, you can have a mock version of landlock which responds with the error/success as you want, regardless of what the system would normally do. It relies on the test not being sabotaged too though...
This doesn't change my opinion in general - that version should be exposed through a library call and knowing about the specific syscalls shouldn't be needed in xv.
This header defines the data types for the Linux kernel interface, but not how the syscall landlock_create_ruleset(2) will be issued. That is provided by libc either as a separate wrapper function (does not exist in glibc) or the implementation needs to use syscall(SYS_landlock_create_ruleset, ...), with the constant also being provided by libc. That is how it works for all syscalls and you won't be able to change this.
- check_include_file(linux/landlock.h HAVE_LINUX_LANDLOCK_H)
...
+ check_c_source_compiles("
+ #include <linux/landlock.h
Is a compilation-test a legitimate/common/typical method to go about this?Independently, of the breaking code, to me it seems accidental failing, or even accidentally not failing, would be in the nature of such an assessment... So, this commit seems to raise the question of "why?", even if you missed the dot, doesn't it? If a feature is formally available, but effectively broken somehow, wouldn't you want the compiler to complain, instead of the feature dropped silently? Is the reasoning in the code comment sound? Can you test, if syscalls are defined in another way?
Yes—in fact, compilation tests are often the only way you can tell if a feature actually works. It's extremely common for C build systems to detect and work around weird systems.
Again, this is intended to be portable software. It is designed to work on lots of OS’s, with lots of compilers, in a lot of future environments that don’t even exist yet.
If you have a security feature for example, which uses the pledge() syscall on OpenBSD, but you can only use that feature on OpenBSD systems, you have two choices:
- Conditionally compile it based on whether you’ve detected that this is an OpenBSD target at build time, or,
- Conditionally compile it based on whether some sample code that uses pledge() builds successfully.
You can’t defer this decision until runtime, because it would require linking to pledge() symbols even though they may not exist on this system, which would cause the executable to fail to link at runtime, unless you completely rearchitected to use a plugin model, which is overkill.
So given the above are your main two options, the latter is preferred mainly because it allows new systems to come in and be compatible with old ones (maybe someone adds pledge() support to Linux one day) without having to fudge the uname command or something. This was super important in the early Unix days… perhaps less so now, but it’s still a good way to write portable software that still can take advantage of platform-specific features.
A scathing criticism of the OpenSSL library by the BSD team was that it was too portable in a (very real) sense that it wasn't even written in "C" any more, or targeting "libc" as the standard library. It would be more accurate to say that it was "Autotools/C" instead. By rewriting OpenSSL to target an actual full-featured libc, they found dozens of other bugs, including a bunch of memory issues other than the famous Heartbleed bug.
Platform standards like the C++ std library, libc, etc... are supposed to be the interface against which we write software. Giving that up and programming against megabytes of macros and Autotools scripts is basically saying that C isn't a standard at all, but Autotools is.
Then just admit it, and say that you're programming in the Autotools standard framework. Be honest about it, because you'll then see the world in a different way. For example, you'll suddenly understand why it's so hard to get away from Autotools. It's not because "stuff is broken", but because it's the programming language framework you and everyone else is using. It's like a C++ guy lamenting that he needs gcc everywhere and can't go back to a pure C compiler.
Job ads should be saying: "Autotools programmer with 5 years experience" instead of "C programmer". It would be more accurate.
PS: I judge languages by the weight of their build overhead in relation to useful code. I've seen C libraries with two functions that had on the order of 50kb macros to enable them to build and interface with other things.
So it has one, but it's small. It has a few useful functions, for example system(const char*) which was used by the exploit.
Additionally, libc is typically more of a system component than a part of your program. You can't choose to distribute a libc you prefer with your program and use that, you have to link to the system libc on many OSs. Even on Linux where it's not strictly required, if you use a different libc than the distribution provided one, you can end up in all sorts of problems when you interact with other programs.
Java has OpenJDK and Oracle JDK.
The difference from libc though is that there is no problem in distributing a program with your preferred JDK, and multiple Java programs can live on the same system while each using its own JDK and even communicate risk free with each other.
Also, different JDKs are significantly more similar in the API they offer to Java programs than different libc are - at least for a common core of functionality.
Just in case, I want to note that pledge(2) and unveil(2) are also supported by SerenityOS, so checking only for an OpenBSD target is insufficent.
https://awesomekling.github.io/pledge-and-unveil-in-Serenity...
Pledge and unveil in SerenityOS, combined with the planned move to memory safety, will be a powerful combination.
The fact is that things have good easier over the last 10 or 20 years. It used to be the case that any program targeting Unix had to either spend a lot of time and energy tracking the precise differences between dozens of different commercial Unices, or use autoconf. Autoconf was the project that combined all of that lore into a single place, so that most people didn’t have to know every single detail. But these days most of the Unices are dead and buried, and 99% of all new projects just target Linux. Kinda sucks if you prefer one of the BSDs, or OpenSolaris/Illumos/SmartOS, but it does mean that new Linux developers never have to jump through those hoops and simply never learn about autoconf. And while on the one hand that represents a loss of knowledge and competency for the community (making this type of supply–chain attack much easier), on the other hand autoconf is in practice an abomination. It is (or at least was) extremely useful, but it was implemented in M4 and reading the source code will literally damage your brain.
As a data point, the place I worked for in the mid-90s had a single codebase with something over 600 permutations of supported OS and compiler when you included the different versions. One thing we take for granted now is how easy it is to get and install updates – back then you might find that, say, a common API was simply broken on one particular operating system version but your customers would have to wait for a patch to be released, sometimes purchased, put onto floppy disks or CD-ROM, and then manually installed in a way which had enough risk involved that people often put it off as long as they could. Some vendors also did individual patches which could be combined by the sysadmin so you had to test for that specific feature rather than just saying “is it greater than 1.2.3?”, and it wasn’t uncommon to find cases where they’d compiled a common library with some weird patches so you had to test whether the specific features you needed functioned.
Part of why Linux annihilated them was cost but much of it was having package managers designed by grownups - I remember as late as the mid-2000s bricking brand new Sun servers by running the new Solaris updater, which left them in an unbootable state.
- instruction set architecture
- OS versions
- ABIs
- libraries (whether they are installed)
- and what functionality they provide
- commands/executables
- anything you can write a macro to check
All stuff too disparate to reliably have the OS be able to answer every question you might have about it and the stuff installed on it. You can't wait for any such system to learn how to answer the questions you might have about it, so some things you can only detect, either at build configuration time, build time, or run time.__has_include and other compile-time checking would work here (eg type_traits and enable_if)
The problem is that already in the 80s there was tons of variability from on Unix system (or version of it) to the next, but there was no standard way of representing what features/standards/APIs/libraries a system supported or had installed. When faced with such a mess people wrote code to detect features are present on the target host.
This then got made into tools with libraries of detection code. Think autoconf/autotools.
Now we also have pkgconfig, but it's too late and it was not enough anyways.
Some things you might only detect at run-time, provided that their ABIs are stable enough that you can copy their headers into your application.
Optional features should always be configured by whoever tries to compile the code. There can be defaults, but they shouldn't depend on the environment.
[0] https://git.tukaani.org/?p=xz.git;a=blob;f=CMakeLists.txt;h=...
There's AC_LINK_IFELSE if you want to test linking too.