Modifying a Stack Walk

for RuBoard

So, say the author of MyDatabase comes along and, having read all about good security practices, rewrites BackupDatabase to be secure. The filename parameter is removed and the backup is written to a known location controlled solely by the MyDatabase assembly, with no input from external sources.

The security demand will still fail, if the caller of BackupDatabase is untrusted to write whatever file the underlying implementation is using. This is probably not the intended effect; the author of BackupDatabase wants to write the backup on behalf of the user, even if that user is otherwise untrusted. This is where the stack walk modification operators come in.

These operations ( Assert , Deny , and PermitOnly ) are methods defined on all the permission classes, as well as PermissionSet . When called, they attach their permission or permission set to the current method's stack frame in such a way that a subsequent stack walk will notice and process them. They are removed automatically when returning from the method to whose frame they are attached (including when the frame is unwound due to an exception being thrown). It's also possible to remove the attached permissions prematurely using the RevertAssert , RevertDeny , RevertPermitOnly , and RevertAll methods.

The most useful of the operations is Assert . Let's use it to solve the problem introduced in the last section. Assert allows a stack walk that has successfully traversed to the frame on which the assertion is attached to terminate early (with success). That is, it declares that further callers need not be checked (in essence, the current method vouches for the trustworthiness of its callers ). Assert is a dangerous operation, and it is up to the code making use of it to either determine the trust level of its callers or ensure that no security risk is implied regardless of their trust level. When security reviews of code are being performed, calls to Assert are usually a good place to begin looking for holes.

