Skip to content

Collect the kernel crashes cameras recover from, rank them, and pay Club stars for them - #411

Merged
widgetii merged 2 commits into
masterfrom
crash-reports
Oct 8, 2026
Merged

widgetii merged 2 commits into
masterfrom
crash-reports

Conversation

@widgetii

@widgetii widgetii commented Oct 8, 2026

Copy link
Copy Markdown
Member

A camera on OpenIPC firmware keeps the kernel's log across a crash (pstore, S98crashlog), and its WebUI offers the owner a crash log. Nobody upstream ever sees it. For example, the lab's gk7205v300 + imx335 has had one since 2026-09-28. It is a NULL dereference in RGN_PutRegion [open_rgn] called from the VENC interrupt (ChnlVeduISR), which reads as an ISP bug because hi_isp_run was the task it interrupted. That bundle is now the parser's test fixture.

What this adds

  • POST /api/v1/crashes accepts the WebUI's crashlog_*.tar.gz. The contract is service/internal/crashes/CRASH.md. A signed-in member can also send the one they downloaded, from the new My crash reports section on /club.
  • Parsing and grouping.
    • Handles ARM, MIPS and arm64 backtraces, and oops, BUG, WARNING, panic and a failsafe boot loop.
    • Each crash is filed under a signature: the top five frames, fn [module], without offsets. One bug from many cameras is one row.
    • pstore's Oops#1 and Panic#2 of one crash count as one crash, and the same records sent again are one event.
    • A crash asked for with sysrq is kept but never listed and never pays.
  • Ranking. Worse kinds count more: boot loop > panic in an interrupt > panic > oops > BUG > warning. That weight is multiplied by how many cameras hit the bug, halved when nothing sent it for a month, and raised by half when firmware built in the last two weeks still has it.
  • /crashes is public and lists signatures, chips, sensors and status. It never shows a log, a camera or a member.
  • /club/crashes is for maintainers, and so is openipc crashes list|show|status|settle|takedown.
    • Triage statuses are confirmed, fixed (with the version), won't fix, bogus, and merging two signatures that are the same bug.
    • The reproduction view shows:
      • the SoC, sensor, kernel, firmware and majestic version each crash was seen on;
      • the firmware builds whose time matches the kernel's;
      • the task that crashed and what it interrupted, uptime, the command line and modules;
      • warnings and i2c or MMZ trouble just before the crash;
      • the log, with MACs and IP addresses replaced by keyed hashes.
  • Storage. The bundle as sent is kept in PostgreSQL (crash_bundles, at most 256 KB each) and never served. Events and bundles are append-only, guarded like owner reports; only takedown removes one.
  • Stars go into crash_stars, a third ledger, migration 030.
    • They are paid by the nightly settlement only, which purge now runs after settle-wall.
    • 1 star per camera and bug. The first reporter gets 3 more once a maintainer confirms the bug, and 5 more when it is fixed.
    • At most 10 crash stars a month; anything over waits for a later night.
    • bogus takes back everything a signature paid.
    • A camera's crashes pay its owner through the Open Wall's link. The leaderboard gets a Crashes column, and the member's total includes the new ledger.
  • nginx proxies ^~ /api/v1/crashes on both vhosts (512k bodies).

Not in this PR

  • The WebUI's Send to OpenIPC button and the owner's opt-in to send automatically (OpenIPC/majestic-webui).
  • S98crashlog additions (OpenIPC/firmware): a meta.json with versions and a stripped majestic.yaml, printk timestamps, and keeping the last 3 crashes.

Tests

  • service/run.sh test, including deploytest, passes.
    • The parser is checked on the real lab bundle, a sysrq crash, a MIPS trace and a failsafe-only bundle.
    • The upload tests cover duplicates, MAC and IP redaction, refusals and limits, and the guard triggers.
    • The settlement tests cover payment, a camera linked later, confirm/fix bonuses, bogus revoke, the monthly cap, ranking and merging.
    • The club tests check that only maintainers can triage, and that the stars reach /me and the leaderboard.
  • deploy/nginx/check-config.sh passes.
  • In frontend/: lint, typecheck and npm test pass (707 tests, including the /club crash section). The build produces /crashes, /ru/crashes and /club/crashes.

…lub stars for them

A camera on OpenIPC firmware keeps the kernel's log across a crash (pstore),
and its WebUI offers the owner a crash log -- which nobody upstream ever
sees. The lab's gk7205v300 + imx335 had one since 2026-09-28: a NULL
dereference in RGN_PutRegion called from the VENC interrupt, which reads as
an ISP bug because hi_isp_run was the task it interrupted.

- POST /api/v1/crashes takes the WebUI's crashlog_*.tar.gz (service/internal/
  crashes, CRASH.md is the contract); a member can send the one they
  downloaded from /club. The records are parsed -- ARM, MIPS and arm64
  backtraces, oops, BUG, WARNING, panic, a failsafe boot loop -- and filed
  under a signature, the top five frames without offsets, so one bug from
  many cameras is one row. pstore's Oops#1 and Panic#2 of one crash are one
  crash, and the same records sent again are one event.
- Signatures are ranked by how bad a crash is and how many cameras it hits,
  more when today's firmware still has it. /crashes lists them publicly:
  signatures, chips and status, never a log, a camera or a member.
- Maintainers triage at /club/crashes or with `openipc crashes status`, and
  see what reproducing one takes: the chips, sensors and builds it was seen
  on, the firmware builds matching its kernel, the task and what it
  interrupted, the warnings and i2c/MMZ trouble before it, and each log with
  MACs and IP addresses replaced by keyed hashes. The bundle as sent is kept
  in PostgreSQL and never served; events and bundles are append-only.
- Stars (crash_stars, migration 030) are paid by the nightly settlement only:
  1 per camera and bug, 3 more to the first reporter once confirmed, 5 when
  fixed, at most 10 a month. A crash asked for with sysrq pays nothing, and
  marking a signature bogus takes back what it paid. A camera's crashes are
  its owner's through the Open Wall's link. The leaderboard and the member's
  total count the third ledger.
