[Haskell-cafe] Safe Haskell?

Joachim Durchholz jo at durchholz.org
Thu Apr 22 19:23:56 UTC 2021


Am 21.04.21 um 18:39 schrieb Sven Panne:
> Am Mi., 21. Apr. 2021 um 15:57 Uhr schrieb Joachim Durchholz 
> <jo at durchholz.org <mailto:jo at durchholz.org>>:
> 
>     Actually Sandboxes have a just as complicated attack surface as
>     languages.
> 
> 
> Perhaps we are talking about different kinds of sandboxes: I'm talking 
> about sandboxes at the syscall level. Compared to higher level stuff 
> they are relatively simple. They are not small (tons of syscalls exist 
> nowadays), but each syscall is relatively simple compared to stuff built 
> upon it. Size != complexity.

True, but the semantics behind each syscall can be horrendously complex.
The syscall for opening a file is straightforward, but its semantics 
depends on a ton of factors, like whether it's in /proc, on a ramdisk, a 
remote file system, and the semantics of whatever filesystem is plugged 
behind.
That's why you can have a sandbox and this still doesn't protect you 
from symlink timing attacks on /tmp, for example (wait for a system 
process to create a temporary file, delete the file and insert a symlink 
to a file of your choice - will work with a low probability if there's a 
window between file creation and file opening, that's why mktemp and 
friends were built to make this atomic, and we hope(!) that all 
security-relevant programs have been updated to use that instead of 
creating a temp file to open it later).

> And the SW engineering point is: You 
> program your sandbox SW once and can use it for every language/program, 
> without any need to follow the latest and greatest hype in languages, 
> compilers, type systems etc.

Instead you have to make sure that all software uses mktemp instead of 
doing nonatomic file creation&opening.
Which isn't secure by design, you still have to double-check all software.

> This means a *much* higher ROI compared to 
> a language-specific solution. Of course you won't get a PhD for a 
> syscall sandbox...

Except that there is no such thing as an inherently safe syscall 
interface, there are unsafe ways to use it.

And that's where language-based safety can help.

>     I also believe they are different domains.
>     Secure languages deal with making guarantees about what a program does
>     and, more importantly, what it does not do. So you can control things
>     like IO effects, Capabilities, and the language can even make these
>     guarantees statically.
> 
> Only if you trust your language itself, your implementation and its RTS. 

Again, trusting an operating system isn't a smaller surface.
After all, you need to trust the OS as a whole, namely that all the 
syscalls do what they're supposed to do.

Not a win, actually. Operating systems are vastly more complex than 
language runtimes.

It's even stronger.
The compiler code to emit useful error messages is complex, because it 
needs to infer the programmer's intentions.
The compiler code for type inference is pretty complex as well - it 
ranges from pretty easy for pure Hindley-Milner typing to arbitrary 
complexity for dependent types and other things.
However, the code that checks if the program type checks after all type 
inference has been applied - that's actually trivial. It's the moral 
equivalent of a proof checker for first-order predicate logic, and 
that's something that you can do at university (I actually did such a 
thing for my Diploma); *finding* a proof is hard but *checking* one is 
trivial, and that translates to type systems and things like Secure Haskell.

There's still another challenge: Set up a system of axioms, i.e. 
security properties and how they apply when going through the various 
primitives, and find a set that's (a) interesting and (b) understandable 
for programmers.
This is pretty hard to do for syscalls, because their semantics was 
never designed to be easily provable. It's easier to languages if you 
restrict syscall use to a safe subset (for whatever definition of "safe" 
resp. for what set of security-relevant properties you're interested 
in). Such a restriction is good for an easy first prototype, but it 
falls flat as soon has you allow arbitrary syscalls, or linking 
arbitrary libraries.
So for system security regardless of what programming language people 
are using, you probably need something at the syscall level - sandboxes, 
jails, cgroups, whatever else. None of these are perfect - kernel bugs 
still exist, and some syscalls have well-known security issues - so you 
*still* need defense in depth.

