The way SpiderMonkey currently captures stacks for JavaScript Error
objects,
errors logged to the console, async function stacks, allocation profiling, etc.
causes the console service to leak windows, and fails to provide developer tools
with the control they need to present the stack appropriately for different
privilege levels. This proposal suggests a simplified, more efficient
implementation that solves these problems.
SpiderMonkey's SavedFrame
type provides an efficient and compact
representation for captured JavaScript stacks, currently used by Error
objects, async function stacks, allocation profiling, and Promise
debugging.
SavedFrame
has a number of valuable properties:
-
Per compartment, only one
SavedFrame
object is ever created for a given source location and caller chain, so repeated captures of similar stacks will automatically share a good deal of structure, makingSavedFrame
suitable for use in profilers and other instrumentation. -
SavedFrame
objects are always created in the compartment that requested the stack capture, not in the compartments of the frames they represent, so retaining aSavedFrame
stack does not prevent other compartments from being collected. -
SavedFrame
stacks always capture the entire JavaScript stack, regardless of the privilege levels of the individual frames.SavedFrame
presents or omits frames depending upon the privilege of the code inspecting them: privileged code sees the full stack; content code sees only the frames whose principals are subsumed by its own. This behavior is called 'filtering'.
Unfortunately, this design is not working out as intended:
-
The console service often retains stacks captured in content, to be displayed in case the developer opens the console. But the console is privileged code, and
SavedFrame
uses the accessing compartment's privileges to decide which frames to reveal. As a result, even though the console wishes to present the stack as content would see it,SavedFrame
presents the console with the full, unfiltered JavaScript stack, including any privileged frames. The console currently works around this problem by entering theSavedFrame
's compartment to inspect it, side-steppingSavedFrame
's attempt to discern the caller's privilege level. -
Since the stack of
SavedFrame
objects was captured by content, it occupies the content compartment. Only empty compartments may be freed, so by retaining the stack, the console service effectively prevents the content compartment from being freed. Of course, the console could copy theSavedFrame
chain into its own compartment, but doing so would break the technique described above, of obtaining the content-privileged view of the stack by entering its compartment to inspect it.
In other words, two major decisions in SavedFrame
's design are not working out:
-
SavedFrame
chains are built in the capturing compartment to simplify ownership questions; but this tactic leaks compartments. -
SavedFrame
carefully filters the stacks it presents based on the caller's privileges; but the console needs to view stacks using privileges other than its own.
Capturing JavaScript stacks in Firefox should have the following characteristics:
-
Captured stacks should be efficient in space and time, to support tools like allocation profilers that capture many stacks.
-
Code should be able to select the principals with which it inspects a stack: either its own principals, or any they subsume. For example, developer tools should be able to elect to view a stack either as content would see it (e.g. for Web developer facing tools), or showing all frames regardless of privilege level (for tools used by Firefox hackers).
-
Captured stacks should include all frames, regardless of the privilege level of the code requesting the capture. The set of principals we might use to view the stack is a dynamic property -- not statically known -- so filtering must occur when the saved stack is accessed, not when it is captured.
-
Captured stacks should not entrain compartments.
-
It must not be necessary for a compartment to still exist to inspect a stack according to the compartment's principals. That is, developer tools should be able to present the stack as content would have seen it, even after the content compartment is gone.
Revising the SavedFrame
implementation as follows would meet the requirements
above:
-
Frames should be de-duplicated to share common stack structure. As the code does now, when creating an object representing a saved frame, if an object already exists with the required properties (function name, source location, and so on) and the proper parent chain, that should be reused.
-
Frames should belong to no compartment. Representing frames as compartment-independent objects (and hence, not as JavaScript objects) will allow them to be shared between content and privileged code like developer tools, without requiring copies or prolonging compartment lifetimes. It sidesteps awkward questions about cross-compartment edges. It also allows each
Activation
'sLiveSavedFrameCache
to be more effective, since it need not be flushed when its contents are in the wrong compartment. -
A
SavedFrame
JavaScript object should be a reference to a compartment-independent frame object, together with a principal and any other parameters needed to determine how the stack should be filtered and presented. TheSavedFrame
's behavior should not vary depending on the privilege level of the accessing code. The current implementation's custom wrapper wrangling will no longer be necessary. -
SavedFrame
objects should provide a 'clone' operation that returns a newSavedFrame
object in the calling compartment, but referring to the same stack, filtered and presented in the same way as the original. The console service can use this to buildSavedFrame
objects in its own compartment that refer to stacks captured from anywhere, without preventing any other compartment from being freed, or affecting how those stacks are presented.This could be surfaced to JavaScript as a static method
SavedFrame.clone
, taking the constructor's compartment (or the method function object's compartment) as the destination for the clone. A non-static method would implicitly enter the originalSavedFrame
's compartment, making the intended destination less clear. -
SavedFrame
objects should provide an 'reveal' operation that returns a newSavedFrame
object in the calling compartment, referring to the same stack as the original, but using a new set of principals to filter the stack. The only difference between 'reveal' and 'clone' is that 'reveal' changes the filtering principals, whereas 'clone' leaves them unchanged. Exactly how the new principals are indicated would depend on the API being used:-
In JavaScript, this could be surfaced as a static method
SavedFrame.reveal(stack)
, which returns a clone ofstack
filtered according to the principals of the compartment to which the constructor belongs. (It might be simpler and almost always equivalent to use the compartment containing thereveal
method fetched from that constructor.)This would make it easy for privileged JS code to reveal a stack as viewed by the code's own principals, and for content to (un-)reveal a stack according to its lower privilege, if it somehow obtained a privileged stack. For privileged code to view a stack from some lower privilege level, it would need to obtain an unprivileged constructor, apply its 'reveal' method, and then perhaps 'clone' that back into its own compartment if needed.
-
In C++, the 'reveal' operation could be surfaced in the JSAPI as a method on
SavedStack
, taking aJSPrincipals*
as an argument.
-
You have a dangling sentence in the second point of "The Reappraised Requirements"