- nginx proxies /api/v1/crashes on both vhosts.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Collect and rank camera kernel crashes with Club star rewards

✨ Enhancement ⚙️ Configuration changes 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Accept camera crash bundles, deduplicate incidents, and group bugs by kernel backtrace.
• Publish ranked signatures while restricting redacted logs and triage to maintainers.
• Settle crash rewards nightly and integrate them into Club totals and leaderboards.
Diagram

graph TD
  camera["Camera WebUI"] --> proxy["nginx proxy"] --> upload["Crash upload API"] --> parser["Bundle parser"] --> database[("Crash storage")]
  club["Club interface"] --> upload
  database --> public["Public crash list"]
  database --> club
  database --> settlement["Nightly settlement"] --> stars[("Crash stars ledger")]
  stars --> club
Loading
High-Level Assessment

Keep the dedicated crash package and ledger: they follow existing report-storage and Wall-star conventions while isolating crash-specific parsing and settlement. Folding crashes into an existing ledger would complicate per-signature deduplication, bonuses, and bogus revocation.

Files changed (40) +3796 / -17

Enhancement (21) +2471 / -16
Club.tsxShow member crashes and maintainer triage link +4/-0

Show member crashes and maintainer triage link

• Adds My crash reports, a crash-stars breakdown, and a maintainer-only triage link.

frontend/apps/site/src/components/club/Club.tsx

CrashTriage.tsxBuild maintainer crash triage interface +153/-0

Build maintainer crash triage interface

• Displays ranked signatures, reproduction details and redacted logs, with status, issue, and merge controls.

frontend/apps/site/src/components/club/CrashTriage.tsx

Crashes.tsxAdd My crash reports section +97/-0

Add My crash reports section

• Lets members upload downloaded bundles and review their reports and reward rules.

frontend/apps/site/src/components/club/Crashes.tsx

Leaderboard.tsxDisplay crash stars on leaderboard +4/-2

Display crash stars on leaderboard

• Adds a Crashes column alongside report, Wall, and total stars.

frontend/apps/site/src/components/club/Leaderboard.tsx

CrashList.tsxDisplay ranked public crash signatures +121/-0

Display ranked public crash signatures

• Provides expandable signature summaries, hardware combinations, and instructions for sending reports.

frontend/apps/site/src/components/crashes/CrashList.tsx

ClubCrashes.astroAdd maintainer crash page shell +19/-0

Add maintainer crash page shell

• Hosts the client-loaded triage interface without embedding protected data in static HTML.

frontend/apps/site/src/components/pages/ClubCrashes.astro

Crashes.astroAdd public crashes page shell +18/-0

Add public crashes page shell

• Hosts the client-loaded public signature list.

frontend/apps/site/src/components/pages/Crashes.astro

club.tsExtend Club member and leaderboard types +3/-2

Extend Club member and leaderboard types

• Defines crash-star fields in frontend API response types.

frontend/apps/site/src/lib/club.ts

crashes.tsAdd typed crash API client +162/-0

Add typed crash API client

• Defines crash data models and request helpers for public, member, and maintainer endpoints.

frontend/apps/site/src/lib/crashes.ts

club.goProvide crash integration to Club commands +2/-1

Provide crash integration to Club commands

• Supplies the crash API when constructing the Club API for Wall settlement.

service/cmd/openipc/club.go

crashes.goAdd crash administration commands +110/-0

Add crash administration commands

• Implements list, show, status, settle, and takedown operations.

service/cmd/openipc/crashes.go

main.goWire crash routes and nightly settlement +23/-1

Wire crash routes and nightly settlement

• Registers the crash API and CLI, and runs crash settlement after Wall settlement during purge.

service/cmd/openipc/main.go

api.goRegister Club crash handlers +9/-1

Register Club crash handlers

• Adds the crash dependency and conditionally exposes member and maintainer endpoints.

service/internal/club/api.go

crashes.goAdd authenticated crash endpoints +149/-0

Add authenticated crash endpoints

• Handles member submissions and history plus maintainer-only listing, details, and decisions.

service/internal/club/crashes.go

members.goInclude crash stars in member totals +7/-1

Include crash stars in member totals

• Adds crash stars to the member response and aggregate star count.

service/internal/club/members.go

wall.goInclude crash rewards in shared star totals +6/-2

Include crash rewards in shared star totals

• Extends the Club total-stars helper to include the crash ledger.

service/internal/club/wall.go

api.goImplement crash upload and public APIs +307/-0

Implement crash upload and public APIs

• Validates and rate-limits uploads, redacts identifiers, stores parsed crashes, and exposes public signature endpoints.

service/internal/crashes/api.go

parse.goParse pstore bundles and derive signatures +577/-0

Parse pstore bundles and derive signatures

• Recognizes crash types across ARM, MIPS, and arm64; extracts context, removes duplicate traces, and identifies sysrq crashes.

service/internal/crashes/parse.go

settle.goSettle crash rewards nightly +119/-0

Settle crash rewards nightly

• Pays once per member, camera, and bug, adds first/fixed bonuses, and enforces a rolling monthly cap.

service/internal/crashes/settle.go

store.goStore, rank, and triage crash signatures +570/-0

Store, rank, and triage crash signatures

• Persists deduplicated events and bundles, ranks signatures, exposes reproduction details, and supports merges, decisions, and takedowns.

service/internal/crashes/store.go

wallstars.goInclude crash ledger in Club rankings +11/-6

Include crash ledger in Club rankings

• Adds crash stars to leaderboard breakdowns and camera-owner totals.

service/internal/wallstars/wallstars.go

Documentation (9) +415 / -1
CLAUDE.mdDocument the crash reporting subsystem +18/-0

Document the crash reporting subsystem

