Neither the compiler nor the runtime are required to guarantee that locals that are out of scope actually have the lifetimes of their contents truncated. It is perfectly legal for the compiler or the runtime to treat this as though the braces were not there, for the purposes of computing lifetime. If you require brace-based cleanup, then implement IDisposable and use the "using" block.
UPDATE:
Regarding your question "why is this different in optimized vs unoptimized builds", well, look at the difference in codegen.
UNOPTIMIZED:
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 28 (0x1c)
.maxstack 1
.locals init (class test.Foo V_0)
IL_0000: nop
IL_0001: nop
IL_0002: newobj instance void test.Foo::.ctor()
IL_0007: stloc.0
IL_0008: nop
IL_0009: call void [mscorlib]System.GC::Collect()
IL_000e: nop
IL_000f: call void [mscorlib]System.GC::WaitForPendingFinalizers()
IL_0014: nop
IL_0015: call string [mscorlib]System.Console::ReadLine()
IL_001a: pop
IL_001b: ret
} // end of method Program::Main
OPTIMIZED:
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 23 (0x17)
.maxstack 8
IL_0000: newobj instance void test.Foo::.ctor()
IL_0005: pop
IL_0006: call void [mscorlib]System.GC::Collect()
IL_000b: call void [mscorlib]System.GC::WaitForPendingFinalizers()
IL_0010: call string [mscorlib]System.Console::ReadLine()
IL_0015: pop
IL_0016: ret
} // end of method Program::Main
Obviously a massive difference. Clearly in the unoptimized build, the reference is stored in local slot zero and never removed until the method ends. Therefore the GC cannot reclaim the memory until the method ends. In the optimized build, the reference is stored on the stack, immediately popped off the stack, and the GC is free to reclaim it since there is no valid reference left on the stack.