Address the review findings from #1: four production bugs and five hardening fixes - #2
Conversation
The browser writes to stderr continuously after startup, and the pipe must have a reader to avoid filling up and blocking the process. The stderr reader is now returned from `read_websocket_url` and spawned as a background task that drains the pipe for the browser's lifetime, with the task aborted during shutdown. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The session limit check had a race condition where concurrent callers could all read a count below the limit, pass the check, and launch multiple browser processes simultaneously. This change introduces an atomic counter for sessions that are starting but not yet in the sessions map, along with a reservation guard that automatically decrements the counter on failure, preventing leaked slots and ensuring the limit is enforced during the entire launch window. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The script extractor now converts `node.tagName` to upper case before comparing it against the skip set. Foreign content such as inline SVG elements report their tag name in original case, which caused elements like `<title>` to bypass the filter and leak unwanted text into the extracted output. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a maximum traversal depth of 1,000 levels to prevent a pathologically nested accessibility tree from exhausting the call stack. The existing depth limit only counts rendered nodes, so a long chain of ignored or filtered wrappers could bypass it and cause unbounded recursion. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The allowlist policy now explicitly permits `about:blank` URLs, which are used to clear the current page and cannot carry network data. Additionally, entries in the form `host:port` are now correctly matched against the URL's host and port, rather than being parsed as a URL with a scheme of `host` and path of `port`, which would silently block all destinations. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Check the estimated decoded size of a screenshot against the store's byte cap before performing base64 decoding, so that a full-page capture that exceeds the limit is refused early without allocating memory for the decoded image. The constant is made public to allow the check in the screenshot function. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The output store now runs a periodic sweeper that drops expired captures without waiting for a new request. Previously, expiry only happened when a caller happened to invoke an operation, which meant a host that took several large screenshots and then went silent would hold every byte until it called again. The sweeper runs every sixty seconds and is started lazily on the first capture so that the constructor does not need an async runtime. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add three test cases covering the time-to-live logic of the output store: one verifying that an output becomes inaccessible after its TTL has passed, one confirming that an output remains accessible while still within its TTL, and one demonstrating that the sweep method releases all expired outputs that would otherwise remain resident. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Pin the tinybrowser-bus git dependency to a specific tag instead of leaving it unpinned, which would resolve to whatever the default branch holds at build time and cause a runtime decode error when the host compiles against payload types newer than the module it loads. The documentation also adds guidance to move the tag and registry version together in one commit, as they represent the same decision and catching drift in review is cheaper than catching it in a session. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Changed the test for output survival within TTL to use `saturating_sub` instead of direct subtraction, preventing a potential underflow when TTL is smaller than one second. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The inline size check in the screenshot function has been moved into a new `within_cap` helper, making the pre-decode guard reusable and testable. Three tests verify that ordinary screenshots pass, oversized ones are refused before decoding, and the check aligns with the store's own capacity limit. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
|
Warning Review limit reachedYour included review limit has been reached. You’re in a promotional period — use the checkbox below to run this review for free:
On-demand reviews are free for the next 30 days. After that, they cost $0.25 per reviewed file. How can I continue?Run this review now using the option above, or comment You can also wait for the limit to reset (next review available in 13 minutes), then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
How this change flows5 changed behaviours across 9 relationships. 4 surrounding behaviours are shown (60 graph nodes walked). 42 further behaviours left out to keep the diagram readable. flowchart LR
n0["pixels<br/>changed"]:::changed
n1["OutputStore<br/>changed"]:::changed
n2["..._than_the_cap_is_refused_rather_than_held<br/>changed"]:::changed
n3["LaunchedBrowser<br/>changed"]:::changed
n4["read_websocket_url<br/>changed"]:::changed
n5["insert"]:::impacted
n6["screenshot"]:::impacted
n7["...ing_releases_what_nothing_else_would_have"]:::impacted
n8["launch_within"]:::impacted
n2 -->|calls| n5
n2 -->|tests| n5
n6 -->|calls| n0
n6 -->|uses| n1
n6 -->|calls| n5
n7 -->|calls| n5
n7 -->|tests| n5
n8 -->|uses| n3
n8 -->|calls| n4
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge. |
Summary
Addresses the nine findings from CodeRabbit's review of #1, which landed as a
blocking review after the branch had already merged. Every one was real; four
were bugs that would have bitten a host in production rather than style points.
Each fix follows the same rule the module already uses: the judgement goes in a
function that can be tested without a browser, and the test names the failure it
prevents.
Related issue
Follow-up to #1 (review comments on that PR).
API or behavior changes
Two behaviour changes, both fixing something that failed closed or leaked.
about:blankis now admitted by a non-emptyallowed_origins. It is not anetwork destination and it is how a caller clears the page; refusing it meant a
session that set an allowlist could never let go of the last page it loaded,
which is the opposite of what the setting is for.
localhost:3000. It could before, in thesense that it was accepted; it matched nothing, because
Url::parsereads itas the scheme
localhostwith the path3000. An operator developing againsta local server got everything blocked, and the failure looked like their typo.
Everything else is internal.
The findings
Bugs that would have hurt in production:
cdp/launch.rs). Thereader was dropped once the debugger URL had been parsed out of it, leaving a
pipe with no reader. Chrome writes to stderr for its whole life; a full pipe
blocks the process writing into it. The browser would have appeared to hang at
some arbitrary later moment, with nothing connecting it to startup. The reader
now comes back with the URL and is drained until EOF.
configure()leaked a browser (session/mod.rs).openpropagated the error and dropped the session; the target stayed open and a
launched Chrome kept running, because
shutdownis async andDropcannotcall it. Every failed open leaked a process and a profile directory.
engine/mod.rs). Theread lock was released before the launch, so concurrent callers all saw room
and all proceeded. Twenty simultaneous opens against a limit of eight gave
twenty Chromes. The slot is now reserved under the write lock and released by
a guard if the launch fails.
extract/script.rs). The skip set isupper case;
tagNamekeeps its original case for foreign content, so<svg>reports
svgand walked straight past it. The symptom is an icon's<title>turning up in the middle of a paragraph.
Robustness:
capture/mod.rs). The storerefused an oversized image, but only after the decode had allocated it.
Checked now from the length of the encoding, in a pure
within_capwith itsown tests.
capture/store.rs,engine/mod.rs).Expiry ran only from
insertandread, so a host that took sixteen largescreenshots and then went quiet held every byte of them — in somebody else's
process — indefinitely. A sweeper now runs on an interval, started on the
first capture rather than in the constructor, because
newis not async andmay be called outside a runtime.
(
snapshot/render.rs).depthonly advances for nodes that are rendered, soa long chain of ignored or filtered wrappers never increased it and could not
bound the recursion.
Tests and docs:
about:blank(session/test.rs) — the twobehaviour changes above, each with the test that would have caught it.
(
docs/openhuman-integration.md). An unpinned git dependency resolves to thedefault branch, so a host would eventually compile against payload types newer
than the module it loads, and the mismatch surfaces at runtime in a call.
Validation
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo build --all-targets --all-featurescargo test --all-featuresAlso: the per-file coverage gate passes, and the live suite runs green against a
real Chrome.
Tests
Eleven new tests, all deterministic and none needing a browser:
within_cap— an ordinary capture passes, an oversized one is refused, and theboundary agrees with the store it guards.
sweep releases what no other call would have. Time is moved with a test-only
age()rather than waited out, so the assertions are exact and instant.any port, an origin entry still carries its port, a default port matches an
entry that spells it out, and
about:blankis admitted under an allowlist.Documentation
docs/openhuman-integration.mdpins the dependency and says why the tag and theregistry
versionmove together. The reasoning for each fix is on the code itbelongs to rather than here — the stderr drain, the slot reservation, and the two
traversal limits each carry the failure they prevent.
Checklist
#[allow(...)],#[ignore], or relaxed lints.envcontents in the diff or the description