• Explains upload, grouping, privacy, triage, and reward rules for contributors.

CLAUDE.md

boards.en.ymlAdd English crash reporting copy +77/-0

Add English crash reporting copy

• Adds source translations for public and Club crash pages, triage, and rewards.

data/locales/boards.en.yml

boards.ru.ymlAdd Russian crash reporting copy +81/-0

Add Russian crash reporting copy

• Adds translated page text, triage labels, reward explanations, and pluralized counts.

data/locales/boards.ru.yml

boards.en.jsonAdd English crash interface translations +72/-0

Add English crash interface translations

• Supplies generated UI text for listing, uploading, triaging, and rewarding crashes.

frontend/apps/site/src/i18n/boards.en.json

boards.ru.jsonAdd Russian crash interface translations +76/-0

Add Russian crash interface translations

• Supplies generated Russian UI text and count plurals for crash workflows.

frontend/apps/site/src/i18n/boards.ru.json

en.jsonAdd English crash page metadata +10/-0

Add English crash page metadata

• Adds titles and introductory copy for public and maintainer crash pages.

frontend/apps/site/src/i18n/en.json

ru.jsonAdd Russian crash page metadata +10/-0

Add Russian crash page metadata

• Adds translated titles and introductory copy for both new pages.

frontend/apps/site/src/i18n/ru.json

README.mdDocument crash commands and settlement +2/-1

Document crash commands and settlement

• Describes the crash CLI and its nightly purge settlement.

service/README.md

CRASH.mdSpecify camera crash upload contract +69/-0

Specify camera crash upload contract

• Documents bundle formats, metadata, limits, responses, privacy, and consent expectations.

service/internal/crashes/CRASH.md

Other (10) +910 / -0
org.openipcProxy production crash API requests +20/-0

Proxy production crash API requests

• Routes the crash API to the web service with a 512 KB body limit and request throttling.

deploy/nginx/sites-available/org.openipc

org.openipc.devProxy development crash API requests +21/-0

Proxy development crash API requests

• Adds equivalent development-vhost proxying, limits, and noindex headers.

deploy/nginx/sites-available/org.openipc.dev

Crashes.test.tsxTest member crash submission +44/-0

Test member crash submission

• Checks upload form data, submission feedback, and the refreshed member crash list.

frontend/apps/site/src/components/club/Crashes.test.tsx

page-paths.tsRegister crash page paths +4/-0

Register crash page paths

• Adds public and maintainer crash pages to page metadata.

frontend/apps/site/src/lib/page-paths.ts

pages.tsMap crash routes to Astro pages +4/-0

Map crash routes to Astro pages

• Registers the new public listing and Club triage components.

frontend/apps/site/src/lib/pages.ts

crashes_test.goTest Club crash authorization and rewards +101/-0

Test Club crash authorization and rewards

• Covers signed-in submission, maintainer restrictions, triage, and stars appearing in member and leaderboard responses.

service/internal/club/crashes_test.go

parse_test.goTest parser against real and synthetic crashes +174/-0

Test parser against real and synthetic crashes

• Covers the lab bundle, repacking, MIPS, sysrq, failsafe-only input, and invalid bundles.

service/internal/crashes/parse_test.go

store_test.goTest crash persistence and reward lifecycle +337/-0

Test crash persistence and reward lifecycle

• Exercises upload limits and redaction, immutable-row guards, settlement, bonuses, revocation, ranking, and merging.

service/internal/crashes/store_test.go

030_crashes.sqlAdd crash tables and append-only guards +165/-0

Add crash tables and append-only guards

• Creates signatures, triage history, bundles, events, and the crash-stars ledger with constraints and guard triggers.

service/internal/db/migrations/030_crashes.sql

routes.jsonRecord public and Club crash routes +40/-0

Record public and Club crash routes

• Adds eight new crash endpoints to the service route manifest.

service/routes.json

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Public crash listings can expose addresses ✓ Resolved
Description
Submit stores a title derived from the raw panic message and accepts caller-supplied SoC and
sensor strings without sanitizing them for publication. A log containing `Kernel panic - not
syncing: 192.168.1.4`, or an upload placing an address in a hardware field, sends that address
through Ranked to the public crash API.
Code

service/internal/crashes/api.go[R142-145]

