feat(bread-utils): breadbug client, panic hook, native crash handler, logs, heartbeat #68
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/breadbug-client"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BREAD-16, BREAD-17 (bread repo issues; the code lives here because bread-client is in bread-utils). Part of ECO-EP1.
What
One call on the existing client turns on everything an app needs for breadbug:
(module, offset, build-id)frames frombacktrace(3)mapped through/proc/self/maps, so stripped release binaries are symbolicated server-side from the build-id. The hook's own frames are cut by an install-time probe panic, so stacks start at the panicking function (this matters: otherwise constant hook frames would eat the fingerprint's top-5). Re-entrancy guarded,catch_unwindaround everything, send on a helper thread with a 400 ms deadline so a wedged breadd cannot delay exit. The previous hook still runs.socket/connect/write/closeand no allocation, runs on an alternate stack (so stack overflows are reportable), thenSA_RESETHANDmakes the process die exactly as it would have. Frames only: no memory, registers or environment. Libraries loaded after install are omitted from the snapshot.BugGuard::logand alog::Logbackend.tick()keeps arriving, so a frozen main loop goes quiet and breadd's watchdog can notice;stoppingis announced on drop.BugOptions::disabled()turns it all off for sensitive apps. Nothing leaves the machine: this only talks to breadd's local socket (breadd scrubs before storing).has_build_id()tells an app whether its binary carries a GNU build-id (needs-C link-arg=-Wl,--build-id; ECO-67 will enforce this in CI).Tests
logbackend, disabled/missing-breadd).crashmarker withsignal, stack passes through the faulting function, frames contain only module/offset/build_id). Run 3x: stable.-D warningsclean. Not run: the gtk-feature build (attach_glib_main_loop), and a release-mode run of the crash tests.Notes
backtrace(3)); other targets compile but report no frames.Independent exact-head review: changes requested for BREAD-16 and BREAD-17
Reviewed
b2614ba0e1against actual main/baseb52f0fd6fa, both Breadway/bread-ecosystem. Expected head matches. Isolated detached scratch ~/.worktrees/bread-ecosystem/review-pr68; primary checkout untouched. All PR-list pages and all 10 changed files exhausted, complete actual diff/source/tests read. Actual BREAD-16/17 comments, ECO-66, CLIP-19 and dependencies BREAD-14/15/18/ECO-67/68 read. Authenticated reviews API empty and existing PR comments empty at review start; branch protections empty.BREAD-16 criterion mapping
BREAD-17 criterion mapping
Repair checklist on this PR
Independent commands PASS: cargo test -p bread-utils --features bread-client --locked (66 unit +4 integration tests,1 ignored doc); cargo test --release -p bread-utils --features bread-client --locked --test bug_crashes (4); cargo check -p bread-utils --features bread-client,gtk --locked; cargo clippy -p bread-utils --features bread-client --all-targets --locked -- -D warnings. Probe compiled via rustc against exact scratch rlibs, timeout3s. Evidence /tmp/opencode/pr68-{probe.rs,tests.log,release.log,gtk.log,clippy.log}. No full workspace independent test claim.
Exact-head required check / check (push) SUCCESS Actions297/job0, status Successful in2m56s. check.yml covers workspace fmt/clippy/tests including gtk/extract; green CI does not waive reproduced defects. Findings filed ECO-76 (eco issue75), high confidence reproduced shutdown/signal/privacy failures. Move BREAD-16 and BREAD-17 todo for coordinator rework, preserve PR/branch. No approval, merge, tag, release, done transition, deployment or live desktop access. Fresh exact-head independent review required after repair.
Independent verdict: changes_requested at exact head
b2614ba0e1, baseb52f0fd6fa. Full criterion mapping, commands and file/line repair checklist persisted: #68 (comment). Findings ECO-76. Reproduced shutdown hang with retained logger (timeout3s exit124), raised SIGSEGV survives(exit0), prior disposition overwritten, raw fake PII preserved. Native glibc backtrace is not justified async-signal-safe; no prior handler/alt-stack restoration. Logs drop-newest without sustained rate limit; adoption remains manual, not automatic connect. Panic previous-hook call retained and debug/release crash fixtures pass, but do not satisfy integrated acceptance. Independent66 unit+4 integration tests,4 optimized crash fixtures,GTK check and targeted clippy pass; exact-head check/check(push) SUCCESS Actions297. Actual store PR22, scrubber and pilot CLIP-19 unmerged; store-entry/report acceptance and overhead unverified. BREAD-16/17 to todo for coordinator repair on existing PR68; preserve branch. No merge/tag/release/done. Fresh exact-head review after repair.Repair for review comment 4431 (ECO-76) - now at
63b855bEach checklist item from 4431 and where it landed:
Automatic on connect (mod.rs / bread_client):
BreadClient::connectnow starts reporting by default (panic hook, crash handlers, logs, heartbeat). Opt-outs:connect_without_bug_reportingfor sensitive apps,BREAD_BUG_REPORTING=0for the user/environment,BugOptionsfor custom setups. GLib heartbeat source is removed on guard drop.Bounded nonblocking lifecycle: log queue (512) drops oldest, producers use
try_lockand never wait, token-bucket rate limit (50/s sustained, 200 burst, batches of 50), per-record format cap (2 KiB), non-blocking connect with 200 ms write timeout, 500 ms bounded shutdown. Regression testshutdown_is_bounded_even_while_a_logger_clone_is_aliveholds a logger clone across guard drop.Scrub before sending:
bug/scrub.rsredacts home paths, user names,/home|/Users|/run/userroots, emails, IPs, token prefixes, secretkey=valuepairs, and long hex ids. Applied to panic messages, panic locations, module paths, and log lines before they reach the socket, with seeded assertions on actual serialized socket bytes inbug/tests.rs.Signal-safe unwinding, prior actions, fatal semantics: the handler no longer calls
backtrace(3); it walks the frame-pointer chain starting from the ucontext RIP/RBP on x86_64 Linux (cfg-gated, fallback elsewhere), probing words through a pipe soEFAULTends a corrupt walk without a second fault. Priorsigactions are saved with checked setup and rollback; sync faults re-execute under the restored action, self-sent signals are re-raised,SIG_IGNfaults terminate,MSG_NOSIGNALavoids SIGPIPE substitution. Covered by loader/allocator-safe design, raised-signal handling, custom SA_SIGINFO handler chaining test, alt-stack handling, setup-failure test, and concurrent-crash guard (RUNNING).std guard chaining: the prior worker's
.init_arraysnapshot could not work (verified with a probe: constructors run before runtime init, so the action then is stillSIG_DFL; std installsSA_SIGINFO|SA_ONSTACK|SA_NODEFERlater, inside the binary). Replaced with an install-time check (is_std_guard): real handler plusSA_SIGINFO|SA_ONSTACKplus address in this binary's own executable (loader object matched by device/inode). Only std matches; C libraries and custom handlers keep chaining. Trade-off documented in module docs anddocs/breadbug-client.md.Tests un-ignored: both
#[ignore]s removed.segv_with_default_prior_action_dies_of_sigsegvanda_real_segfault_is_reported_then_the_process_still_dies_of_itpass asserting death by SIGSEGV; the latter also asserts frame 0 (ucontext RIP) lies insidefixture_segv_site. New test: a custom non-std prior SIGSEGV handler still runs (exit 77 + marker byte) and the crash marker is still sent.Overhead:
cargo run --release -p bread-utils --features bread-client --locked --example bug_overheadplus measured numbers indocs/breadbug-client.md(AMD Ryzen AI 7 350, rustc 1.97.1, 7 runs, medians: tick 48.9 ns, log 869.6 ns, log::Log 1204.9 ns). Also fixed: example gated withrequired-features = ["bread-client"]so default-feature builds pass.Checks on final tree, all green: fmt, clippy
-D warnings(base, gtk, extract, plus bread-client all-targets), workspace tests (base, gtk, extract),--lockedextract build,lint-pins.sh. One transient lib failure seen once mid-work (77/1) passes on every re-run including full workspace runs; looks like a timing flake, flagging in case it recurs.Still honestly open (unchanged): breadd store entry and induced breadclip pilot panic (bread PR22, CLIP-19 unmerged), breadd-side allowlist and report-time scrub layers, stack-overflow-as-overflow report wording.
Independent exact-head review: changes requested
Reviewed
63b855bce6against main4f70691b33(merge-baseb52f0fd6fa). Expected head matches. Isolated detached scratch ~/.worktrees/bread-ecosystem/review-pr68-63b855b. Primary checkout left atb2614ba. Reviews API empty. Prior changes_requested comment 4431 was againstb2614ba; repair commitsa9f01d7and63b855bwere read in full (14 files).What the repair fixed (verified, not taken from comment 4614)
mail <email> path /home/<user>/secret token=<redacted> <secret> ip <ip>with no raw email, token, or IP.Still failing
Drop-oldest is not actually true when the worker holds the queue lock.
bread-utils/src/bug/mod.rs:256-274: Queue::push uses try_lock and, on WouldBlock, returns 1 without storing the line. enqueue at 518-530 counts that as a drop. take at 278-287 holds the same mutex, including across Condvar::wait_timeout while idle. A log call during that wait drops the newest line even when the queue has room, and does not wake the worker. The 512-cap path does drop the oldest, but only after the lock is acquired.
Reproduced twice:
The same test passed 5 times alone and 3 times as bread-utils --lib --test-threads=8, so it is racy, not a bad assertion. Filed ECO-77.
Unverified, not passed
Repair checklist (existing PR, do not close)
No merge. Cards stay out of done.
ECO-77 fix is in (commit
47dbe00).Defect (review comment 4678):
Queue::pushused a singletry_lockand returned 1 (dropped) onWouldBlock, discarding the NEW line even when the 512-queue had room. A producer racing the worker'stake()lost lines to pure contention.Fix (
bread-utils/src/bug/mod.rs):pushretriestry_lockforPUSH_RETRY(1ms,Duration::from_micros(1000)), then takes the blocking lock. Only a FULL queue drops, and then the oldest (pop_front, returns 1); otherwise 0.take()) only holds the lock for a microsecond-scaledrain, never for IO.send_logs(socket write) runs aftertake()returns with an ownedVec, guard already dropped; verified by readinglog_loop/send_logs. Documented onpush.take()pre-reserves its output buffer before locking, so no allocation happens under the lock while producers may be waiting on it.into_inner), with a justifying comment onlock(): the queue holds only best-effort telemetry lines with no cross-line invariant, so a partial line is already an accepted loss mode; dropping the backlog would need&mutaccess we do not have and would destroy evidence for zero safety gain.Regression test (
bread-utils/src/bug/tests.rs::contended_push_drops_nothing_while_under_cap): 8 producers x 50 lines (400 total, under the 512 cap) under a live taker mimicking the log worker. Old-fails/new-passes proof: with onlymod.rsreverted to63b855bthe test FAILS (contention dropped lines while under cap, left: 325, right: 0); with the fix it passes 5/5 runs. Also fixed the testrecorder()to accept connections sequentially (a thread per connection reordered lines across sends and made the batching test flaky).Evidence, all green:
cargo fmt --all -- --check: cleancargo clippy --workspace --all-targets --locked -- -D warnings(base,gtk,extract): clean; plus-p bread-utilswithgtkand withbread-client: cleancargo test -p bread-utils --locked --features bread-client: 79 passed +bug_crashes7 passedcargo test --workspace --locked(+gtk, +extract): all ok, 0 failed (extract: 448 passed, 0 failed)scripts/lint-pins.sh: all tag-pinnedPushed fast-forward 63b855b..47dbe00 to
feature/breadbug-client.Independent SHA-bound re-review of PR #68 at
47dbe00f5e(base4f70691b33): changes requested.Criteria and evidence
Evidence/checks
47dbe00f5e; base4f70691b33; PR open and mergeable.No merge. Keep PR and cards in review for repair; do not mark done. Next action: replace blocking mutex tail with bounded/nonblocking producer path, add contention-duration regression, rerun full locked workspace + gtk checks and exact-head CI, then request fresh review. No tag.
Independent SHA-bound re-review of PR #68 at
47dbe00f5e: changes requested. Queue::push in bread-utils/src/bug/mod.rs spins 1ms then uses blocking lock(). This leaves an unbounded producer wait if a lock holder is preempted or stalled, violating the app-thread nonblocking contract. No IO under lock does not bound mutex acquisition. Replace with a genuinely bounded/nonblocking path retaining newest under capacity; add regression holding lock past retry duration and assert prompt return plus item retained. Preserve full-cap drop-oldest. Prior fixes for shutdown, signal chaining/fatal semantics, redaction, etc. retain their earlier evidence. Exact SHA status check/check (push) SUCCESS (Actions 301); 14 changed files exhausted; base4f70691b33. BREAD-16/17 integrated store and breadclip pilot/CLIP-19 gates remain open, no tag. No merge, no done. Keep PR and cards in review for repair, then rerun full locked workspace/gtk and exact-head CI and request fresh review.Repair pushed at exact head
711e3e8261064e066b53b8f31d0cfade94d1d8ba. Root cause: producer retriedtry_lockfor 1ms, then called blockinglock, so a preempted consumer could stall the app thread indefinitely despite no I/O under the mutex. Replaced the mutex-protected deque with vettedcrossbeam_queue::ArrayQueue; enqueue no longer takes a mutex, and only evicts oldest on full. Added regression holding the consumer wait lock across enqueue: producer returns within 100ms and retains the new below-cap line. Validation:cargo test --locked --offline --workspacepassed (one ignored heavyweight image case);cargo clippy --locked --offline --workspace --all-targets --all-features -- -D warningspassed;cargo check --locked --offline --workspace --all-targets --all-featurespassed including GTK;cargo fmt --allapplied andgit diff --checkpassed. Store/pilot integration gates remain open; no merge or done claim. Note: bread PR22 is already merged, so review's contrary claim is stale; CLIP-19 remains unmerged. Holding for fresh exact-head review.Independent re-review of PR68 at exact head
711e3e8261: provider implementation acceptance passes, subject to pending CI. Queue now uses crossbeam ArrayQueue; producers do not acquire the consumer wait mutex. Drop-oldest-on-capacity retry logic appears correct. Held-consumer-lock regression verifies prompt return and retention under capacity. Targeted bug unit tests passed 14/14; native crash integration passed 7/7; cargo test --workspace --locked passed. Ignored heavyweight palette test is unchanged and documented as allocating/encoding about 1 GiB, not an introduced waiver. Consumer checks empty, acquires wait mutex, rechecks before Condvar wait; producer enqueues then notifies, covering the wakeup race. Shutdown still sets stop, notifies all, and uses bounded worker join. Rate budget logic unchanged. Prior fixes for chaining, signal death, and redaction remain. Exact SHA check/check (push) is pending at latest observed state (Actions run 302), so NO MERGE until green. Bread PR22 store prerequisite is integrated; CLIP-19 breadclip pilot remains unmerged. Accept only provider-scope implementation; preserve pilot/rollout gates and do not mark BREAD-16/17 done. Commands: cargo test -p bread-utils --features bread-client bug::tests:: --locked (14 passed); cargo test -p bread-utils --features bread-client --test bug_crashes --locked (7 passed); cargo test --workspace --locked passed, with three unrelated ignored bakery platform tests, one documented 1GiB palette test, one display-dependent GTK test, and ignored doc examples. After CI green, re-read SHA/base/checks/reviews/mergeability and merge using repo strategy, no tag.Independent final defect gate: ECO-76 / ECO-77 on merged PR68 (defect-only, no source change)
Reviewed accepted code head
711e3e8261, merge commit94ea37490f(parents4f70691b33and711e3e8). Read-only; isolated detached scratch ~/.worktrees/bread-ecosystem/review-pr68 at94ea374, clean. This is not full BREAD-16/17 acceptance.Merge tree equals reviewed head
711e3e8and at94ea374. Changed-file set of 4f70691..94ea374 equals the PR set exactly.4f70691.4f70691+711e3e8yields tree7a498cb825, equal to 94ea374's tree. No unreviewed code entered the merge.Checks and reviews (live reads)
711e3e8: check / check (push) success, Actions run 302, 7m33s.94ea374: check / check (push) success run 303 (3m9s), dev bakery / build success run 304, dev bread-theme / build success run 305. The workflow defines one job (check) covering lint-pins, fmt, clippy base/gtk/extract, test base/gtk/extract and locked extract build, so the single status context is the whole job. Per-step logs need a login (run pages return 307 unauthenticated), so step results are read from the job status plus my own reruns below.94ea374, base main. Branch protection API needs a token (401); not observable, merge already done.ECO-76 defect map (original card vs merged tree)
94ea374(/tmp/opencode/eco-probe, native::install with prior SIG_DFL): raise(SIGSEGV) exit 139, kill(getpid, SIGSEGV) exit 139, synchronous null write exit 139.ECO-77 defect map
Commands run on
94ea374(my own, not copied)Not rerun: full workspace and gtk/extract suites; relied on exact-SHA green CI on
711e3e8and on94ea374plus the earlier reviewer and coordinator workspace runs.Non-blocking residual (not a defect gate failure)
Committed tests do not include a raised-signal case or a prior SIG_IGN case; those behaviors are verified only by the probe above. Recommend a follow-up test card (raise(SIGSEGV) dies of signal 11 under default prior; SIG_IGN prior chain; held-loader contention if feasible). Not filed by this pass.
Verdict
ECO-76 and ECO-77 defect acceptance: pass at
711e3e8/ merged94ea374. Both defect cards' acceptance is genuinely covered; nothing from the original ECO-76/77 text was dropped except the optional extra regressions noted above. Full BREAD-16/BREAD-17 acceptance remains OPEN: CLIP-19 breadclip pilot unmerged, rollout and app-overhead gates unverified, no tag. Board transitions left to the coordinator.