I wanted to talk a little bit about securing C code, this came up a few weeks ago in a friendly twitter discussion and I said I would write something; so here it is.
Securing C is not an easy thing to do generally but we had pretty good success in Project Lightspeed. Now please don’t consider this a pwnme challenge: it’s not that. Nobody needs that in their life (see this if that excites you). But I think we ended up in a pretty good place and it is C, so I thought it would be worth it to discuss how we got to where we got, and why it’s better than usual for C. I think a lot better than usual.
For starters, it’s not what you would call idiomatic C. I think we all know that the sort of K&R style C patterns or anything like that just end you up at a very bad place. We’ve all seen it. But I think what we did works very well, and it still has good performance and size characteristics. This is the secret sauce if you will. It’s not magic. The elements work together so read through the list and see how they synergize before you get too skeptical of just the first or second bullet.
Use Basic Types
- Use a standard memory management system rather than malloc/free directly. We used retain/release just like it is in CF. In fact on iOS it really is CF.
- Use standard primitives for the base types like String, Blob, Number (again CF provides these for us on iOS; we use something isomorphic elsewhere)
- Use a small number of well tested collections and nothing else for storage, we have List, Dictionary, and uh, List.. Dictionary… List… yeah. You can do a ton of stuff with just a few data structures
- Verify your arguments ruthlessly on entry/exit
- Leave that code in production
The entire system is basically looking for any excuse to fall on a sword. Because of this API calls internally end up being super clean and if anything were to start to go off the rails everything in sight is prepared to just crash before things go bad. This keeps us from getting into weird bad states that we can’t understand or debug.
A lot of times trying to be resilient just gets you in trouble. Failfast works great for a mobile app because (e.g.) nobody is going to DDOS a phone and crashing is far better than getting pwnt.
- Get to 100% line coverage at least [we were inspired by SQLite]
- Run those tests under ASAN/NullSAN/LeakSAN/OMGAllTheSans
- Add fuzzing as needed, bake it right into the unit tests.
To do the above you will need to be able to mock your key systems, like for us that meant the network layer and SQLite. We have a FakeSQLite thing that can emulate any successful or failed return codes with no database at all, or it can shim the real SQLite and mutate the values a bit. We use this to force all the failure paths to run (e.g. any/every prepare can fail) and to “fuzz” failures.
When “fuzzing” a test that is supposed to succeed we have lots of built-in invariants that have to keep working even though you’ve forced it to fail. For instance, if you inject a failed statement prepare then the test that was supposed to pass better report that it failed but it also should not leak, it should not fail to finalize any SQLite statements, and so forth.
The test infra does simple things like keep track of retain/release counts so that tests automatically fail if the retains don’t balance the releases in any given test. This isn’t quite as good as ASAN but it is very good at catching things in our system and it runs always. Combining the two is very helpful.
Get Third Party Audits
Much as we’re careful we’re limited by our own imagination sometimes. Getting additional eyes on there is very helpful, and while I’m really happy with how we did I’m also grateful for the suggestions to improve. I’m sure your code will benefit from such an audit too.
That’s pretty much it. Simple types, no pointer stuff. Arg checking everywhere and 100% test coverage. Tests run under ASAN etc. The result was I think very good indeed. Bug count per line of code in this codebase is generally very very low.
It doesn’t look like K&R C at all but it’s still small and fast. It’s not full of crazy macros or anything like that. Just lots of standard object pointers and retain/release calls.
I can’t say I recommend this for everyone, but if you want to go in this direction, this gives you an idea what the ante looks like to play. And it really is only that — ante. I don’t think Rust should be running scared, but I did think this was an interesting data point.