+	writeJSON(w, code, map[string]any{
+		"id": id, "signature": sig, "title": c.Fatal.Title(), "kind": c.Kind, "duplicate": dup,
+		"self_inflicted": c.SelfInflicted,
+		"url":            strings.TrimRight(a.SiteURL, "/") + "/crashes/#" + sig,
Evidence
Panic messages become trace reasons and titles; the event and signature store retain that title, and
the unauthenticated list returns signatures including titles and hardware arrays.

service/internal/crashes/parse.go[99-115]
service/internal/crashes/parse.go[475-481]
service/internal/crashes/api.go[115-145]
service/internal/crashes/store.go[201-213]
service/internal/crashes/api.go[253-262]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Crash-derived titles and caller-supplied hardware fields can contain addresses and are returned by the public API without redaction.
## Fix Focus Areas
- service/internal/crashes/api.go[115-145]
- service/internal/crashes/parse.go[99-115]
- service/internal/crashes/store.go[194-213]
## Recommended Fix
Validate hardware identifiers and redact or otherwise constrain titles before storing or returning them publicly. Add public-response tests containing addresses in panic messages and hardware fields.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Some addresses survive log redaction ✓ Resolved
Description
Redact searches only for IPv4 dotted quads, its IPv4 regex consumes separators needed to match the
next address, and the shared MAC redactor does not recognize dotted MACs without an optional mac
identifier. IPv6 addresses and those MACs remain in stored logs or metadata, while single-separator
IPv4 sequences such as the kernel ip= boot argument can leave every second address visible in the
redacted log maintainers read.
Code

service/internal/crashes/api.go[R249-251]

+// ipv4 is a dotted quad that is not part of a longer dotted number (a
+// version, 4.9.37.1.2).
+var ipv4 = regexp.MustCompile(`(?:^|[^\d.])((?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3})(?:$|[^\d.])`)
Evidence
The crash-specific loop uses only the ipv4 pattern, while the shared redactor's generic MAC
patterns cover colon and hyphen formats but depend on a supplied identifier for dotted variants. The
IPv4 pattern (?:^|[^\d.])(IP)(?:$|[^\d.]) consumes both boundary characters; because Go's
FindAllStringSubmatchIndex returns non-overlapping matches, matching =10.0.0.2: leaves no
preceding separator available to match the next address. Redact replaces only matched addresses,
and its output becomes crash_events.redacted, which /club/crashes shows as the log.

service/internal/crashes/api.go[121-129]
service/internal/crashes/api.go[232-251]
service/internal/reports/parse.go[273-278]
service/internal/reports/parse.go[302-310]
service/internal/crashes/CRASH.md[53-59]
service/internal/crashes/api.go[121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The crash redactor leaves IPv6 and dotted MAC addresses without an optional `mac` identifier intact, and its IPv4 regex can skip addresses separated by one character.
## Fix Focus Areas
- service/internal/crashes/api.go[229-251]
- service/internal/crashes/store_test.go[83-114]
## Recommended Fix
Recognize IPv6 and dotted MAC addresses. Match only the IPv4 dotted quad, `(?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}`, then check the bytes immediately before and after each match in the loop, rejecting a match if either neighbor is a digit or `.`. Test each format in logs and metadata, including `ip=10.0.0.2:10.0.0.1:10.0.0.254:255.255.255.0` and `1.2.3.4 5.6.7.8`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. One camera can erase another's report ✓ Resolved
Description
Insert treats content_sum alone as the identity of an event, without considering its camera or
reporting member. Two cameras sending identical records or failsafe notes cause the second upload to
return the first event, so the second camera is absent from counts and cannot earn its report star.
Code

service/internal/crashes/store.go[R81-85]

+	err = pgx.BeginFunc(ctx, s.DB, func(tx pgx.Tx) error {
+		if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended('crash:' || $1, 0))`, c.ContentSum); err != nil {
+			return err
+		}
+		err := tx.QueryRow(ctx, `SELECT id, signature_id FROM crash_events WHERE content_sum = $1`, c.ContentSum).Scan(&id, &signature)
Evidence
ContentSum hashes record bodies and the failsafe note, while the lookup and unique constraint use
that hash globally; camera identity is stored separately only after the duplicate lookup.

service/internal/crashes/parse.go[247-282]
service/internal/crashes/store.go[81-92]
service/internal/crashes/store.go[105-115]
service/internal/db/migrations/030_crashes.sql[56-68]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A globally unique content hash collapses identical records from different cameras into one event.
## Fix Focus Areas
- service/internal/crashes/store.go[81-92]
- service/internal/db/migrations/030_crashes.sql[56-68]
- service/internal/crashes/parse.go[247-282]
## Recommended Fix
Scope event deduplication and its unique constraint to camera identity, with a deliberate policy for uploads lacking a MAC. Test identical failsafe bundles from two distinct cameras.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (4)
4. Real MIPS oops traces lose their frames ✓ Resolved
Description
traces1 opens an oops only for the ARM-style Internal error: Oops header and does not recognize
the MIPS Oops[#1]: header. For the MIPS record represented in the new test, the actual header
leaves cur nil throughout the backtrace, so the later panic is grouped by its message rather than
the faulting functions.
Code

service/internal/crashes/parse.go[R459-462]

+		case strings.HasPrefix(l, "Internal error: Oops"):
+			if cur == nil || cur.Kind != KindOops || len(cur.Frames) > 0 {
+				start(KindOops, "Oops")
+			}
Evidence
The test fixture contains Oops[#1]: but the test replaces it with Internal error: Oops[#1]:
before parsing. Without a recognized opening header, frame processing is skipped while cur is nil.

service/internal/crashes/parse_test.go[123-147]
service/internal/crashes/parse.go[455-483]
service/internal/crashes/parse.go[484-525]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Native MIPS `Oops[#1]:` records do not start a trace, causing their backtrace frames to be discarded.
## Fix Focus Areas
- service/internal/crashes/parse.go[403-413]
- service/internal/crashes/parse.go[437-483]
- service/internal/crashes/parse_test.go[123-147]
## Recommended Fix
Recognize the native MIPS oops header and retain its subsequent frames. Test the existing MIPS fixture without replacing its header.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Bogus merged reports retain their stars ✓ Resolved
Description
Decide revokes only awards whose signature ID equals the signature being marked bogus, whereas
Settle records merged events' awards under their target ID. Marking an already-merged source bogus
therefore leaves target-ID awards paid for its events untouched and reports zero or too few stars
taken back.
Code

service/internal/crashes/store.go[R530-535]

+		return tx.QueryRow(ctx, `WITH back AS (
+				INSERT INTO crash_stars (member_id, signature_id, mac_key, kind, reason, points)
+				SELECT member_id, signature_id, mac_key, 'revoke', reason, -points FROM crash_stars
+				WHERE signature_id = $1 AND kind = 'award'
+				ON CONFLICT DO NOTHING RETURNING points
+			) SELECT coalesce(-sum(points), 0)::int FROM back`, id).Scan(&taken)
Evidence
The merge target becomes the settlement root and award signature, but the reversal query filters
only on the source ID passed to Decide.

service/internal/crashes/store.go[499-535]
service/internal/crashes/settle.go[49-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Bogus triage of a merged source cannot reverse awards that settlement recorded against its target.
## Fix Focus Areas
- service/internal/crashes/store.go[499-535]
- service/internal/crashes/settle.go[48-78]
## Recommended Fix
Track award provenance through merges and revoke the awards attributable to the bogus source without revoking legitimate target events. Add a settle-then-merge-then-bogus test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. A member can claim another camera's star ✓ Resolved
Description
Submit accepts a member-supplied MAC without verifying its camera link, while Settle prefers the
uploader’s member ID over the linked owner and awards report stars per distinct member, signature,
and MAC. A signed-in member can claim a crash labeled with someone else’s camera or vary a log and
supply fabricated MACs to earn stars for fake cameras, inflate the public camera count, and defer
awards beyond the monthly cap.
Code

service/internal/crashes/settle.go[R50-54]

+				SELECT e.id, e.received_at, coalesce(sig.merged_into, sig.id) AS root, e.mac_key,
+				       coalesce(e.member_id, l.member_id) AS member
+				FROM crash_events e
+				JOIN crash_signatures sig ON sig.id = e.signature_id
+				LEFT JOIN camera_links l ON e.mac_key <> '' AND l.mac_key = e.mac_key
Evidence
Club uploads supply the signed-in member ID, but the request’s MAC is accepted if it matches macRe
and is neither all zeros nor all f’s; ownership is not checked. Duplicate detection uses
content_sum, so changing a log byte creates another event. In Settle, `coalesce(e.member_id,
l.member_id) selects the uploader before the camera-link owner, and per_camera` is distinct on
member, root, and mac_key, allowing separate report awards for supplied MACs. The public ranking
also counts distinct mac_key values as cameras. Marking the entire signature bogus would also
remove stars from honest reporters of a real bug.

service/internal/club/crashes.go[26-35]
service/internal/crashes/api.go[81-91]
service/internal/crashes/api.go[114-119]
service/internal/crashes/settle.go[48-62]
service/internal/crashes/settle.go[49-62]
service/internal/crashes/store.go[204]
service/internal/crashes/parse.go[247-282]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Uploads trust a member-supplied MAC without verifying its camera link. Settlement can therefore credit an uploader for another member’s camera or award per-camera report stars for fabricated MACs, which also inflate the public camera count.
## Fix Focus Areas
- service/internal/crashes/api.go[81-91]
- service/internal/crashes/api.go[114-119]
- service/internal/crashes/settle.go[48-62]
## Recommended Fix
For events with a MAC, credit a verified camera link rather than an unrelated uploader, or reject a club MAC linked to someone else. Award per-camera report stars only for MACs present in `camera_links`; treat an unlinked MAC on a club upload as an empty `mac_key`. Count only linked or webui-sourced MACs toward the ranking’s camera count. Test a member submitting a crash labeled with another member’s linked camera, as well as uploads using fabricated MACs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Merging two signatures pays the same stars twice ✓ Resolved
Description
Decide redirects a source signature's events to the merge target but leaves existing crash_stars
awards under the source ID, while Settle checks for prior awards only under the target root. When
A is merged into B after settlement, previously paid report awards can be paid again on the next
nightly run, as can A's first-reporter bonus if B's earliest reporter changes.
Code

service/internal/crashes/settle.go[R75-77]

+			SELECT o.member, o.root, o.mac_key, o.reason, o.points, o.received_at FROM owed o
+			WHERE NOT EXISTS (SELECT 1 FROM crash_stars c WHERE c.member_id = o.member AND c.signature_id = o.root
+				AND c.mac_key = o.mac_key AND c.reason = o.reason AND c.kind = 'award')
Evidence
The ev CTE assigns events coalesce(sig.merged_into, sig.id) AS root, and the owed rows carry
that root into a NOT EXISTS check requiring c.signature_id = o.root. Earlier awards were
inserted under the signature that was root at payment time; Decide changes the redirect without
rewriting those crash_stars rows. Because the unique key includes signature_id, an award under B
does not conflict with an otherwise matching award previously recorded under A.

service/internal/crashes/settle.go[49-78]
service/internal/crashes/settle.go[105-107]
service/internal/crashes/store.go[512-519]
service/internal/db/migrations/030_crashes.sql[136-146]
service/internal/crashes/settle.go[48-78]
service/internal/crashes/store.go[499-519]
service/internal/db/migrations/030_crashes.sql[129-145]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Settlement treats events from a merged signature as belonging to the target but does not recognize awards already paid under the source signature. This can repay report awards and a previously paid first-reporter bonus after a merge.
## Fix Focus Areas
- service/internal/crashes/settle.go[48-78]
- service/internal/crashes/store.go[499-519]
- service/internal/db/migrations/030_crashes.sql[129-145]
## Recommended Fix
Make the existing-award check account for awards under merged source signatures. In the `NOT EXISTS` subquery, join `crash_stars` to `crash_signatures` and compare `coalesce(cs.merged_into, cs.id)` with `o.root`, while retaining the member, camera, reason, and award-kind checks. Also treat an existing `first` award under the same root as already paid. Add a test that settles report and bonus awards, merges signatures, settles again, and verifies that no additional awards are issued.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

8. Maintainer details expose raw log values ✓ Resolved
Description
Insert stores parsed cmdline, leadup, and trace JSON from Crash without redacting them,
while Submit redacts only the full log and metadata. If a command line, lead-up line, or trace
reason contains an IP or MAC address, maintainer detail responses, the triage page, and `openipc
crashes show` can display it beside the redacted log.
Code

service/internal/crashes/store.go[R112-115]

+			e.ID, c.ContentSum, e.SignatureID, e.Channel, e.MACKey, e.Member, e.ClientKey, c.Kind, c.Fatal.InIRQ,
+			c.SelfInflicted, c.Fatal.Title(), e.Firmware, e.Majestic, e.SoC, e.Sensor, c.Board, c.Machine, c.Kernel,
+			c.KernelBuild, built, c.Cmdline, uptime, c.Records, orEmpty(c.Modules), fatal, before, anomalies, leadup,
+			meta, e.Redacted, e.BundleSHA256)
Evidence
Parse fills Cmdline from the raw kernel command-line record and Leadup from lines before the
fault. Submit redacts the full text and metadata, but Insert writes the parsed fields directly;
Details returns them, and the triage component renders d.cmdline and d.leadup beside the
redacted log.

service/internal/crashes/api.go[115-129]
service/internal/crashes/store.go[58-64]
service/internal/crashes/store.go[107-115]
service/internal/crashes/store.go[375-392]
frontend/apps/site/src/components/club/CrashTriage.tsx[142-150]
service/internal/crashes/parse.go[371-373]
service/internal/crashes/parse.go[546-558]
service/internal/crashes/api.go[115-122]
service/internal/crashes/store.go[376-383]
frontend/apps/site/src/components/club/CrashTriage.tsx[144-148]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Parsed crash fields bypass the identifier redactor even though they are stored and shown to maintainers alongside the redacted log.
## Fix Focus Areas
- service/internal/crashes/api.go[114-129]
- service/internal/crashes/store.go[58-64]
- service/internal/crashes/store.go[112-115]
## Recommended Fix
In `Submit`, after `Parse` and before building the event, apply `Redact` with the same key to `c.Cmdline`, each `c.Leadup` entry, and each trace reason so the stored parsed fields and trace JSON contain no raw identifiers. Add a store test covering IP and MAC addresses in command lines, lead-up lines, and trace reasons, and verify they are absent from detail responses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Restored reports remain unpaid ✓ Resolved
Description
Decide adds permanent revoke rows when a signature becomes bogus, but Settle considers the
original award row sufficient proof that each payment is complete. If a maintainer corrects a bogus
decision by returning the signature to open or confirmed, its net stars remain revoked and
settlement will not restore them.
Code

service/internal/crashes/settle.go[R75-77]

+			SELECT o.member, o.root, o.mac_key, o.reason, o.points, o.received_at FROM owed o
+			WHERE NOT EXISTS (SELECT 1 FROM crash_stars c WHERE c.member_id = o.member AND c.signature_id = o.root
+				AND c.mac_key = o.mac_key AND c.reason = o.reason AND c.kind = 'award')
Evidence
All five statuses are accepted on every decision; bogus creates a revoke row, while the settlement
exclusion checks for any prior award regardless of its net revocation.

service/internal/crashes/store.go[467-477]
service/internal/crashes/store.go[517-535]
service/internal/crashes/settle.go[72-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A signature can leave bogus status, but the ledger and settlement cannot restore its revoked awards.
## Fix Focus Areas
- service/internal/crashes/store.go[475-535]
- service/internal/crashes/settle.go[72-107]
- service/internal/db/migrations/030_crashes.sql[129-145]
## Recommended Fix
Define and implement a ledger-safe restoration when bogus triage is reversed, and test bogus followed by confirmed and another settlement.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Concurrent uploads exceed daily limits ✓ Resolved
Description
Submit checks SentSince before parsing and inserting, with no reservation or lock spanning the
count and insertion. Concurrent requests from one client or camera can all observe a count below its
limit and then all insert distinct events.
Code

service/internal/crashes/api.go[R92-98]

+	client := reports.Keyed(key, "client", httpx.ClientIP(r))
+	byClient, byCamera, err := a.Store().SentSince(ctx, client, macKey, a.now().Add(-24*time.Hour))
+	if err != nil {
+		a.fail(w, "the daily count", err)
+		return
+	}
+	if byClient >= DailyPerClient || byCamera >= DailyPerCamera {
Evidence
SentSince is a standalone count query and Insert starts its own transaction only after the
upload has passed the quota check.

service/internal/crashes/api.go[92-131]
service/internal/crashes/store.go[81-85]
service/internal/crashes/store.go[128-136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Daily upload quotas are checked outside the transaction that inserts an event, so concurrent requests can pass the same count.
## Fix Focus Areas
- service/internal/crashes/api.go[92-131]
- service/internal/crashes/store.go[81-118]
- service/internal/crashes/store.go[128-136]
## Recommended Fix
Serialize each client and camera quota check with its insertion using transaction-scoped locks or an atomic quota reservation. Add a concurrent-upload limit test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (3)
11. Long hardware fields cause server errors ✓ Resolved
Description
read permits up to 200 bytes for every text field, but the event table limits soc and sensor
to 100 characters. A valid-looking upload with either field between those limits reaches Insert,
fails its database check, and receives a retryable 500 response rather than an input error.
Code

service/internal/crashes/api.go[R205-213]

+		case "mac", "firmware", "majestic", "soc", "sensor":
+			b, err := io.ReadAll(io.LimitReader(p, maxField+1))
+			if err != nil {
+				return nil, http.StatusBadRequest, err
+			}
+			if len(b) > maxField {
+				return nil, http.StatusBadRequest, fmt.Errorf("%s: at most %d characters", name, maxField)
+			}
+			out.fields[name] = strings.TrimSpace(string(b))
Evidence
The multipart parser applies the shared 200-byte maxField, while the new table checks `length(soc)
<= 100 and length(sensor) <= 100`; insert failures go to the generic 500 handler.

service/internal/crashes/api.go[24-30]
service/internal/crashes/api.go[131-135]
service/internal/crashes/api.go[205-213]
service/internal/db/migrations/030_crashes.sql[73-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The upload parser accepts hardware fields longer than their database columns permit and returns a server error for them.
## Fix Focus Areas
- service/internal/crashes/api.go[205-213]
- service/internal/crashes/api.go[114-135]
- service/internal/db/migrations/030_crashes.sql[73-77]
## Recommended Fix
Validate SoC and sensor lengths against their 100-character database limits before insertion and return a client error. Add an upload test for a 101-character value.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Marking a merged bug bogus leaves old stars paid ✓ Resolved
Description
The revoke in Decide copies only awards WHERE signature_id = $1, the signature being marked
bogus. Stars paid under signatures that were later merged into it are booked under their own ids, so
they are never taken back, even though the PR says 'bogus takes back everything a signature paid'.
Code

service/internal/crashes/store.go[R530-535]

+		return tx.QueryRow(ctx, `WITH back AS (
+				INSERT INTO crash_stars (member_id, signature_id, mac_key, kind, reason, points)
+				SELECT member_id, signature_id, mac_key, 'revoke', reason, -points FROM crash_stars
+				WHERE signature_id = $1 AND kind = 'award'
+				ON CONFLICT DO NOTHING RETURNING points
+			) SELECT coalesce(-sum(points), 0)::int FROM back`, id).Scan(&taken)
Evidence
Settle records awards under the root at payment time (settle.go inserts d.signature, the root). A
child signature paid before being merged keeps awards with its own id. When the root is later marked
bogus, the revoke selects only signature_id = $1, so the children's awards stay. Marking the child
bogus has no effect on settlement either, because settlement checks root.status.

service/internal/crashes/store.go[527-535]
service/internal/crashes/settle.go[57-59]
service/internal/crashes/settle.go[105-107]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Marking a signature bogus revokes only awards booked under its own id. Awards under signatures merged into it remain paid.
## Fix Focus Areas
- service/internal/crashes/store.go[530-535]
## Recommended Fix
Change the revoke's source filter to `WHERE kind = 'award' AND signature_id IN (SELECT id FROM crash_signatures WHERE id = $1 OR merged_into = $1)`, keeping each award's own signature_id so ON CONFLICT stays correct. Add a test: pay A, merge A into B, mark B bogus, and check that A's stars are revoked.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Re-triaging a merged bug silently unmerges it ✓ Resolved
Description
Decide unconditionally writes merged_into = $6, treating an empty merge target as NULL, while
the /club triage form initializes that target to '' and the CLI supplies empty defaults for unset
fixed_in, issue_url, and note flags. Saving a status or note on an already-merged signature
through the UI or running openipc crashes status can split its events back into a separate public
row, while the CLI also erases previously recorded triage details.
Code

service/internal/crashes/store.go[R517-519]

+		if _, err := tx.Exec(ctx, `
+			UPDATE crash_signatures SET status = $2, fixed_in = $3, issue_url = $4, note = $5, merged_into = $6, updated_at = now()
+			WHERE id = $1`, id, t.Status, t.FixedIn, t.IssueURL, t.Note, merge); err != nil {
Evidence
Merged entries remain editable, but CrashTriage initializes merge with useState('') instead of
g.merged_into and submits it on save. In Decide, merge remains nil unless MergeInto is set,
and the UPDATE assigns it to merged_into unconditionally. The CLI passes *fixedIn, *issue, and
*note, whose unset defaults are empty, so a status change can erase an existing issue link and
other recorded values.

service/internal/crashes/store.go[499-519]
frontend/apps/site/src/components/club/CrashTriage.tsx[66-76]
service/cmd/openipc/crashes.go[40-44]
service/cmd/openipc/crashes.go[80-81]
frontend/apps/site/src/components/club/CrashTriage.tsx[44-56]
frontend/apps/site/src/components/club/CrashTriage.tsx[63-78]
service/internal/crashes/store.go[499-520]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Saving an already-merged signature without changing its merge target clears `merged_into`, and CLI status changes clear triage fields whose flags were not set.
## Fix Focus Areas
- service/internal/crashes/store.go[499-520]
- frontend/apps/site/src/components/club/CrashTriage.tsx[63-78]
- service/cmd/openipc/crashes.go[80-81]
## Recommended Fix
In `Decide`, preserve the existing merge target when none is supplied, for example with `merged_into = coalesce($6, merged_into)`, and provide an explicit unmerge operation such as `merge_into = "-"`. Initialize the `CrashTriage` merge field from `g.merged_into ?? ''`, or otherwise distinguish an unchanged merge from an explicit unmerge. In the CLI, update only fields whose flags were set (tracked with `fs.Visit`), or read the current row and retain its values for unset flags. Test saving a note on an already-merged signature.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Findings visible per group, which tucks the rest behind a View link

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread service/internal/crashes/api.go
Comment thread service/internal/crashes/api.go Outdated
Comment thread service/internal/crashes/store.go
Comment thread service/internal/crashes/store.go Outdated
Comment thread service/internal/crashes/parse.go Outdated
Comment thread service/internal/crashes/api.go Outdated
Comment thread service/internal/crashes/api.go
Comment thread service/internal/crashes/settle.go Outdated
Comment thread service/internal/crashes/store.go Outdated
Comment thread service/internal/crashes/store.go Outdated
…ner, keep the ledger net through merges

- Titles, trace reasons, the command line and the lines before a crash are
  redacted before they are stored, so a panic message carrying an address
  reaches neither the public list nor a maintainer's detail. Redaction finds
  IPv6 addresses and dotted MACs, and looks at an IPv4 address's neighbours
  instead of matching them, so ip=10.0.0.2:10.0.0.1 loses both.
- A SoC or sensor that is not a chip's name is refused (400), never stored.
- The same records are one crash per camera: two cameras' identical
  failsafe notes are two.
- MIPS's own "Oops[#1]:" header opens a trace, as do "CPU 0 Unable to
  handle..." lines.
- A crash with a MAC pays the camera's linked owner, whoever sent it, and a
  member sending from /club may name only a camera of theirs. Cameras are
  counted by MAC, or by sender address when there is none.
- The ledger is net per bug through merges: merging pays nothing again,
  bogus takes back what the bug and everything merged into it hold, undoing
  it pays again, and the monthly cap counts net stars. A signature merged
  into another is not marked bogus alone.
- A triage keeps the fields it does not send; the CLI changes only the flags
  given, and the /club form starts from the merge target.
- The daily limits are counted inside the insert, under the client's and the
  camera's locks.
@widgetii

widgetii commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Qodo review addressed in ecdb979. All 13 findings were real; each is fixed and has a test.

  1. Addresses in public titles and hardware fields. Trace reasons (and so titles), the command line and the lead-up lines are redacted before they are stored. A soc or sensor that is not a chip name (^[a-z0-9][a-z0-9._+-]{0,39}$) is refused with 400, and one parsed from the log in another shape is dropped. TestAddressesNeverReachThePublicListOrTheDetails.
  2. Addresses surviving redaction. Added IPv6 (full, or compressed with ::, never a time or a register dump) and dotted MACs. IPv4 now checks its neighbours instead of consuming them, so ip=10.0.0.2:10.0.0.1:10.0.0.254:… loses all three. TestRedact. A four-part version number is redacted along with IPs, by design; three- and five-part numbers are kept.
  3. One camera erasing another's report. Deduplication is UNIQUE (content_sum, mac_key), so two cameras' identical failsafe notes are two crashes. TestIdenticalRecordsFromTwoCameras.
  4. Native MIPS oops. Oops[#1]: and CPU 0 Unable to handle… now open a trace. pstore's own Oops#1 Part1 header is still not one. The test no longer rewrites the fixture.
    5, 7, 9, 12. Stars across merges and bogus decisions. The ledger no longer has a unique key. What a member holds is the net of their rows under the bug, through every signature merged into it.
    • Settlement pays only what is not held: per member and camera for reports, per bug for the first-reporter bonuses. So a merge pays nothing again.
    • bogus revokes everything still held under the bug and its merged signatures, each row under the signature it was booked to. Undoing it lets the settlement pay again.
    • The monthly cap counts net stars.
    • A signature merged into another cannot be marked bogus on its own; mark its target, or unmerge it first.
    • TestMergesAndStars, and the restore step at the end of TestSettle.
  5. Claiming another camera's star. A crash with a MAC pays the camera's linked owner, whoever sent it, and a /club upload may name only the member's own camera (400 otherwise). Cameras are counted by MAC, or by the sender's address when there is none.
    • Left open: an anonymous upload's MAC can't be verified (cameras have no credential). It can lift a signature's camera count, within 20 uploads a day per address, but it pays nobody.
    • TestACameraCrashPaysItsOwner and the club test.
  6. Raw values in maintainer details. Covered by the fix for 1.
  7. Concurrent uploads exceeding the limit. The daily limits are counted inside the insert transaction, under the camera's and the client's advisory locks. TestConcurrentUploadsKeepTheLimit sends 15 at once and gets exactly 5.
  8. Long hardware fields causing a 500. Now 400, by the same check as 1.
  9. Re-triage unmerging. Triage fields are pointers: left out keeps the value, "" clears it, and merge_into: "" unmerges. The CLI passes only the flags given, and the /club form starts from the current merge target.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Qodo review addressed in ecdb979. All 13 findings were real; each is fixed and has a test.

  1. Addresses in public titles and hardware fields. Trace reasons (and so titles), the command line and the lead-up lines are redacted before they are stored. A soc or sensor that is not a chip name (^[a-z0-9][a-z0-9._+-]{0,39}$) is refused with 400, and one parsed from the log in another shape is dropped. TestAddressesNeverReachThePublicListOrTheDetails.
  2. Addresses surviving redaction. Added IPv6 (full, or compressed with ::, never a time or a register dump) and dotted MACs. IPv4 now checks its neighbours instead of consuming them, so ip=10.0.0.2:10.0.0.1:10.0.0.254:… loses all three. TestRedact. A four-part version number is redacted along with IPs, by design; three- and five-part numbers are kept.
  3. One camera erasing another's report. Deduplication is UNIQUE (content_sum, mac_key), so two cameras' identical failsafe notes are two crashes. TestIdenticalRecordsFromTwoCameras.
  4. Native MIPS oops. Oops[#1]: and CPU 0 Unable to handle… now open a trace. pstore's own Oops#1 Part1 header is still not one. The test no longer rewrites the fixture.
    5, 7, 9, 12. Stars across merges and bogus decisions. The ledger no longer has a unique key. What a member holds is the net of their rows under the bug, through every signature merged into it.
    • Settlement pays only what is not held: per member and camera for reports, per bug for the first-reporter bonuses. So a merge pays nothing again.
    • bogus revokes everything still held under the bug and its merged signatures, each row under the signature it was booked to. Undoing it lets the settlement pay again.
    • The monthly cap counts net stars.
    • A signature merged into another cannot be marked bogus on its own; mark its target, or unmerge it first.
    • TestMergesAndStars, and the restore step at the end of TestSettle.
  5. Claiming another camera's star. A crash with a MAC pays the camera's linked owner, whoever sent it, and a /club upload may name only the member's own camera (400 otherwise). Cameras are counted by MAC, or by the sender's address when there is none.
    • Left open: an anonymous upload's MAC can't be verified (cameras have no credential). It can lift a signature's camera count, within 20 uploads a day per address, but it pays nobody.
    • TestACameraCrashPaysItsOwner and the club test.
  6. Raw values in maintainer details. Covered by the fix for 1.
  7. Concurrent uploads exceeding the limit. The daily limits are counted inside the insert transaction, under the camera's and the client's advisory locks. TestConcurrentUploadsKeepTheLimit sends 15 at once and gets exactly 5.
  8. Long hardware fields causing a 500. Now 400, by the same check as 1.
  9. Re-triage unmerging. Triage fields are pointers: left out keeps the value, "" clears it, and merge_into: "" unmerges. The CLI passes only the flags given, and the /club form starts from the current merge target.

Thanks for the detailed update. The fixes in ecdb979c address all 13 findings, including redaction, deduplication, parser coverage, authorization, quota concurrency, and merge-safe star settlement. The added regression tests cover the reported cases, including restoration after bogus triage.

@widgetii
widgetii merged commit ce684af into master Oct 8, 2026
4 checks passed
@widgetii
widgetii deleted the crash-reports branch October 8, 2026 16:37
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.

2 participants