...
The API call AccessController.getContext
also gives a view derived from the control stack. This view cannot omit the effects of historical frames, if the presence of those frames would affect access control checks. The information content of an access control context is a set of protection domains, each one derived from the class loader of a method in a pending frame. (There are other details, such as user-supplied protection domains and stack boundaries, but they are irrelevant to this discussion.) The set of protection domains is unordered and indifferent to repetitions. (That is, it is really a set.) The most important property of this set is that, if a stack frame is contributing to the current computation, the protection domain from that stack frame must be present in the set. Crucially, this must be true even of the historical frames C(1)..C(k-1).
Options for implementing access control tracking
Suppose the protection domains for the various frames C(1)..C(k) are PD(1)..PD(k). Each new call to C(k+1) must be inspected to see if it contributes a new
There are several basic requirements:
- Any protection domains that apply to P0 and its callers (and/or other surrounding context) must be retained through all execution of P0 and C(1)..C(k).
- For any recursive call from C(k) that performs access control checking, the protection domains {PD(1)…PD(k-1)} must also be available for access checks.
- During the same recursive call from C(k), PD(k) must also be available.
- At every tail call, a new call to C(k+1), the protection domain PD(k
...
- ) must be added to the existing set {PD(1)…PD(k-1)}.
Requirements 1 and 3 do not need any special provision, since they just restates the normal effect of pending frames on all normally recursive JVM calls.
Requirement 2 implies that there is a place where the set {PD(1), …, PD(k-1)} accumulates. (It is initially empty, during C(1).) Without tail calls, it would naturally accumulate as the pending stack frames C(1)..C(k-1) accumulate, but the precise effect of a tail call is that these frames are only past history.
Requirement 4 implies that the history actually accumulates in the provided place. Call this accumulated set A(k) = {PD(1)…PD(k-1)}. One simple way to do this is to detect when PD(k) and PD(k+1) differ; if they do not differ, then normal stack walking will detect the protection domain even after C(k) has been removed Thus, A(k) is the set of protection domains which is recorded during the execution of C(k), and which records the historical effects of the previous C(i).
These requirements can be weakened slightly by observing that A(k) does not need to contain a duplicate entry for PD(k), since C(k) already provides that protection domain. This observation leads to a simple, localized optimization. The last entry PD(k-1) can be held back from A(k), as long as PD(k-1)=PD(k). When setting up the next call to C(k+1), the held-back PD(k-1) must be added to A(k+1), unless PD(k)=PD(k+1), in which case the protection domain can continue to be held back. This implies that there can be a "fast path" for calls from C(k) to C(k+1), in the common case where the tail-caller and tail-callee are in the same protection domain.
(The current mlvm prototype detects "shifts" where PD(k)≠PD(k+1) and throws an exception. These exception paths are where summarization must take place instead.)
A likely place to maintain A(k) from the stack. Other optimizations are possible, with the normal application of "fast path" and "slow path" techniques. The collected set must be maintained somewhere. A likely place would be an extra hidden frame between the parent frame P0 and the first tail-called child C(2). Thus, when taking down C(1) during the first tail call, the hidden frame (initialized with A(2)) would be pushed above P0.
For optimized code which inlines a sequence of C(i), the compiler must record an equivalent to the A(i) sets. Again, this could be done by extra hidden frames.
It is likely that the sets A(i) will generally be small and stable through a long series of tail calls. The worst case would be a loop through a long list of nodes, each of which is defined in a distinct protection domain, an unlikely scenario. More likely is a user protection domain and one or more system-level domains for library types. In that case the loop will quickly stop adding new elements to the set. It is likely that a direct vector on the stack of protection domain references is an effective representation.
Another possibility is to keep the frames C(i) until some consolidation point is reached (say, a stack depth limit) and "compress" the stack when it overflows. The historical frames will need to be marked so that they can be hidden as required.
In the case of a megamorphic virtual or interface tail call, the check for protection domain shifting (PD(k)≠PD(k+1)) must be incorporated into the method dispatch. For monomorphic call sites, the check for shifting can be done once, at link time. In all cases, if the check detects a shift, there must be a slow path which locates A(i) stored nearby and adjoins the caller's protection domain to the set (if it is new to the set). There must also be an even slower path which creates a recording point if needed.
Further discussion of the problem of access control may be found in a paper by Clements and Felleisen, linked here: http://mail.openjdk.java.net/pipermail/mlvm-dev/2013-July/005417.html