Let's look at the modified code for BackupDatabase :

 public void BackupDatabase() {     const String filename = "c:\MyDatabase\WorkArea\Backup.dat";     new FileIOPermission(FileIOPermissionAccess.Write, filename).Assert();     FileStream stream = new FileStream(filename, FileMode.Create);     // Code to save database to newly created file... } 

When the security demand stack walk reaches the frame for BackupDatabase , the following steps will be taken:

  1. The permissions for the MyDatabase assembly will be checked against the demand. It is important to do this before processing the assertion because a security hole is opened otherwise (to put it another way, the security system needs to check that you have the permissions you're attempting to assert before it allows you to assert them).

  2. The security stack walker notices an assertion attached to the current method frame. It retrieves the permission associated with the assertion (in this case, a FileIOPermission for write access to the database backup file) and checks whether this satisfies the demand. If so, as in this case, the stack walk is halted immediately and successfully (no security exception is thrown).

This now makes BackupDatabase usable by anyone , regardless of his or her trust level.

Note that an assertion does not always terminate a stack walk. If the assertion is for an entirely different permission (or set of permissions) than the demand being made, it will be ignored completely. Likewise, if the assertion does not fully satisfy the demand, the overlapping portion will be removed from the demand and the stack walk continues. For example, say a demand is made for FileIOPermission , including write access to c:\a.dat and c:\b.dat , and a frame on the stack has an assertion for FileIOPermission write access to c:\a.dat . When the stack walk has reached the assertion, it will remove the asserted permission from the demand, but continue on to the next frame, now demanding FileIOPermission with write access to c:\b.dat . Assert can be looked at as taking a bite out of the demand on its way down the stack. If that bite leaves nothing of the demand behind, the walk terminates. Figure 7.5 illustrates the effect.

Figure 7.5. The effect of Assert on a security stack walk.

graphics/07fig05.gif

Deny can be thought of as the opposite of Assert . Whereas Assert causes (or can potentially cause) the early termination of a stack walk with a successful outcome, Deny will cause early termination of the stack walk with a security exception. That's to say, Deny ensures that any security demand for the associated permission or permission set that would get as far as checking your caller will result in a failure, regardless of the trust level of the caller in question.

In practice, Deny is rarely a useful operation; any assembly denied access to a resource in this fashion is either granted the permissions to make the request directly anyway or, in the absence of such permissions, would be denied access by the security system in the normal fashion. An area where Deny commonly does prove its worth is in testing ( especially when adding new security permissions). Take the following example, which assumes a new permission type, MyPermission , has been implemented:

 // Force a security stackwalk for an instance of MyPermission. public static void InvokeDemand(MyPermission permission) {     permission.Demand(); } // Test demands on MyPermission. public static boolean TestMyPermission() {     MyPermission permission = new MyPermission(PermissionState.Unrestricted);     // Assuming this test program has been granted unrestricted MyPermission,     // perform a positive test case first.     try     {         InvokeDemand(permission);     }     catch (SecurityException)     {         return false;     }     // Now try a negative test.     permission.Deny();     try     {         InvokeDemand(permission);         return false;     }     catch (SecurityException)     {      // We expect a security exception in this case.     }     return true; } 

We use Deny in this negative test case (that is, testing that MyPermission correctly throws a security exception when demanded in a situation where it is not granted). Deny is convenient for this purpose because it allows the set of permissions the assembly is granted to be effectively modified at runtime (at least from the point of view of the current thread). This allows us to place the positive and negative test cases within the same assembly.

It is reasonable to ask here whether the test is valid, given that a security demand that fails due to a Deny would seem to be a different scenario from a demand failing due to a constrained grant set. In actual fact, the difference between the scenarios is negligible to the implementer of a custom permission; the same interface methods are called with identical arguments in each case. The only real difference in code paths and algorithms is within the security system itself, which is outside the scope of testing MyPermission .

Note that we encapsulated calling Demand on MyPermission within a method ( InvokeDemand ). This choice was not arbitrary and, in fact, was necessary for the success of the test. Recall that a security stack walk will skip the frame of the method that initiated the demand (because making a demand against your own method is usually a worthless operation). If the frame that is skipped contains a stack walk modifier ( Assert , Deny , or PermitOnly ), that modifier is skipped as well. So if we'd made the demand on MyPermission directly from TestMyPermission , the Deny would have been skipped, and the stack walk would not have thrown a security exception.

The last of the stack walk modifiers is PermitOnly . This works in a negative fashion akin to Deny in that it attempts to terminate a stack walk early with a security exception. Whereas the permission or permission set associated with Deny indicates permissions that are refused and will cause an exception, the permissions associated with PermitOnly must satisfy the current demand to prevent a security exception being thrown. That is, the stack walk will proceed only if the demand can be entirely satisfied by the permissions associated with the PermitOnly .

PermitOnly is semantically close to Deny (it's really just an alternative way of specifying the accompanying permission set). So, similarly, it has little real use (beyond the testing scenario previously presented).

A NOTE ON INLINING

Because the .NET Framework security system depends so heavily on the structure of the stack for its correct operation, it's reasonable to ask whether compiler optimizations could lead to a security hole. The chief worry here is a technique known as inlining. Examining why this is not an issue will provide further insight into the workings of the security system.

Inlining is the process whereby a compiler copies the implementation of a method into a caller in lieu of actually making a call to that method. This is an optimization that trades off the speed gain realized by avoiding the overhead of making a method call versus the additional memory costs of making multiple copies of the same piece of code.

In the .NET Framework runtime, inlining is performed by the JIT when compiling a method's IL instructions into native code. The JIT is fairly aggressive and may collapse several levels of call into a single linear piece of code.

On the face of it, inlining may seem to present a problem to the security system. An inlined method no longer has its own frame on the stack, so it effectively disappears from any subsequent stack walk. This would allow a single untrusted method on the stack to disappear from sight if inlined into its trusted caller, for example.

In actual fact, the .NET Framework security system is careful to ensure that inlining does not compromise the safety of the system in this way. As previously discussed, there are two principal pieces of data the security system is searching for when walking the stackassemblies (for their grant set) and stack walk modifiers.

When determining which assemblies are represented on the stack, the actual methods called are irrelevant and can be ignored. If a chain of three methods, all from assembly MyAssembly , are present on the stack, we determine whether MyAssembly has sufficient permissions when we encounter the first method, but can ignore the other two methods (for the purposes of assembly grant set checks). It would be pointless to check the same assembly grant set for compliance multiple times; such grant sets, once computed, are immutable.

From this it follows that only inlining that traverses an assembly boundary (where the caller and callee are in different assemblies) is interesting from an assembly grant set perspective. If the caller and callee are from the same assembly and the callee is inlined into the caller, the caller still contains the correct information for a valid security check.

The .NET Framework security system prevents the cross-assembly case from posing a security risk through the cooperation of the JIT. A simple way of ensuring safety would be to disable cross-assembly inlining altogether. However, the JIT can do better than this; it will allow a cross-assembly inline where the callee is completely trivial (defined here as the callee having no further method calls within its implementation). This allows the optimization of a large class of simple methods (most notably, property accessors) without compromising security. The algorithm applied by the JIT in future versions of the runtime might change and should not be relied on (for example, later versions of the JIT may be able to further refine the definition of "trivial callee" by differentiating calls made to security methods versus intraassembly calls).

We still have the problem of stack frames that introduce one or more stack walk modifiers ( Assert , Deny , or PermitOnly ). We can't allow any methods that introduce one of these operations to be inlined, because we lose the stack frame to which they're attached. We can't attach the operation to the caller's frame because this would affect the semantics of the modifier (recall the earlier Deny code example, where we relied on the demand being made from a different method than the Deny ).

Instead, the security system again relies on the cooperation of the JIT. Because there are a limited number of method calls we care about (the Assert , Deny , and PermitOnly methods on the PermissionSet and CodeAccessPermission classes), the JIT simply "special cases" these and will refuse to inline any method that makes a call to one of them. This works because none of the methods are virtual and can be detected statically at JIT time.


for RuBoard


. NET Framework Security
.NET Framework Security
ISBN: 067232184X
EAN: 2147483647
Year: 2000
Pages: 235

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net