[Mono-devel-list] CAS update / feedback

Sébastien Pouliot spouliot at videotron.ca
Mon Nov 1 09:44:40 EST 2004


Hello,

I added a (more complete) example at the end of the email.

> On 10/29/04 Sébastien Pouliot wrote:
> > Right now I'm thinking about dropping the JIT code generation for stack
> > modifiers (Assert, Deny and PermitOnly), i.e. everything that isn't a
> > demand.
>
> The last time I had a look at some CAS details was many months ago, so
> I swapped them out of my mind: what JIT code generation would you avoid?

Right now I generate code for six actions:
- Demand
- NonCasDemand
- DemandChoice (new in 2.0)
- Assert
- Deny
- PermitOnly

The first three cases trigger a stack walk (so code must be generated),
while the last three cases mark the stack (I think we can avoid code
generation for those).

> These should just set some sort of markers for the stack frame, right?

Yes - but I'd like to avoid them (when possible) because the stack is
(indirectly) already marked. The MethodInfo already knowns it has security
information (MethodBase.Attributes / MethodAttributes.HasSecurity).


> > (b) if the demand requires a stack walk (e.g. some permissions
> inherits from
> > CodeAccessPermission)
> > 	- use the execution stack for retreive all methods;
> > 	- check if methods have security attributes
> > 		- look if the items are cache for the stack modifiers;
> > 			- if not then create (and cache) the PermissionSet;
> > 			- keep pointers to the PermissionSet into
> MonoMethod/MethodInfo;
>
> It could be stored in MonoJitInfo in most cases. We need to deal
> with the case
> when the method is compiled as appdomain-neutral, but the
> permissing obj is
> appdomain-specifc.

I'll keep that a future email :-)

> > The extra difficulties are:
> > - add an internal API to get the declarative security data from
> the managed
> > side (right now it's push from the JIT/runtime into the managed world);
> > - to be able keep in order the execution stack (used for declarative
> > security) with the security stack (to be used for the
> imperative security);
>
> This may have to do with the code generation issue above. From
> what I remember,
> the security stack could be maintained in two ways:
> *) implicitly by having some data in the execution stack and letting the
> stack walk code find the data
> *) explicitly, by having a pointer in a thread local slot, with
> each new frame
> doing a push/pop at enter/leave.
> The first would be the most efficient, but I don't remember all
> the details
> to judge if it is sufficient to implement the semantics (re the
> CompressedStack,
> for example).

Agreed. This is why I use the existing informations (in the stack) for the
declarative security. It may also be possible to do so with the imperative
security but will require more modifications.

We cannot use (only) the first case (implicitly) because of the
CompressedStack. But we can treat the compressed stack as a separate case
(i.e. start with the "normal" stack and if the thread has a compressed stack
then evaluate it).


------ EXAMPLE -------

#3 was suggested by Paolo during the Boston meeting.
Right now we're are at #4.


static void CallUnmanagedCode (int i)
{
	// this *may* calls something unmanaged
}

[SecurityPermission (SecurityAction.Demand,
Flags=SecurityPermissionFlag.UnmanagedCode)]
static void DeclSecAssert (int i)
{
	CallUnmanagedCode (i);
}

static void ImprSecAssert (int i)
{
	new SecurityPermission (SecurityPermisionmFlag.UnmanagedCode).Assert ();
	CallUnmanagedCode (i);
}


In this example DeclSecAssert and ImprSecAssert are (almost) the same thing.


*** STEP 1 ***

The JIT generates code to replace the security attribute

[SecurityPermission (SecurityAction.Demand,
Flags=SecurityPermissionFlag.UnmanagedCode)]

