Skip to content

Limit BlueScreen agent argument dumps - #618

Open
janlanger wants to merge 21 commits into
nette:masterfrom
slevomat:fix-agent-dump-limits
Open

janlanger wants to merge 21 commits into
nette:masterfrom
slevomat:fix-agent-dump-limits

Conversation

@janlanger

Copy link
Copy Markdown
  • bug fix / new feature? bug fix
  • BC break? no
  • doc PR: no

BlueScreen agent output currently dumps stack trace arguments without applying the same item and string limits that the HTML dump uses. In practice this can produce extremely large .md exception logs when a stack frame contains a large root array or object graph.

A real case was a hydration frame with a large result. The MD file grew to ~100 MB because the agent dump expanded the whole argument list.

This change fixes that in two parts:

  • BlueScreen::getAgentDumper() now respects maxLength and maxItems
  • stack trace arguments in agent.phtml are dumped with their argument name as the dumper key, so the existing Dumper::ITEMS limiting also applies to large root values

@dg
dg force-pushed the master branch 2 times, most recently from 1731e8d to efbf873 Compare July 18, 2026 02:16
@jkuchar

jkuchar commented Jul 31, 2026

Copy link
Copy Markdown

Independent confirmation of this, from production: we hit the same defect and filed #620 before finding this PR — closing that one as a duplicate. Two things from our side that may help move this along, because the impact is worse than a large .md.

It is a fatal, not just a big file. One stack frame holding a list of domain objects exhausted a 128 MB PHP-FPM worker inside Dumper/Describer.php. The worker dies, and the unrelated request it happened to be serving dies with it. Measured on Tracy 2.12.0 / PHP 8.5.7, N small objects in a single trace argument, peak memory the render allocates:

N render() (HTML) renderAgent() (.md)
5 000 27.37 MB total render, .md 681 kB —
20 000 +2.01 MB, 148 kB output +106.97 MB, 2.79 MB output
50 000 +3.89 MB, 148 kB output +266.81 MB

With memory_limit=128M the process dies from ~22 000 objects. Turning BlueScreen::$maxDepth/$maxLength/$maxItems to the floor changes nothing, for exactly the reason this PR fixes: those values never reach the agent dumper.

Debugger::$reservedMemorySize does not save it, and enlarging it makes things worse. The reserve is released in shutdownHandler()/exceptionHandler(), so a bluescreen rendered from application code — ours goes through a Monolog handler bridging into Tracy\Logger during an ordinary request — never gets it. And since it is a plain string held for the whole request, raising it just lowers the threshold:

reserve N=5 000 N=12 000 N=20 000
500 kB (default) ok ok ok
8 MB ok ok fatal
64 MB ok fatal fatal

One residual this PR does not cover, in case it is worth a second commit: BlueScreen::findGeneratorsAndFibers() → Helpers::traverseValue() walks everything reachable from the exception with no depth or item limit at all, and runs before either template. With the markdown copy taken out of the picture entirely, our reproducer still dies at ~210 000 objects, inside Helpers.php. Much further out than the case this PR fixes, but structurally the same shape.

Happy to test a build of this against the original reproducer if that helps.

@spaze

spaze commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

This also fixes a security bug, not just memory use.

Right now the .md log prints argument values that the .html log hides. Both #[SensitiveParameter] and keysToHide (including Tracy's own defaults like password and authorization) are silently ignored in agent.phtml, because isSensitive() is only reached for values that arrive with a key.

Btw, passing $name in this patch fixes the attribute but not keysToHide, because $name is '$' . $params[$k]->getName() and the lookup is by exact name, so $password never matches password. isSensitive() matches SensitiveParameterValue by type, which is why the attribute works with any key at all. Can send a followup PR.

In practice "just" stack arguments are affected, $_GET/$_POST and object properties are fine because those are array elements and object properties, so they have a key

A simple reproduction code for the security-side:

