Levi Steenbergen Levi Steenbergen - 2 months ago 6
C# Question

Is the "when" keyword in a try catch block the same as an if statement?

In C# 6.0 the "when" keyword was introduced, now you're able to filter an exception in a catch block. But isn't this the same as a if statement inside a catch block? if so, isn't it just syntactic sugar or i'm missing something?

For example a try catch block with the "when" keyword:

try { … }
catch (WebException ex) when ex.Status == WebExceptionStatus.Timeout {
//do something
}
catch (WebException ex) when ex.Status== WebExceptionStatus.SendFailure {
//do something
}
catch (Exception caught) {…}


Or

try { … }
catch (WebException ex) {
if(ex.Status == WebExceptionStatus.Timeout) {
//do something
}
}
catch (WebException ex) {
if(ex.Status == WebExceptionStatus.SendFailure) {
//do something
}
}
catch (Exception caught) {…}

Answer

In addition to the several fine answers you already have here: there is a very important difference between an exception filter and an "if" in a catch block: filters run before inner finally blocks.

Consider the following:

void M1()
{
  try { N(); } catch (MyException) { if (F()) C(); }
}
void M2()
{
  try { N(); } catch (MyException) when F() { C(); }
}
void N()
{
  try { MakeAMess(); DoSomethingDangerous(); } 
  finally { CleanItUp(); }
}

The order of calls differs between M1 and M2.

Suppose M1 is called. It calls N(), which calls MakeAMess(). A mess is made. Then DoSomethingDangerous() throws MyException. The runtime checks to see if there is any catch block that can handle that, and there is. The finally block runs CleanItUp(). The mess is cleaned up. Control passes to the catch block. And the catch block calls F() and then, maybe, C().

What about M2? It calls N(), which calls MakeAMess(). A mess is made. Then DoSomethingDangerous() throws MyException. The runtime checks to see if there is any catch block that can handle that, and there is -- maybe. The runtime calls F() to see if the catch block can handle it, and it can. The finally block runs CleanItUp(), control passes to the catch, and C() is called.

Did you notice the difference? In the M1 case, F() is called after the mess is cleaned up, and in the M2 case, it is called before the mess is cleaned up. If F() depends on there being no mess for its correctness then you are in big trouble if you refactor M1 to look like M2!

There are more than just correctness problems here; there are security implications as well. Suppose the "mess" we are making is "impersonate the administrator", the dangerous operation requires admin access, and the cleanup un-impersonates administrator. In M2, the call to F has administrator rights. In M1 it does not. Suppose that the user has granted few privileges to the assembly containing M2 but N is in a full-trust assembly; potentially-hostile code in M2's assembly could gain administrator access through this luring attack.

As an exercise: how would you write N so that it defends against this attack?

(Of course the runtime is smart enough to know if there are stack annotations that grant or deny privileges between M2 and N, and it reverts those before calling F. That's a mess that the runtime made and it knows how to deal with it correctly. But the runtime doesn't know about any other mess that you made.)

The key takeaway here is that any time you are handling an exception, by definition something went horribly wrong, and the world is not as you think it should be. Exception filters must not depend for their correctness on invariants that have been violated by the exceptional condition.

UPDATE:

Ian Ringrose asks how we got into this mess.

This portion of the answer will be somewhat conjectural as some of the design decisions described here were undertaken after I left Microsoft in 2012. However I've chatted with the language designers about these issues many times and I think I can give a fair summary of the situation.

The design decision to make filters run before finally blocks was taken in the very early days of the CLR; the person to ask if you want the small details of that design decision would be Chris Brumme. He used to have a blog with a detailed exegesis of the exception handling model, but I don't know if it is still on the internet.

It's a reasonable decision. For debugging purposes we want to know before the finally blocks run whether this exception is going to be handled, or if we're in the "undefined behaviour" scenario of a completely unhandled exception destroying the process. Because if the program is running in a debugger, that undefined behaviour is going to include breaking at the point of the unhandled exception before the finally blocks run.

The fact that these semantics introduces security and correctness issues was very well understood by the CLR team; in fact I discussed it in my first book, which shipped a great many years ago now, and twelve years ago on my blog:

https://blogs.msdn.microsoft.com/ericlippert/2004/09/01/finally-does-not-mean-immediately/

And even if the CLR team wanted to, it would be a breaking change to "fix" the semantics now.

The feature has always existed in CIL and VB.NET, and the attacker controls the implementation language of the code with the filter, so introducing the feature to C# does not add any new attack surface.

And the fact that this feature that introduces a security issue has been "in the wild" for some decades now and to my knowledge has never been the cause of a serious security issue is evidence that it's not a very fruitful avenue for attackers.

Why then was the feature in the first version of VB.NET and took over a decade to make it into C#? Well, "why not" questions like that are hard to answer, but in this case I can sum it up easily enough: (1) we had a great many other things on our mind, and (2) Anders finds the feature unattractive. (And I'm not thrilled with it either.) That moved it to the bottom of the priority list for many years.

How then did it make it high enough on the priority list to be implemented in C# 6? Many people asked for this feature, which is always points in favour of doing it. VB already had it, and C# and VB teams like to have parity when possible at a reasonable cost, so that's points too. But the big tipping point was: there was a scenario in the Roslyn project itself where exception filters would have been really useful. (I do not recall what it was; go diving in the source code if you want to find it and report back!)

As both a language designer and compiler writer, you want to be careful to not prioritize the features that make only compiler writer's lives easier; most C# users are not compiler writers, and they're the customers! But ultimately, having a collection of real-world scenarios where the feature is useful, including some that were irritating the compiler team itself, tipped the balance.

Comments