> And given the complexity of each of these aspects, basically any 
> language has security holes (see e.g. the recent discussion here about 
> various GHC bugs). A sandbox has a much easier job, it can e.g. always 
> err on the safe side.

No sandbox does that.
Well, the new ones do. Then people start demanding additional features, 
and off you go with new features, new bugs, and new unforeseen interactions.
I'm speaking from experience with the JVM sandboxes, which is an 
in-language mechanism but the problems I saw there could happen 
regardless of whether the sandboxing is done inside a runtime or at the 
syscall level.

>     A sandbox deals more with API objects. This is a much more complicated
>     surface because today's APIs tend to be large, complex, and interact in
>     unexpected fashions; on the other hand, it is much nearer to the actual
>     objects being protected.
>     I.e. I believe the two approaches, while they have some overlap, they
>     serve different purposes and need to complement each other.
> 
> 
> A syscall sandbox needs no help whatsoever from a language.

Well, I hope I just argued why a syscall sandbox is never going to be 
complete.

An additional argument: syscall sandboxes can't prevent data 
exfiltration. There are just too many channels.
And in-language security check could prevent exfiltration of user 
passwords (with the right axioms and inference rules).

>     I believe the opposite is true.APIs change over time.
> 
> The syscall interface is extremely stable, at least compared to the rest 
> of all APIs in existence.

But first-order predicate logic is immutable, and that's all you need 
for a checker.
(The axioms and inference rules can depend on the API, but that's just 
the same as with syscalls.)

BTW syscalls aren't immutable. They're backwards-compatible.
That's irrelevant for a programmer, but it is a world of difference for 
security - a programmer is interested in what can be done, security is 
interested in what can *not* be done, and this changes the picture 
completely.

>     Languages do that, too, but to a much lesser
>     extent, and type system guarantees tend to hold for decades.
> 
> There is no such thing as a type system in the machine code actually 
> running, so you can't trust any higher-level guarantees.

You can't trust the compiler, you can't trust the runtime, and you can't 
trust the kernel.
There's really no big difference there. You just happen to know the ghc 
loopholes better than the syscall loopholes (for me it's the other way 
round).

>     Even filesystem APIs are less stable than that (think NFS, or
>     filesystem-dependent ACLs).
> 
> 
> Do you really see the filesystem implementation on the syscall level? 
> Unless you're doing serious fcntl()/ioctl()-Kung-Fu or stuff like that, 
> I don't think so, but that's just a guess.

No, it's much worse.
Some syscalls are atomic on a local filesystem and nonatomic on NFS. So 
if you're writing security-relevant code, you have to know whether 
you're on NFS or not, and I'm not even sure if you can reliably detect 
if the file you're trying to create&open is local or NFS.
Then there's tons of interesting games you can play with symlinks and 
hardlinks, further complicated by questions like what happens if a 
symlink is cross-filesystem (it may well be that all atomicity 
guarantees go out of the window). Do you even write your code in a way 
that you detect if a physical file was turned into a symlink behind your 
back? Even if your code is running inside a VM and might be subject to 
suspend/resume? (Cloud management software like OpenShift does things 
like suspend your VM, move it do a different hardware, and resume; 
filesystem boundaries can indeed change, and it's easy to confuse 
OpenShift enough to trigger that "oh we need to relocate to another 
hardware" reaction).

Quite a while ago, attacks have started shifting from exploiting 
operating system bugs to exploiting application programmer expectations 
and making these fail.
And a syscall will do nothing to stop that. A language-based security 
framework cannot fully stop that, but it can catch another class of errors.

So I still stand by my assessment that the approaches don't replace each 
other, they're complementing each other and you should pursue both.

That's my 5 cents - YMMV.

Regards,
Jo


More information about the Haskell-Cafe mailing list