function connect(string $dsn, #[SensitiveParameter] string $password): never
{
    throw new RuntimeException('connection refused');
}

@mkdir(__DIR__ . '/log');
Debugger::enable(Debugger::Production, __DIR__ . '/log');
$password = 'hunter2-' . bin2hex(random_bytes(4));
try {
    connect('mysql:host=localhost', $password);
} catch (Throwable $e) {
    Debugger::log($e);
}

In the stack trace, the .html shows ***** (string), the .md shows value: 'hunter2-...'.

dg added 20 commits August 18, 2026 13:15
Single helper replacing e.target everywhere; with shadow DOM coming,
composedPath()[0] is the element the event actually originated from.
Infrastructure for distributing styles as constructed stylesheets;
nothing consumes it yet.
CSS is no longer injected into the host page's document.head. Styles are
distributed as constructed stylesheets (adoptedStyleSheets) built from
the Tracy.css registry, so host-page styling cannot be affected by Tracy
and vice versa. This also makes Tracy styles work under CSP without
'unsafe-inline' style-src.

- Bar: <tracy-bar> shadow host, panels live inside a <tracy-div id=tracy-debug>
  wrapper kept for backward compatibility of third-party panel CSS; the
  panel popup window adopts the stylesheets explicitly in toWindow()
- BlueScreen: <tracy-bs> shadow host; the standalone error page keeps a
  document-level <style> so it renders without JavaScript
- standalone dumps: <tracy-dump> custom element with shadow root; upgrade is
  deferred while the parser is still constructing children
The shadow boundary now provides the isolation, so the #tracy-debug and
#tracy-bs selector prefixes and most of reset.css are no longer needed.
Layers are named tracy-base and tracy-components so they cannot collide
with the host page's own cascade layers (e.g. Tailwind).
setPointerCapture() replaces the document-level mousemove/mouseup +
touchmove/touchend pairs, works for touch input (which passive touchmove
listeners could not preventDefault), guarantees drag end delivery when
the pointer leaves the window, and removes the module-global dragging
flag. options.start is no longer invoked twice per drag.
…ssets

renderTemplate() no longer reads and minifies CSS/JS assets for templates
that do not embed them (agent markdown, AJAX content) and the expensive
object-graph traversal in findGeneratorsAndFibers() is computed once per
exception and reused by all render targets. The agent console block is no
longer emitted into .html files written by renderToFile().
…ustom properties

Adds 'auto' theme support driven by prefers-color-scheme. The .tracy-light
and .tracy-dark classes emitted by the Renderer keep working.
- BlueScreen panel callbacks may return a 'text' key with a markdown
  section included in renderAgent() output and .md log files
- new AgentPanel interface formalizes getAgentInfo() for Bar panels,
  method_exists() discovery still works
- Bar::renderAgent() isolates a throwing panel instead of aborting
- info panel provides a System Info section for agents
- redirect queue entries are logged to console in chronological order
…ception reports

- every logged exception appends a JSON Lines record (time, level, hash,
  message, report filename, source) usable by log aggregators and AI agents
- Logger::$retention ('30 days') deletes old .html/.md exception reports,
  checked opportunistically at most once a day
The header offers a button that copies the complete markdown report
(same content renderAgent() produces) to the clipboard - ready to paste
into an issue tracker or an AI assistant. The payload is embedded as an
inert JSON script inside the shadow root; the button appears only when
the Clipboard API is available.
Re-stamps request time and output-buffer level, clears timers, barDump
data, the live dump snapshot and per-request strategy state, and flushes
FileSession so deferred content of the previous request is persisted.
Intended for FrankenPHP worker mode, RoadRunner and Swoole.
- Bar lives in the top layer via the Popover API, no more z-index battles
  with host-page overlays and modal dialogs (with graceful fallback)
- toggles are keyboard-accessible (tabindex + Enter/Space activation)
- collapsed content uses hidden='until-found' so browser find-in-page
  reaches it and auto-expands the match
- Logger::$notifiers[] hooks for one-line Slack/Sentry/webhook integrations
- mailers receive the exception report filename as a third argument

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants