feat(callstack): restore call stack instrument (FF150 JSM->ESM port) - #1178
feat(callstack): restore call stack instrument (FF150 JSM->ESM port)#1178vringar wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR restores the callstack_instrument on modern Firefox (FF150) by porting the legacy stackDump WebExtension Experiment machinery from JSM to system ESM, updating window-actor registration/loading semantics, and adjusting Firefox deployment so the content sandbox can read the privileged child actor module. It also re-enables and stabilizes the callstack test by localizing assets and narrowing assertions to the supported (script-initiated) scope.
Changes:
- Port stackDump actors to
.sys.mjs(ESM) and update actor registration (esModuleURI,sharedData.flush(), modernresource://handler acquisition). - Install the extension non-temporarily into the Firefox profile
extensions/dir whencallstack_instrumentis enabled (to satisfy the content sandbox), and skip geckodriver’s temporary install in that case. - Reactivate and de-flake
test_http_stacktraceby using local test assets and asserting a deterministic script-initiator stack; update docs/agent notes accordingly.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
test/test_pages/shared/inject_pixel.js |
New local script asset used to make the initiating script request deterministic/offline. |
test/test_pages/http_stacktrace.html |
Removes remote gist dependency and simplifies page to only exercise script-initiated stack capture. |
test/test_callstack_instrument.py |
Re-enables the callstack test and updates expectations for the script-initiated stack only. |
openwpm/deploy_browsers/deploy_firefox.py |
Adds profile-sideload install path + prefs when callstack capture is enabled; avoids double-install. |
openwpm/config.py |
Removes the unconditional “broken” guard so callstack_instrument can run again (still requires js_instrument). |
Extension/eslint.config.mjs |
Adds globals config for privileged .sys.mjs system ES modules. |
Extension/bundled/privileged/stackDump/OpenWPMStackDumpParent.sys.mjs |
Parent actor ported to ESM; adds one-time observer registration. |
Extension/bundled/privileged/stackDump/OpenWPMStackDumpChild.sys.mjs |
Child actor ported to ESM; improves teardown/navigation robustness and event-based instantiation. |
Extension/bundled/privileged/stackDump/api.js |
Updates resProto acquisition, registers window actor via esModuleURI, flushes sharedData, adds clearer failure reporting. |
docs/Configuration.md |
Updates callstack_instrument docs for restored behavior, limitations, and deployment implications. |
AGENTS.md |
Updates “Known Issues” to reflect restored instrument + limitations and deployment behavior. |
Comments suppressed due to low confidence (2)
openwpm/config.py:257
- The ConfigError message is now misleading:
callstack_instrumentis restored, so saying it "currently doesn't work" (and pointing to #557) is inaccurate. The error should simply explain thatcallstack_instrumentrequiresjs_instrumentto be enabled.
if browser_params.callstack_instrument and not browser_params.js_instrument:
raise ConfigError(
"The callstacks instrument currently doesn't work without "
"the JS instrument enabled. see: "
"https://github.com/openwpm/OpenWPM/issues/557"
)
Extension/bundled/privileged/stackDump/OpenWPMStackDumpParent.sys.mjs:94
receiveMessagewaits indefinitely forgChannelMapto containchannelId. If the matching *-on-opening-request is missed (or the child sends a channelId that never gets recorded), this async method will spin forever and can accumulate stuck tasks. Add a bounded timeout and bail (optionally logging) to prevent an infinite wait.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1178 +/- ##
==========================================
+ Coverage 62.36% 62.48% +0.11%
==========================================
Files 40 40
Lines 3930 3974 +44
==========================================
+ Hits 2451 2483 +32
- Misses 1479 1491 +12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
0257f32 to
7044c59
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
openwpm/config.py:257
validate_browser_paramsstill raises a ConfigError that says the callstack instrument "currently doesn't work" withoutjs_instrument. The instrument is being restored in this PR, so this message is misleading; it should state the actual requirement (callstack_instrument requires js_instrument) without implying the feature is broken.
raise ConfigError(
"The callstacks instrument currently doesn't work without "
"the JS instrument enabled. see: "
"https://github.com/openwpm/OpenWPM/issues/557"
)
77a972d to
21dcb67
Compare
OpenWPM requires its sideloaded web extension to collect any data. A user pref in browser_params.prefs could override the extension-load prefs at browser launch, silently disabling the extension and all instrumentation. Validate the extension-load-critical prefs at config-validation time (before any browser launches) and raise a hard ConfigError naming the offending pref: - extensions.startupScanScopes must include SCOPE_PROFILE (bit 1) - extensions.autoDisableScopes must NOT include SCOPE_PROFILE (bit 1) - xpinstall.signatures.required must be falsy Pref values are coerced robustly from int/string forms.
857689a to
d2d54d5
Compare
…e race The sync-path stack capture dropped every row on Firefox 152: the parent actor's receiveMessage waits asynchronously for the channel-tracking observer to record the request's channel, but on a fast (localhost) connection http-on-examine-response fired and deleted the entry within a single 10ms poll gap -- before the async wait loop ever observed it. The consumer then spun to its deadline and dropped the stack, leaving the callstacks table empty. FF152 tightened the open-request -> examine-response timing enough that this race, latent on FF150, now always loses. Resolve the WebRequest requestId eagerly at http-on-opening-request (while the channel is alive) and retain it in a bounded FIFO map keyed by channelId, instead of storing the channel object and deleting it at examine-response. receiveMessage then reads the requestId by channelId, waiting briefly only for it to appear. This removes the record-then-delete race entirely; the map is capped to keep memory bounded over long crawls. The async (network-monitor-alternate-stack) path is unchanged -- it already resolves the requestId directly from the notification's channel subject.
…age script The parent actor set watchedByDevTools in actorCreated, but a JSWindowActorParent is instantiated lazily -- only when the child sends its first *sync* script-initiated stack. Firefox gates the network-monitor-alternate-stack initiator stack (fetch/XHR/WebSocket) on that flag at request time, so any async request that precedes the first sync child message was captured with no initiator stack. In the limit -- a page whose first subresource is an async fetch -- the first page of every browser under-captured non-deterministically. Set the flag in the parent-process observer at document-on-opening-request instead: the top-level document channel opens in the parent process before the content process creates the window or runs any page script, so watchedByDevTools is replicated to content before the page's first async request fires. Race-free. The actorCreated call is kept as an idempotent defense-in-depth fallback.
d2d54d5 to
f573005
Compare
Summary
Restores the
callstack_instrument(disabled since #557), which captures theJavaScript call stack responsible for each HTTP request and writes it to the
callstackstable. It had been disabled because the privilegedstackDumpWebExtension Experiment no longer loaded on modern Firefox. This is primarily a
JSM→ESM port plus the actor‑registration and module‑loading fixes those changes
require — the underlying capture mechanism (the same one that powers the DevTools
"Initiator" column) is intact. It also wires the async‑initiator path so
fetch/XHR/WebSocket/worker stacks are now captured (closes #1177).
What broke (Firefox 150) — four distinct issues
EXPORTED_SYMBOLS+ChromeUtils.import(...jsm)wereremoved in Firefox 136 (Bug 1881888). Ported
OpenWPMStackDump{Child,Parent}.jsm→
.sys.mjs(ESM).XPCOMUtils.defineLazyServiceGetterforresProtothrew on FF150 — replacedwith
Services.io.getProtocolHandler.registerWindowActornow requiresesModuleURI(notmoduleURI).Services.ppmm.sharedData.flush()and a content‑readable module path (see sandbox note below).
How it captures
Synchronous, script‑initiated requests. A JSWindowActor child observes
http-on-opening-requestin the content process, walksComponents.stack, andserializes frames as
name@file:line:col;asyncCause; the parent mapschannelId → ChannelWrapper.id(== the webRequestrequestId) and re‑broadcastsvia the
openwpm-stacktraceobserver;callstack-instrument.tswrites to thecallstackstable.Asynchronous initiators (fetch/XHR/WebSocket/worker) — closes #1177. These
open their channel off the JS stack, so
Components.stackin the content processis empty. Firefox instead captures the initiator stack at request time, serializes
it (
SerializedStackHolder, converted to a plain SavedFrame object andJSON‑stringified), and delivers it in the parent process via the
network-monitor-alternate-stackobserver notification (subject = the requestchannel, data = the JSON stack). The parent actor:
watchedByDevToolson the topBrowsingContextin
actorCreated— the same gecko flag the DevTools network monitor relies on;the C++ fetch/XHR/WebSocket paths all gate origin‑stack capture on it, and on
FF150 these stacks are otherwise never captured without an attached client. The
only other documented side effect is HTML‑content reporting, benign for
measurement;
network-monitor-alternate-stack,JSON.parses the stack, walksparent/asyncParentlinks (preservingasyncCause), and reformats each frameinto the same
name@file:line:col;asyncCausestring the sync path produces,resolving the
requestIdstraight from the channel subject viaChannelWrapper.get(channel).id.Both observers are registered once as guarded module‑global singletons (extending
the existing
ensureObserverRegistered()overOBSERVED_TOPICS), so they are notappended per page over a long crawl.
Module loading & the content sandbox
The privileged child ES module must be readable by the content process, which the
content sandbox blocks for geckodriver's temp‑installed xpi. Rather than relaxing
the sandbox, this installs the extension non‑temporarily into the profile's
extensions/directory — a path the Linux sandbox broker grants read‑onlyunconditionally — so the content sandbox stays fully enabled at its default
level: no
security.sandbox.content.levelchange and no path whitelist. Theprofile‑sideload prefs (
extensions.startupScanScopes,extensions.autoDisableScopes)and the xpi copy are applied unconditionally to every crawl, not gated on
callstack_instrument: the extension is now installed this way for all crawls(its experiment APIs load on a non‑privileged profile sideload), so the install
path is uniform regardless of which instruments are enabled.
Detectability
callstack_instrumentrequiresjs_instrument(the legacy, already‑detectableinstrument). The
resource://openwpmsubstitution is registered withoutALLOW_CONTENT_ACCESS, so page script cannotfetch()it (verified: returnsNetworkError) — it does not add a new content‑observable artifact.Tests
test/test_callstack_instrument.py::test_http_stacktracenow asserts threenon‑vacuous captured initiator stacks against real Firefox 150: the sync
<script>‑initiated request, plus an asyncfetch‑initiated and an asyncXHR‑initiated request (the two alternate‑stack paths). Each asserts a specificcaptured frame chain and fails if that path's capture breaks.
test/test_pages/http_stacktrace.htmlexercises all three mechanisms; assets arelocal for deterministic, offline runs.
Docs
The detailed callstack mechanism (privileged JSWindowActor, profile
extensions/install, capture scope, the alternate‑stack path) now lives in the
callstack_instrumentsection ofdocs/Configuration.md; AGENTS.md keeps only ashort pointer.
Closes #557. Closes #1177.