Re: Sourceware mitigating and preventing the next xz-backdoor

anderson.jonathonm--- via Gdb
2024-04-09T16:45:28Z

Hello,

On Thu, Apr 4, 2024, 09:00 Michael Matz <matz@suse.de> wrote:

> Hello,
>
> On Wed, 3 Apr 2024, Jonathon Anderson wrote:
>
> > Of course, this doesn't make the build system any less complex, but
> > projects using newer build systems seem easier to secure and audit than
> > those using overly flexible build systems like Autotools and maybe even
> > CMake. IMHO using a late-model build system is a relatively low
> > technical hurdle to overcome for the benefits noted above, switching
> > should be considered and in a positive light.
>
> Note that we're talking not (only) about the build system itself, i.e. how
> to declare dependencies within the sources, and how to declare how to
> build them. [...]
>
> But Martin also specifically asked about alternatives for feature tests,
> i.e. autoconfs purpose. I simply don't see how any alternative to it
> could be majorly "easier" or "less complex" at its core.

My point was not that newer build systems are any less complex taken wholistically. The upthread has already discussed why configuration tests are necessary and aren't going away anytime soon. My horror stories would not add much to that conversation.

My point, or rather my opinion as a humble end-user, is that newer build systems (e.g. Meson) make identifying the xz backdoor and injections following the same pattern much easier than older build systems (e.g. autoconf+automake+libtool). Paraphrasing my original message to highlight the backdoor protections:

- This xz backdoor injection unpacked attacker-controlled files and ran them during `configure`. Newer build systems implement a build abstraction (aka DSL) that acts similar to a sandbox and enforces rules (e.g. the only code run during `meson setup` is from `meson.build` files and CMake). Generally speaking the only way to disobey those rules is via an "escape" command (e.g. `run_command()`) of which there are few. This reduces the task of auditing the build scripts for sandbox-breaking malicious intent significantly, only the "escapes" need investigation and they which should(tm) be rare for well-behaved projects.

- This xz backdoor injected worked due to altered versions of the build system implementation being shipped with the source. Newer build systems do not ship any part of the build system's implementation with the project's source, it somes as a separate package and the most the project does is specify a compatible version range. This removes the possibility of "hidden" code being mixed to the shipped source tarball, well-behaved projects will have source tarballs that can be byte-for-byte reproduced from the VCS (e.g. Git).

- This xz backdoor injected the payload code by altering the files in the source directory. Newer build systems do not allow altering the source directory (for good reason) in their build abstraction. For build workers/CI, this restriction can be enforced using low-level sandboxing (e.g. conainers or landlock) and well-behaved projects should(tm) be unaffected. This reduces the possibilities on how code can appear in the project, only code generation sequences (e.g. `custom_target()` or `generator()`) can produce arbitrary code.

Just by transitioning to a newer build system (and being "well-behaved") it seems like you get certain protections almost for free. And IMHO compared to other options to improve project security (e.g. automated fuzzing), transitioning build systems is a medium-effort, low-maintainence option with a comparatively high yield.

> make is just fine for that (as are many others). (In a way
> I think we meanwhile wouldn't really need automake and autogen, but
> rewriting all that in pure GNUmake is a major undertaking).

Makefile (the language) is extremely complex, it is nigh impossible to ensure something won't expand to some malicious code injection when the right file arrangement is present. (IMHO Make also is not a great build system, it has none of the protections described above... and it's [just plain slow](https://david.rothlis.net/ninja-benchmark/).)

Newer build systems seem to be moving in a direction where the bulk of the build configuration/scripts are non-Turing-complete. This is a highly effective method for clarifying the high-level complexity of the DSL: simple things are short and sweet, complex things (and discouraged solutions) are long and complex. Less complex high-level build logic makes it easier to verify and audit for security (and other) purposes.

> Going with the
> examples given upthread there is usually only one major solution: to check
> if a given system supports FOOBAR you need to bite the bullet and compile
> (and potentially run!) a small program using FOOBAR. A configuration
> system that can do that (and I don't see any real alternative to that), no
> matter in which language it's written and how traditional or modern it is,
> also gives you enough rope to hang yourself, if you so choose.

Both very true, as discussed upthread.

It may be a helpful policy to use templates for configure tests where possible. For example the [now-famous XZ landlock configure test (that contained a rouge syntax error)](https://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2bbb81307644efdb58db2c422d9ba7) could be written via Meson template configure tests as:

cc = meson.get_compiler('c')
if (
cc.has_function('prctl', prefix: '#include <sys/prctl.h>')
and cc.has_header_symbol('sys/syscall.h', 'SYS_landlock_create_ruleset', prefix: '#include <linux/landlock.h>')
and cc.has_header_symbol('sys/syscall.h', 'SYS_landlock_restrict', prefix: '#include <linux/landlock.h>')
and cc.has_header_symbol('linux/landlock.h', 'LANDLOCK_CREATE_RULESET_VERSION')
)
# ...
endif

The templates are well-tested as part of the build system, so if the `prefix:` has no syntax error (and the spelling is right) I'm confident these checks are sane. And IMHO this expresses what is being checked much better than an actual snippet of C/C++ code.

>
> If you get away without many configuration tests in your project then this
> is because what (e.g.) the compiler gives you, in the form of libstdc++
> for example, abstracts away many of the peculiarities of a system. But
> in order to be able to do that something (namely the config system of
> libstdc++) needs to determine what is or isn't supported by the system in
> order to correctly implement these abstractions. I.e. things you depend
> on did the major lifting of hiding system divergence.
>
> (Well, that, or you are very limited in the number of systems you support,
> which can be the right thing as well!)

We do get away with less than a dozen configuration tests, in part because we are fairly limited in what we support (GNU/Linux) and we use the C, C++ and POSIX standards quite heavily. But we're also an end-user application with a heavy preference towards bleeding-edge systems, not a low-level library that can run on 1980-something technology, so our experience might be a bit... unique. ;)

Thanks,\
-Jonathon