Skip to content

Address the review findings from #1: four production bugs and five hardening fixes - #2

Merged
senamakel merged 11 commits into
mainfrom
tinybrowser-review-fixes
Aug 21, 2026
Merged

Address the review findings from #1: four production bugs and five hardening fixes#2
senamakel merged 11 commits into
mainfrom
tinybrowser-review-fixes

Conversation

@senamakel

Copy link
Copy Markdown
Member

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:blank is now admitted by a non-empty allowed_origins. It is not a
    network 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.
  • An allowlist entry may name a port — localhost:3000. It could before, in the
    sense that it was accepted; it matched nothing, because Url::parse reads it
    as the scheme localhost with the path 3000. An operator developing against
    a 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:

  1. The launched browser's stderr was never drained (cdp/launch.rs). The
    reader 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.
  2. A failed configure() leaked a browser (session/mod.rs). open
    propagated the error and dropped the session; the target stayed open and a
    launched Chrome kept running, because shutdown is async and Drop cannot
    call it. Every failed open leaked a process and a profile directory.
  3. The session limit did not hold under concurrency (engine/mod.rs). The
    read 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.
  4. Inline SVG was extracted as text (extract/script.rs). The skip set is
    upper case; tagName keeps its original case for foreign content, so <svg>
    reports svg and walked straight past it. The symptom is an icon's <title>
    turning up in the middle of a paragraph.

Robustness:

  1. Screenshots are bounded before decoding (capture/mod.rs). The store
    refused an oversized image, but only after the decode had allocated it.
    Checked now from the length of the encoding, in a pure within_cap with its
    own tests.
  2. Held outputs expire on their own (capture/store.rs, engine/mod.rs).
    Expiry ran only from insert and read, so a host that took sixteen large
    screenshots 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 new is not async and
    may be called outside a runtime.
  3. Snapshot traversal is bounded independently of rendered depth
    (snapshot/render.rs). depth only advances for nodes that are rendered, so
    a long chain of ignored or filtered wrappers never increased it and could not
    bound the recursion.

Tests and docs:

  1. Allowlist cases for ports and about:blank (session/test.rs) — the two
    behaviour changes above, each with the test that would have caught it.
  2. The integration doc pins the contract crate to a tag
    (docs/openhuman-integration.md). An unpinned git dependency resolves to the
    default 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 -- --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo build --all-targets --all-features
  • cargo test --all-features

Also: 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 the
    boundary agrees with the store it guards.
  • Output expiry — an output past its TTL is gone, one inside it survives, and a
    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.
  • Allowlist — a host-and-port entry matches that port only, a bare host admits
    any port, an origin entry still carries its port, a default port matches an
    entry that spells it out, and about:blank is admitted under an allowlist.

Documentation

docs/openhuman-integration.md pins the dependency and says why the tag and the
registry version move together. The reasoning for each fix is on the code it
belongs to rather than here — the stderr drain, the slot reservation, and the two
traversal limits each carry the failure they prevent.

Checklist

  • The change is focused on one logical change (one review, nine findings)
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

senamakel and others added 11 commits August 21, 2026 23:25
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>
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Your included review limit has been reached.

You’re in a promotional period — use the checkbox below to run this review for free:

  • Run 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 @coderabbitai review --use-credits.

You can also wait for the limit to reset (next review available in 13 minutes), then comment @coderabbitai review or push new commits to the PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 16aea53a-acb2-4c96-92a7-dfd877bbdd94

📥 Commits

Reviewing files that changed from the base of the PR and between 15bb036 and 1b06941.

📒 Files selected for processing (11)
  • crates/tinybrowser/src/capture/mod.rs
  • crates/tinybrowser/src/capture/store.rs
  • crates/tinybrowser/src/capture/test.rs
  • crates/tinybrowser/src/cdp/launch.rs
  • crates/tinybrowser/src/engine/mod.rs
  • crates/tinybrowser/src/extract/script.rs
  • crates/tinybrowser/src/session/mod.rs
  • crates/tinybrowser/src/session/policy.rs
  • crates/tinybrowser/src/session/test.rs
  • crates/tinybrowser/src/snapshot/render.rs
  • docs/openhuman-integration.md

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out · 741 embedded · openrouter/openai/text-embedding-3-small

@tinysweeper

tinysweeper Bot commented Aug 21, 2026

Copy link
Copy Markdown

How this change flows

5 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
Loading

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.

tinysweeper 0.1.0

@senamakel
senamakel merged commit 6fc4648 into main Aug 21, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant