Security and Crashing with Modal Logic

December 17, 2023

Modal logic is a fascinating field which has been applied to computer science in all sorts of weird ways. In this post I'll talk about an interpretation of modal logic that I've been exploring which gives type-theoretic interpretations to crashing and security.

This post assumes a familiarity with logic, and a very vague familiarity with monads, comonads, and the Curry-Howard correspondance. Also, I need to say from the start that this post is not very rigorous; it's meant as interesting food for thought and intuition.

What's Modal Logic?

Modal logic is a version of logic that has two new and fascinating operators, Possibly and Necessarily. These operators work exactly as you'd expect, but it's hard to say exactly what it is that we expect, so I'll simplify the idea. Consider the python program below:

# this is equivalent to print(input() + 4) 
# but the variables make it easier to talk about
x = input()
y = 2 + 2 # always evaluates to 4
z = y + x # depends on user input
print(z)

When a program is run, it has some set of inputs that change how the program progresses to completion. In this case, it's the value assigned to x. For a given run of the program, it only has one concrete input, and only unfolds in one way. If you enter 3, the program calculates and prints 7. But there are other ways it could have gone, if the original input were different. It wouldn't have printed 7 if the input were, say, 5. And there are some parts of each run of the program that are the same no matter what the inputs are, like the value of y. Framed in modal logic, we'll say that z is possibly 7, and y is necessarily 4. That's the understanding of possibility and necessity we'll use for this post. A vocabulary for talking about different runs of a given program. Regular propositions are for the "current run," necessary ones are true in all runs, and possible ones are true in some runs, which could be the current one but might not be.

In logic, "\(P\) is possible" is written \(\Diamond P\) and "\(P\) is necessary" is written \(\Box P\).

First of all, it's worth taking a moment to appreciate the beauty of modal logic. Possibility and necessity are dual in the same way conjunction and disjunction are, or universal and existential quantification are. That is, take some proposition \(P\). \(\neg (\Diamond (\neg P))\), or "it is not possible that \(P\) is false," is equivalent to \(\Box P\), or "\(P\) is necessary." \(\neg (\Box (\neg P))\), or "it is not necessary that \(P\) is false," is equivalent to \(\Diamond P\), or "\(P\) is possible." That is, negating the inputs and outputs of one gets you the other, just like conjunction/disjunction and just like existential/universal quantification.

They're also dual in a sort of category-theory way: possibility is a monad and necessity is a comonad. Showing why is a nice demonstration of the modalities, so here it is: for any proposition \(P\),

\[P\implies\Diamond P\]
\[\Diamond (\Diamond P)\implies\Diamond P\]
\[\Box P\implies P\]
\[\Box P\implies\Box (\Box P)\]
\[\]

You can see in these rules the monadic "return" and "join" operations and comonadic "extract" and "duplicate" operations. It's also worth going through them and intuitively checking if they make sense.

Security

If you're familiar with language-based security theory, you might immediately see a use for this. LBS is largely built around a concept called noninterference, the property that secret inputs don't interfere with less-secret outputs. It's a hyperproperty, meaning that it's talking about all possible runs of the program instead of just a few particular ones. Sound familiar? In our modal vocabulary we might say that public outputs are necessary with respect to secret inputs; they aren't affected in any run of the program by those inputs.

This is a little bit different from how LBS is usually studied. To oversimplify, there's usually a Secret monad that ensures the values it holds can only affect other Secret values. In the modal language, however, we're talking about a Public comonad that makes sure the held value only depends on Public values. This flipping of perspective has been studied a bit, most notably by Choudhury. He takes inspiration from a system by Davies (that is explicitly inspired by necessity in modal logic) that is used to determine which expressions in a language don't depend on user input, and can therefore be evaluated at compiletime as an optimization. This is like noticing that y is necessarily 4 in the python code above, and removing the addition. Choudhury extends this idea for language-based security, combining it with the Secret monad I just mentioned.

Crashing

Consider for a moment what it is that an assert statement does in a program. Hopefully it does nothing! It has some proposition that it expects to be true in the current run of the program, and it crashes if it doesn't. We can describe this naturally in our modal vocabulary: it takes a proposition that's possibly true (true in some runs of the program) and "proves" that it's true in the current run by crashing if it's not: \(\Diamond P\implies P\). This is a weird sort of implication because it's not generally true: not all things that are possibly true are actually true. It's like an "extract" operation for monads: it doesn't generally exist. (Side note: Rust's unwrap operation for the Result monad comes to mind.) Therefore it's an implication that you don't want to be applying unless you really think it's true for the proposition in question.

We talked a lot about duality in this post so far, so just for some informal fun, what would the dual of an assertion be? If an assertion is an implication \(\Diamond P\implies P\) then its dual would be an implication \(P\implies\Box P\), which also doesn't exist in general. What does it mean? If \(P\) is true in this run then it's true in every other run. This is obviously not true in general but we can imagine that it works like assertions, simply crashing the program when it's false.

This is a weird operation: take some fact about the current run of the program, and crash the program if it's not true in every other run of the program. Is there anything like this in computer science? There actually is, surprisingly enough, and big corporations like Amazon are using it to make a ton of money. It's called dynamic taint tracking. The idea is you "taint" certain inputs at runtime which may give sensitive information, then you taint every value that is derived from the inputs, then if a tainted value is about to be printed in a public way, the program crashes. This can easily be seen in terms of the Public comonad I mentioned earlier: dynamic taint tracking takes a value and coerces it to be Public, and thus publicly viewable, crashing if the value depends on sensitive information. That is, the program crashes if the value isn't necessary with respect to the secret inputs.

Conclusion

That's all I have to say about this stuff for now but I hope to keep exploring modal logic as a language for security and crashing. I care a lot about crashing because I like the crash-only software philosophy a lot, and microreboot architectures, but I'm a type theorist and logician primarily. I have a crash-based project I'm working on that I'll hopefully post about soon. Also, if you're interested in how comonads are useful I've got to link this. I hope this was interesting!