into IL code (shown here as C#)

new SecurityPermission (SecurityPermisionmFlag.UnmanagedCode).Assert ();

Actually it's more complicated than this because:
- we must decode the declarative security information from the metadata
(where it is stored in unicode-encoded XML for Fx 1.0/1.1 but not for 2.0);
- there can be multiple permissions asserted (in a set);
- some permissions, like non CAS permisions, may not be able to Assert
(IPermission doesn't define Assert, IStackWalk does);


*** STEP 2 ***

So we need a (managed) method to decode a permission set (1..n permissions)
from the metadata - let it be managed and named "Decode".
Also the Assert method, of the PermissionSet class, must be wise enough to
handle the non-CAS permissions properly.

PermissionSet ps = Decode (...);
ps.Assert ();

But this is not optimal because PermissionSet.Assert must copy itself (the
PermissionSet) before attaching itself to the stack.
Why ? because in the imperative mode someone may have a reference to the
permission (or permissionset) and change it's value after it's been on the
stack (but before it gets evaluated).

So the following code isn't not optimal for declarative security as the
PermissionSet cannot be changed (no one outside the security manager has a
reference to it).

namespace System.Security {

	[Serializable]
	public class PermissionSet: ICollection, IStackWalk ... {

		public virtual void Assert ()
		{
			// check for SecurityPermissionFlag.Assertion
			// check that we have the asserted permission

			// set the frame with a copy of the PermissionSet (unrequired for
declarative security)
			CodeAccessPermission.SetCurrentFrame
(CodeAccessPermission.StackModifier.Assert, this.Copy ());
		}
	}
}


*** STEP 3 ***

The change is easy, another method is required to do the same job (as
PermissionSet.Assert does) but without the Copy part.
So the generated code becomes:

PermissionSet ps = Decode (...);
SecurityManager.InternalAssert (ps);

Now this is better but as we said in step 2 the PermissionSet cannot
change - they are created from the metadata.
Right now we would be re-creating a new PermissionSet object each time we
call this method (e.g. worst case is a loop).


*** STEP 4 ***

We can keep a cache of the "declarative only" permission set (e.g. the key
being their address in the metadata).
This cache can be handled transparently by the Decode method so the
generated code doesn't change (but other options exists).

PermissionSet ps = Decode (...);
SecurityManager.InternalAssert (ps);

Now we only create the permission set the first time the method is called.
But did we have to ?


*** STEP 5 ***

So far we had to add some code (e.g. Decode) and change the execution path a
little (e.g. InternalAssert) to get the declarative security attributes
working more efficiently. The next steps involves more drastic changes.

Many things could be done at this point. However all (interesting)
possibilities I see involves even more differences between declarative and
imperative security codes.  So let's not care, for a moment, for the
differences and try to see how (or if) they can be useful.

The point of my last email was that Assert (like Deny and PermitOnly) are
stack modifiers. Calling them has no impact until a stack walk is triggered
by any kind of Demand. If this has no impact maybe it could (or should) be
delayed...

So what happens if I change CallUnmanagedCode to...

[DllImport (...)]
static void PInvokeSomething ()

static void CallUnmanagedCode (int i)
{
	if (i % 1024 == 0) {
		PInvokeSomething ();
	}
	else {
		// e.g. we buffer the data so we
		// don't P/Invoke at every call
	}
}

The DllImport ensure that a "new SecurityPermission
(SecurityPermissionFlag.UnmanagedCode).Demand ()" will be called (which will
trigger a stack walk).

Now if we call this code thru ImprSecAssert, like:

static void Main (string[] args)
{
	for (int i=0; i < UInt32.MaxValue; i++)
		ImprSecAssert (i);
}

We will get:
* UInt32.MaxValue PermissionSet created (by the Copy);
* UInt32.MaxValue stack marks inserted;
* UInt32.MaxValue / 1024 stack walks;

Sadly there is not much we can do to optimize this scenario (but I'm very
open to suggestions :-).


Calling the same code via DeclSecAssert, like

static void Main (string[] args)
{
	for (int i=0; i < UInt32.MaxValue; i++)
		DeclSecAssert (i);
}

would result in
* 1 PermissionSet created (by Decode) by JIT generated code;
* UInt32.MaxValue-1 reused PermissionSet (by Decode) by JIT generated code;
* UInt32.MaxValue stack marks inserted;
* UInt32.MaxValue / 1024 stack walks;

That is much better than imperative (and indeed MS documentation suggest to
use declarative security for Assert, Deny and PermitOnly for better
performance).

However like I said Assert *DOES NOTHING* unless a stack walk is triggered.
So we're still "loosing" time to mark the stack UInt32.MaxValue -
(UInt32.MaxValue / 1024) as most calls will never reach a Demand. At this
time I'm unsure about the performance of stack marking (it depends on the
type of stack we'll be using/reusing) but anyway it will be > 0.

So the idea is that we already have two ways to populate the security stack
(declarative and imperative) so why not "create" two stack ?
Why ?
- well we don't really need a stack for declarative security (we already
have the execution stack and its permissions don't changes);
- anyway we have to create something for the ever-possibly-changing
"imperative" stack;

a) Do not generate code for Assert (nor Deny, nor PermitOnly);
	- Decode won't get called so the PermissionSet won't be created (first
time) or retrieved from cache (afterward);

b) When a stack walk is triggered
	- look for every method if is has security attributes (for Assert, Deny and
PermitOnly);
	- if so, then
		- Create (first time) or get from cache (afterward) the PermissionSet
		- Assert (Deny or PermitOnly) the PermissionSet
	- look if the imperative security stack has information on the current
frame
	- if so, then
		- Assert (Deny or PermitOnly) the PermissionSet

Now calling Main with DeclSecAssert would looks like:
* 1 PermissionSet created (by Decode) by the first stack-walk;
* UInt32.MaxValue / 1024 reused PermissionSet by subsequent stack-walk;
* 0 stack marks inserted;
* UInt32.MaxValue / 1024 stack walks;


Pros
* We get the "fast path" (as documented by MS) really the fastest path on
Mono too;
* We reuse the existing stack (as much as possible) as an alternate stack
would penalize performance;
* Software running at fulltrust (or with CAS turned off) would have *NO*
penality for using declarative Assert, Deny or PermitOnly (as they wouldn't
mark the stack);

Cons
* We need to add an internal API to get the declarative security data from
the managed side (right now it's push from the JIT/runtime into the managed
world);
* We still need to implement the other half (imperative) to match MS
implementation (we're not saving development time);
* We must be able keep in order the execution stack (used for declarative
security) with the security stack (to be used for the imperative security);

Notes
* Stack walks would be a little slower (a bit more job to do) but (worst
case scenario) wouldn't be slower than "really" applying stack modifiers
(i.e. the execution is done at a different time);
* Code generation is still required for Demand (NonCasDemand and
DemandChoice) so we still need to look at the metadata for methods/class
with declarative security attributes. But this can be a source for other
optimizations :-)





More information about the Mono-devel-list mailing list