19 comments

[ 29.2 ms ] story [ 705 ms ] thread
This is really interesting in terms of mitigating trusting trust attacks.
Maybe?

You still have to trust the original compiler, and you still can't rely on reading the source.

You just have to hope that the simple compiler is simple enough to read at the machine code level, and trust your file system / hex viewer / text editor.

No, it’s possible to fully mitigate it. See https://dwheeler.com/trusting-trust/
that assumes the other compilers aren't compromised.

lets say gcc is compromised.

if it detects its compiling a compiler it inserts a back door routine, if it detects it's compiling an os, it inserts a backdoor, if it detects its compiling a file system it inserts code to hide that back door and prevent it being shown to the user, when a user accesses those files.

if you compile tinycc with gcc, youve lost. if you compile tinycc with anything thats been compiled by gcc youve lost.

you arent going to detect it by looking at the source code, you arent going to detect it looking at the binary, you would have to look at the running executable, and hope that the attackers didn't take my example one step further.

you basically need to make your own trusted stack. but then anyone else that uses that stack has to trust you.

You can't fully mitigate it, all you can do is trust that all the people building their own trusted stacks aren't in on it, and are competent. But then we're back to trust again.

I assume you’ve skimmed the paper, because it addresses and mitigates exactly the scenario you’re describing. I can understand if you assume that it’s impossible to achieve an uncompromised system from a totally compromised one, I did the same mistake at first. The solution really seems like a magic trick.
> if it detects its compiling a compiler it inserts a back door routine,

That's going to be hard.

If you're compiling yourself, sure, you know how the compiler is structured and what the source code looks like, so it's easy to spot the point to add the hidden backdoors to.

But inserting the backdoors into an arbitrary compiler? That may be structured differently, or use a completely different set of intermediate representations? Or even just uses `camelCase()` instead of `lower_underscores()` for function naming?

I'm not sure a generic backdoored compiler would be an option. I think you'd need separate payloads for, say, gcc and tinycc. But then the problem becomes, how do you add a new backdoor for a compiler that was released after yours, without anyone noticing? If you do a 3-stage bootstrap of GCC, the end result will be noticeably different if someone started with a pre-new-backdoor binary compiler than with a post-new-backdoor binary compiler.

The simple compiler mes/stage0 is using is ~300 bytes and comes with commented assembly source, so it should be, yeah. Then there’s a chain of progressively more capable ones (the next couple are still commented machine code, but at least you don’t have to poke at them with a hex editor).
This was really interesting, I (being a C buff, beard and all) love this kind of low-level hacking to get critical infrastructure to work on a platform. Many of the gotchas they experienced seemed familiar (such as char's lack of signed-ness) from helping beginners with the language, too.

It was surprising that the OP calls C's shift operators (<< and >>) rotate, especially surprising in the context of low-level/assembly programming where rotate is very often a separate instruction. A rotate will bring bits "around", while a shift just inserts 0s or sign bit copies.

A shift is basically a rotate and mask. If wonder if they came from a machine where there was no shift instruction?
Not for the signed ("arithmetic") case when you need to duplicate the sign bit into the topmost bit. You can't replicate that with a rotate, it's a distinct operation.
No but if this hypothetical instruction set doesn't even include shifts, it wouldn't be surprising if it missed other instructions.
Note that this specific aspect discussed here isn't about getting things working as such. Cross-compilation of RISC-V has been absolutely trivial for many years. For example, support for riscv64-*-linux-gnu architectures was added to build-many-glibcs.py more than five years ago: https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=0fd5d... This gives you ABI-verified cross-compilers for GNU/Linux. Other similar scripts exist, and may even build more complete sysroots.

The difference to the approach referenced here is that build-many-glibcs.py and similar efforts start out from a native GNU toolchain (contemporary GCC with the C and C++ front ends, binutils, GNU make, and so on) plus some extensions (such as Python). Most people already have such an environment and trust it to some degree, so there is no need to bootstrap off some minimal seed.

When I look at that and the bootstrapping process, I am still sad at one of the biggest mistake in open source software: switching gcc to c++.

To a lesser extend, letting the army of gcc extensions being required to compile a linux kernel is not that far away...

I still think that developers (both of gcc and linux kernel) should be free to choose the tools serving them better.

The opinion of us, non-paying users, is irrelevant.

Giving work away for free means no obligation, not immunity from all criticism. In this case, GCC using c++ and Linux using C extensions are both choices with real trade-offs in both directions, and pointing out the downsides is a perfectly legitimate thing to do.
GCC switched to C++ for the same reason Linux uses C extensions; the base language doesn’t adequately support the target problem domain.
The problems are c++ and C gcc extensions now.
You are allowed to think that and in some ways they are, but both exist due to problems in C itself.

Lack of interest in evolving C at a reasonable pace has left the standard catching up to the implementations where there is no such ideological battle to implement a feature. The attribute situation in C is a great example of this.

I do disagree, and actually that type or reasoning is, on the long run, leading straight to significant planned obsolescence and vendor/developer lock-in once tools get of significant complexity and size (which is the de-facto case with c++ and similar due to the inherent complexity of their syntax). This is no ideological battle, but a sanity/toxicity, and in some context, strategic battle. But I concede that it looks like an ethical battle from far away.

And I agree on the fact that C is bad, but much less worse than c++ and similar, plain and simple C (without the latest ISO tantrums) is still the less worse compromise.

And if something had to be done with C, it was the "other way around": only 1 loop statement (loop{}), no integer promotion, no switch enum typedef (and the filthy _generic and typeof), no implicit cast except from literals (see rust and others) and void*, hard compiler constant declarations, fix for good the variable argument list macro, no anonymous code block (but ok for union and struct), only sized primary types (u8/s8...u64/s64...f32...), and all those pesky little things I forget.

We did reach a state in our domain where "innovation" is actually not "adding and complexifying" (unless Big Tech, since this is their bread and butter), but "removing", and the really hard part, then, to keep it that way.