Skip to content

Instantly share code, notes, and snippets.

@jimblandy
Last active December 4, 2019 06:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jimblandy/4f4f14d5a175f0106e1becee27437534 to your computer and use it in GitHub Desktop.
Save jimblandy/4f4f14d5a175f0106e1becee27437534 to your computer and use it in GitHub Desktop.
Non-Leaky, Privilege-Sensitive Saved Stacks

Non-Leaky, Privilege-Sensitive Saved Stacks

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.

Intentions versus reality

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, making SavedFrame 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 a SavedFrame 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 the SavedFrame's compartment to inspect it, side-stepping SavedFrame'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 the SavedFrame 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.

The Reappraised Requirements

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.

Proposed Implementation

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's LiveSavedFrameCache 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. The SavedFrame'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 new SavedFrame 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 build SavedFrame 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 original SavedFrame's compartment, making the intended destination less clear.

  • SavedFrame objects should provide an 'reveal' operation that returns a new SavedFrame 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 of stack 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 the reveal 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 a JSPrincipals* as an argument.

@Mossop
Copy link

Mossop commented Mar 14, 2018

You have a dangling sentence in the second point of "The Reappraised Requirements"

@Mossop
Copy link

Mossop commented Mar 14, 2018

In the reveal discussion it is unclear what new set of principals are used for filtering the stack.

@jimblandy
Copy link
Author

You have a dangling sentence in the second point of "The Reappraised Requirements"

Urg. Thanks.

In the reveal discussion it is unclear what new set of principals are used for filtering the stack.

That's covered in the paragraphs on how the operation would look in JavaScript, and in C++. I've bumped it up.

@Mossop
Copy link

Mossop commented Mar 14, 2018

This might read better if you gave the compartment independent frame object a pithy name. Otherwise I think this makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment