Skip to content

Club: majestic's own crashes, symbolized against its CI's debuginfo - #415

Merged
openipc-ai merged 3 commits into
masterfrom
majestic-crashes
Oct 9, 2026
Merged

openipc-ai merged 3 commits into
masterfrom
majestic-crashes

Conversation

@openipc-ai

Copy link
Copy Markdown
Collaborator

Kernel crashes reach the Club already (#411, #413). This adds the streamer's own crashes. majestic now writes /etc/crash/majestic.dump when it dies of SIGSEGV, SIGBUS, SIGILL, SIGFPE or SIGABRT. The dump holds the registers at the fault, a slice of the faulting thread's stack, the memory map, and each loaded module's build-id. A release build has no unwind tables, so the camera can't produce its own backtrace; the site produces it.

Ingest (internal/crashes)

  • What counts: a bundle carrying majestic.dump, either in a tar.gz with meta.json or as the bare body, is filed as class user, kind signal (migration 032).
  • Who sees it: maintainers only. It is never on /crashes or /api/v1/crashes/{id}. The sender and the member's own list show the signal (SIGSEGV (NULL pointer)), never where in majestic it happened.
  • On arrival: it is filed under a provisional signature (the faulting instruction's module and offset, plus the build-id). The raw dump waits in crash_dumps. crash_bundles keeps the dump with its stack removed.
  • Self-inflicted: a signal sent by another process (si_code <= 0, and the new sender= in the header is not majestic's own pid) counts as caused and pays nothing. majestic aborting itself counts as a bug.

Symbolizer (internal/crashes/symbolize, firmware role)

  • Trigger: NOTIFY crash_symbolize, plus a 10-minute sweep, one dump at a time.
  • Core: an ELF core written in Go from the dump: NT_PRSTATUS in each architecture's elf_gregset_t order (ARM, MIPS, x86_64, AArch64), NT_AUXV to place the PIE, the stack, and the modules' code.
  • gdb: gdb-multiarch loads the executable and debuginfo majestic's CI publishes by build-id (MAJESTIC_SYMBOLS_BASE, the S3 buildid/ tree). A small Python script prints frames as JSON. Host init files, debuginfod and auto-load are all off.
  • Library frames: named from the firmware build's own rootfs. The build and platform come from meta.json. The rootfs is taken from that build's release tarball via the existing release cache, with unsquashfs extracting lib and usr/lib only. Symlinks are followed inside the image, never into the host's /lib.
  • Probable callers: where gdb stops (most camera libraries ship no unwind tables), a scan of the stack from that frame's SP keeps only return addresses that follow a call instruction. These frames are marked probable, and count toward the signature only when fewer than two unwound frames have names.
  • After it succeeds: the event is refiled under its backtrace's signature. This is the package's one change to an event, made with the guard off, as Takedown does. The provisional signature is deleted, or merged into the new one if stars or a triage were booked under it. The dump is deleted.
  • When it can't: retried with backoff over about four days (debuginfo not published yet, gdb error), then given up and the dump deleted.
  • Cache: RELEASE_CACHE_ROOT/symbols, swept after 30 days unused. There is no new volume.

UI

/club/crashes shows the backtrace with file:line, probable frames marked, and the symbolization status.

Tests

  • Fixtures: a stand-in program (testdata/dump/toy.c, with gen.sh) that writes the same dump. It faulted on the lab gk7205v300 (ARM, musl, Thumb-2, -Os, no unwind tables in the executable) and on x86_64, in four ways: its own code, a stripped library, libc, and raise(). The fixtures were generated in an empty environment and checked to hold no host or lab details.
  • End to end: gdb unwinds store ← parse_level ← main at their lines. The stripped library's frame is named from its dynamic symbols, with the caller below it. The libc frame is named by its module.
  • Storage: maintainers-only visibility, the stack-less bundle, refiling, provisional cleanup, stars booked before symbolization, give-up, and the rootfs path from the builds tables through unsquashfs.
  • CI: installs gdb-multiarch and squashfs-tools. These tests fail rather than skip when CI is set, as the ffmpeg ones do.

Checked locally: Go vet and tests (all packages), frontend lint, typecheck and 710 tests, and the locale export.

Depends on majestic's recorder landing in a nightly. The WebUI sender and the firmware's respawn come after this.

majestic now leaves /etc/crash/majestic.dump when it dies of a fatal
signal: the registers at the fault, a slice of the stack, the memory map
and each module's build-id. A bundle carrying one is filed as class
'user', kind 'signal' (migration 032), maintainers' only: never on
/crashes, and its sender is told the signal, not where.

It arrives under a provisional signature (module and offset), with its
dump in crash_dumps. The firmware role's new symbolize package, woken by
NOTIFY crash_symbolize, writes an ELF core from the dump and unwinds it
with gdb-multiarch against the executable and debuginfo majestic's CI
publishes by build-id. Library frames are named from the firmware build's
own rootfs, unsquashed from its release tarball. Where gdb stops (most
camera libraries have no unwind tables) a scan of the stack for return
addresses that follow a call finds the probable callers. The event is
then refiled under its backtrace's signature and the dump, being
majestic's memory, is deleted.

Tested against dumps of a stand-in program that faulted on a gk7205v300
and on x86_64: in its own code, in a stripped library and in libc.
The service image and CI gain gdb-multiarch and squashfs-tools.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Ingest and symbolize majestic crashes for maintainer triage

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

Grey Divider

AI Description

• Accept majestic signal dumps as maintainer-only crashes without exposing their stack memory or
 backtraces publicly.
• Symbolize dumps against build-matched debuginfo and firmware libraries, then refile events under
 backtrace signatures.
• Add triage backtraces, retry handling, cross-architecture fixtures, and CI symbolization
 dependencies.
Diagram

graph TD
  CAM["Camera dump"] --> API["Crash API"] --> DB[("Crash tables")] --> WORKER["Firmware worker"] --> GDB["gdb-multiarch"]
  SYMBOLS["CI debuginfo"] --> WORKER
  RELEASE["Release cache"] --> WORKER
  GDB --> WORKER --> DB --> UI["Club triage"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Separate symbolization service
  • ➕ Isolates gdb and untrusted dumps from the firmware role.
  • ➕ Allows independent resource limits and scaling.
  • ➖ Adds deployment, artifact-sharing, and queue coordination overhead.
  • ➖ Duplicates access to the existing release cache or requires another artifact path.
2. Unwind on the camera
  • ➕ Avoids server-side core construction and gdb processing.
  • ➖ Release builds lack the unwind information needed for reliable backtraces.
  • ➖ Would put debuginfo or heavier tooling on constrained cameras.

Recommendation: Keep server-side symbolization in the firmware role: it can reuse the release cache and CI debuginfo without adding camera requirements or another service. Consider process-level isolation and resource limits for gdb if dump volume or trust risk grows.

Files changed (45) +3559 / -53

Enhancement (17) +2359 / -48
CrashTriage.tsxShow symbolized backtraces in maintainer triage +35/-10

Show symbolized backtraces in maintainer triage

• Displays source locations, probable callers, and symbolization status. Omits kernel-only fields when a signal crash has none.

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

Crashes.tsxKeep member signal reports unlinked from public crashes +7/-2

Keep member signal reports unlinked from public crashes

• Renders signal-crash titles as plain text in submission feedback and the member list instead of linking to the public list.

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

crashes.tsExtend frontend crash types for signal backtraces +25/-4

Extend frontend crash types for signal backtraces

• Adds the user class, signal kind, source locations, probable-frame markers, and symbolization details to API types.

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

main.goStart the symbolizer in the firmware role +10/-0

Start the symbolizer in the firmware role

• Wires the worker to the database, shared release cache, and build-id symbol source.

service/cmd/openipc/main.go

api.goFile signal crashes without retaining their stack in bundles +24/-5

File signal crashes without retaining their stack in bundles

• Stores a stack-less bundle while passing the raw dump to temporary storage. Returns only the signal and a Club URL to the sender.

service/internal/crashes/api.go

dump.goParse and classify majestic signal dumps +442/-0

Parse and classify majestic signal dumps

• Reads versioned MJCD sections and architecture-specific registers, derives provisional module-offset signatures and signal titles, and strips stack sections from retained bundles.

service/internal/crashes/dump.go

parse.goRecognize signal dumps in crash bundles +83/-15

Recognize signal dumps in crash bundles

• Accepts majestic.dump in archives or as a bare body and gives it precedence over kernel records. Adds signal classification and frame metadata.

service/internal/crashes/parse.go

store.goPersist and restrict signal-crash records +43/-12

Persist and restrict signal-crash records

• Creates pending dump and symbolization records on insertion, excludes user-class signatures from public queries, and exposes backtrace status in maintainer details. Member lists show only the signal.

service/internal/crashes/store.go

core.goConstruct ELF cores from majestic dumps +239/-0

Construct ELF cores from majestic dumps

• Writes architecture-specific register notes, auxiliary-vector data, stack memory, and executable segments into cores that gdb can inspect.

service/internal/crashes/symbolize/core.go

frames.pyEmit machine-readable gdb frames +56/-0

Emit machine-readable gdb frames

• Provides embedded gdb commands that print unwound frames and resolved addresses as JSON without reading frame arguments.

service/internal/crashes/symbolize/frames.py

gdb.goRun isolated, bounded gdb sessions +165/-0

Run isolated, bounded gdb sessions

• Invokes gdb-multiarch with a timeout, controlled environment, and host init, debuginfod, and auto-load disabled. Parses the embedded script's frame output.

service/internal/crashes/symbolize/gdb.go

scan.goFind probable callers beyond failed unwinds +133/-0

Find probable callers beyond failed unwinds

• Scans stack words for return addresses preceded by architecture-specific call instructions and bounds the number of candidates.

service/internal/crashes/symbolize/scan.go

sources.goFetch build-matched symbols and firmware libraries +339/-0

Fetch build-matched symbols and firmware libraries

• Caches build-id artifacts, extracts only libraries from a matching firmware release, confines symlink resolution to the image, and sweeps unused cache entries.

service/internal/crashes/symbolize/sources.go

symbolize.goTurn dumps into named backtraces +312/-0

Turn dumps into named backtraces

• Combines build-matched executable and library code with a generated core, gdb unwinding, and probable-caller scanning. Names frames and records the sources used.

service/internal/crashes/symbolize/symbolize.go

worker.goProcess pending dumps with notification and sweep +161/-0

Process pending dumps with notification and sweep

• Runs one-at-a-time symbolization after database notifications or periodic checks, records failures for retry, and sweeps unused artifacts.

service/internal/crashes/symbolize/worker.go

symbolized.goRefile symbolized crashes transactionally +238/-0

Refile symbolized crashes transactionally

• Builds backtrace signatures, moves events from provisional signatures, preserves booked triage or stars, and deletes raw dumps after success or retry exhaustion.

service/internal/crashes/symbolized.go

032_crash_symbolizations.sqlAdd temporary dumps and symbolization state +47/-0

Add temporary dumps and symbolization state

• Extends crash classifications with user signals and adds tables for bounded raw dumps, backtraces, retry state, and pending-work lookup.

service/internal/db/migrations/032_crash_symbolizations.sql

Tests (19) +1130 / -0
Crashes.test.tsxTest private signal-crash presentation +15/-0

Test private signal-crash presentation

• Verifies that a member sees the signal and no link to the public crash list.

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

dump_test.goTest dump ingestion, privacy, and refiling +382/-0

Test dump ingestion, privacy, and refiling

• Covers dump validation, bare and archived submissions, visibility, stack removal, retry exhaustion, signature cleanup, and stars retained across refiling.

service/internal/crashes/dump_test.go

rootfs_test.goTest firmware rootfs library extraction +114/-0

Test firmware rootfs library extraction

• Builds a release-cache fixture and verifies build lookup, limited unsquashfs extraction, in-image symlink handling, and cache reuse.

service/internal/crashes/symbolize/rootfs_test.go

symbolize_test.goExercise cross-architecture symbolization +364/-0

Exercise cross-architecture symbolization

• Tests ARM and x86_64 unwinds, source lines, stripped-library frames, missing-library fallback, call scanning, core segments, and path confinement.

service/internal/crashes/symbolize/symbolize_test.go

arm-lib.dumpAdd ARM stripped-library crash fixture +0/-0

Add ARM stripped-library crash fixture

• Captures a stand-in program faulting inside a library on ARM for dump and frame-resolution tests.

service/internal/crashes/testdata/dump/arm-lib.dump

arm-libc.dumpAdd ARM libc crash fixture +0/-0

Add ARM libc crash fixture

• Captures an ARM fault inside libc to test module attribution when the library is unavailable.

service/internal/crashes/testdata/dump/arm-libc.dump

arm-own.dumpAdd ARM executable crash fixture +0/-0

Add ARM executable crash fixture

• Captures an ARM fault in the stand-in executable for register, core, and source-line unwind tests.

service/internal/crashes/testdata/dump/arm-own.dump

arm-sent.dumpAdd ARM self-sent signal fixture +0/-0

Add ARM self-sent signal fixture

• Captures a raised signal to test sender-aware classification.

service/internal/crashes/testdata/dump/arm-sent.dump

toyAdd ARM fixture executable +0/-0

Add ARM fixture executable

• Provides the stripped stand-in executable associated with the ARM dumps.

service/internal/crashes/testdata/dump/arm/toy

toy.debugAdd ARM fixture debuginfo +0/-0

Add ARM fixture debuginfo

• Provides build-matched symbols and source lines for ARM gdb tests.

service/internal/crashes/testdata/dump/arm/toy.debug

gen.shDocument fixture generation commands +38/-0

Document fixture generation commands

• Builds the stand-in program and separated debuginfo with reproducible source paths; documents how target-generated dumps were collected.

service/internal/crashes/testdata/dump/gen.sh

libtoy.cAdd stripped-library fault source +9/-0

Add stripped-library fault source

• Defines the library fault used to test dynamic-symbol naming without unwind tables.

service/internal/crashes/testdata/dump/libtoy.c

toy.cAdd stand-in MJCD dump recorder +208/-0

Add stand-in MJCD dump recorder

• Records real faults and raised signals in majestic's dump format across the supported architecture families for test fixture generation.

service/internal/crashes/testdata/dump/toy.c

x86_64-lib.dumpAdd x86_64 stripped-library crash fixture +0/-0

Add x86_64 stripped-library crash fixture

• Captures a stand-in program faulting inside a library on x86_64.

service/internal/crashes/testdata/dump/x86_64-lib.dump

x86_64-libc.dumpAdd x86_64 libc crash fixture +0/-0

Add x86_64 libc crash fixture

• Captures a fault inside libc for x86_64 dump coverage.

service/internal/crashes/testdata/dump/x86_64-libc.dump

x86_64-own.dumpAdd x86_64 executable crash fixture +0/-0

Add x86_64 executable crash fixture

• Captures a fault in the stand-in executable for x86_64 register and unwind tests.

service/internal/crashes/testdata/dump/x86_64-own.dump

x86_64-sent.dumpAdd x86_64 self-sent signal fixture +0/-0

Add x86_64 self-sent signal fixture

• Captures a raised signal for x86_64 sender-classification tests.

service/internal/crashes/testdata/dump/x86_64-sent.dump

toyAdd x86_64 fixture executable +0/-0

Add x86_64 fixture executable

• Provides the stripped stand-in executable associated with x86_64 dumps.

service/internal/crashes/testdata/dump/x86_64/toy

toy.debugAdd x86_64 fixture debuginfo +0/-0

Add x86_64 fixture debuginfo

• Provides build-matched symbols and source lines for x86_64 gdb tests.

service/internal/crashes/testdata/dump/x86_64/toy.debug

Documentation (6) +56 / -0
CLAUDE.mdDocument the majestic crash lifecycle +15/-0

Document the majestic crash lifecycle

• Records maintainer-only visibility, provisional filing, server-side symbolization, dump deletion, retries, and cache retention.

CLAUDE.md

boards.en.ymlAdd English signal-crash labels +5/-0

Add English signal-crash labels

• Adds copy for majestic crashes, backtraces, probable frames, and pending or failed symbolization.

data/locales/boards.en.yml

boards.ru.ymlAdd Russian signal-crash labels +5/-0

Add Russian signal-crash labels

• Adds Russian copy for majestic crashes, backtraces, probable frames, and symbolization states.

data/locales/boards.ru.yml

boards.en.jsonExport English crash translations +5/-0

Export English crash translations

• Adds generated English keys for signal crashes, backtrace display, probable frames, and symbolization status.

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

boards.ru.jsonExport Russian crash translations +5/-0

Export Russian crash translations

• Adds generated Russian keys for signal crashes, backtrace display, probable frames, and symbolization status.

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

CRASH.mdDocument sending and handling majestic dumps +21/-0

Document sending and handling majestic dumps

• Explains accepted dump bundles, response fields, maintainer-only access, temporary stack retention, and caused-signal treatment.

service/internal/crashes/CRASH.md

Other (3) +14 / -5
build.ymlInstall symbolization tools in CI +5/-3

Install symbolization tools in CI

• Adds gdb-multiarch and squashfs-tools so unwind and rootfs extraction tests run rather than skip in CI.

.github/workflows/build.yml

DockerfilePackage gdb and rootfs extraction tools +5/-2

Package gdb and rootfs extraction tools

• Installs gdb-multiarch and squashfs-tools in the service runtime image for symbolization.

service/Dockerfile

config.goConfigure the majestic symbols source +4/-0

Configure the majestic symbols source

• Adds MAJESTIC_SYMBOLS_BASE with a default build-id artifact URL.

service/internal/config/config.go

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

qodo-free-for-open-source-projects Bot commented Oct 9, 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. Merged crashes expose private crash data ✓ Resolved
Description
ranked excludes a user-class root from public results but does not exclude user-class events
aggregated under a public root. If a maintainer merges a signal signature into a kernel signature,
public crash endpoints include the signal events in counts, hardware and firmware summaries, and
timing.
Code

service/internal/crashes/store.go[250]

+		WHERE ($1 OR (r.merged_into IS NULL AND r.status <> 'bogus' AND r.class <> 'user')) AND ($2 = '' OR r.id = $2)
Evidence
The event CTE aggregates by merged root without checking event class; the new predicate checks only
the root. Public combinations use the same merged-root behavior, and the existing merge operation
permits different classes.

service/internal/crashes/store.go[234-250]
service/internal/crashes/store.go[367-381]
service/internal/crashes/store.go[562-585]
service/internal/crashes/api.go[376-418]

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

## Issue description
Merging a signal signature into a public signature makes private event data contribute to public crash aggregates.
## Fix Focus Areas
- service/internal/crashes/store.go[234-250]
- service/internal/crashes/store.go[367-381]
## Recommended Fix
Filter user-class events from every public aggregate, including ranking and combinations, regardless of the merged root's class. Add a test that merges a signal signature into a public signature.

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


2. Symbolization loses crash triage decisions ✓ Resolved
Description
Symbolized moves an event to a new signature, while leave merges its provisional signature
without transferring its triage status to the new root. When a maintainer marks the provisional
signature bogus before symbolization, settlement can subsequently pay that crash under the new open
root; confirmed and fixed decisions likewise stop governing it.
Code

service/internal/crashes/symbolized.go[R141-143]

+		if provisional != sig {
+			if err := leave(ctx, tx, provisional, sig); err != nil {
+				return err
Evidence
Refiling changes the event's signature, but cleanup only changes merged_into. Ranking and
settlement read status from the resulting root, so a decision recorded on the provisional signature
no longer controls the event.

service/internal/crashes/symbolized.go[126-143]
service/internal/crashes/symbolized.go[157-188]
service/internal/crashes/store.go[234-252]
service/internal/crashes/settle.go[48-66]

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

## Issue description
Refiling a symbolized event leaves its provisional triage decision behind, allowing a bogus crash to become payable.
## Fix Focus Areas
- service/internal/crashes/symbolized.go[126-143]
- service/internal/crashes/symbolized.go[157-188]
## Recommended Fix
Preserve or reconcile the provisional signature's triage decision with the destination root within the refiling transaction. Test symbolization after bogus, confirmed, and fixed decisions, including settlement afterward.

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


3. A crafted dump can crash-loop the firmware role ✓ Resolved
Description
Scan computes start := int(from - d.StackAt). When the gdb frame's SP is more than 2^63 above
StackAt (a garbage or attacker-supplied SP on x86_64 or AArch64), start becomes negative,
d.Stack[i:] panics, and nothing recovers the panic in Worker.Run. Dumps come in through the
unauthenticated POST /api/v1/crashes, and the dump stays pending, so every restart of the
firmware role picks it up again and panics again.
Code

service/internal/crashes/symbolize/scan.go[R49-51]

+	start := int(from - d.StackAt)
+	start += (size - start%size) % size
+	for i := start; i+size <= len(d.Stack) && len(out) < maxCandidates; i += size {
Evidence
Symbolize passes last.SP (the newest frame's SP is the dump's own register value when gdb stops
at frame 0) as from. Scan only clamps from from below. Converting a uint64 difference above
MaxInt64 to int gives a negative index into d.Stack. The worker goroutine is started with `go
sym.Run(bg)` and has no recover. A failed attempt is never recorded, so the dump stays due.

service/internal/crashes/symbolize/scan.go[39-57]
service/internal/crashes/symbolize/symbolize.go[139-145]
service/internal/crashes/symbolize/worker.go[105-124]
service/cmd/openipc/main.go[637-640]
service/internal/crashes/api.go[60-63]

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

## Issue description
`Scan` converts `from - d.StackAt` to int with no bound. A huge SP makes the index negative and panics the symbolizer goroutine, which takes the whole firmware role down. The dump stays pending, so the panic repeats after every restart.
## Fix Focus Areas
- service/internal/crashes/symbolize/scan.go[39-51]
- service/internal/crashes/symbolize/worker.go[122-147]
## Recommended Fix
In `Scan`, return nil when `from - d.StackAt >= uint64(len(d.Stack))`, before converting to int. In `Worker.one`, add a `defer` with `recover()` that turns a panic into a `SymbolizeFailed` call, so one bad dump cannot kill the process or be retried forever.

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



Remediation recommended

4. Temporary library failures lose backtraces ✓ Resolved
Description
Symbolize records a Rootfs.Dir error in sources but continues without the firmware libraries
and returns success if gdb produces frames. When a build or release is temporarily unavailable, the
worker marks that incomplete result done and deletes the dump, preventing a retry after the
libraries become available.
Code

service/internal/crashes/symbolize/symbolize.go[R72-75]

+		root, err := s.Rootfs.Dir(ctx, fw.Build, fw.Platform)
+		if err != nil {
+			sources["libraries_error"] = err.Error()
+		} else {
Evidence
An empty library map omits library code and names, but the worker's nil-error branch commits the
result. That transaction sets status to done and removes the only dump from which a better trace
could be made.

service/internal/crashes/symbolize/symbolize.go[69-99]
service/internal/crashes/symbolize/symbolize.go[247-264]
service/internal/crashes/symbolize/worker.go[122-146]
service/internal/crashes/symbolized.go[146-151]

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 temporary failure to obtain firmware libraries is treated as successful symbolization, permanently retaining an incomplete backtrace.
## Fix Focus Areas
- service/internal/crashes/symbolize/symbolize.go[69-85]
- service/internal/crashes/symbolize/worker.go[122-146]
## Recommended Fix
Return retryable rootfs acquisition errors rather than committing a successful result without required libraries. Cover a failed first lookup followed by a successful retry.

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


5. Parallel refiling leaves empty signatures ✓ Resolved
Description
leave skips provisional-signature cleanup whenever it sees another event still filed there, but
Symbolized locks only the event it is moving. If two worker instances symbolize events sharing a
provisional signature concurrently, each can see the other's unmoved event and commit, leaving an
empty signature visible in maintainer triage.
Code

service/internal/crashes/symbolized.go[R168-170]

+	if events > 0 {
+		return nil
+	}
Evidence
The row lock covers only the selected event and symbolization row, while cleanup counts every event
under the provisional signature. Maintainer ranking includes signatures even when that count
ultimately reaches zero.

service/internal/crashes/symbolized.go[111-119]
service/internal/crashes/symbolized.go[137-173]
service/internal/crashes/store.go[249-253]

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

## Issue description
Concurrent refilings can each skip cleanup because each sees the other's event under the shared provisional signature.
## Fix Focus Areas
- service/internal/crashes/symbolized.go[111-143]
- service/internal/crashes/symbolized.go[157-173]
## Recommended Fix
Serialize refiling by provisional signature before moving events and checking for remaining events, or otherwise make final cleanup safe against concurrent moves. Add a two-event concurrent refiling test.

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


6. A dump that can't be stored spins the symbolizer ✓ Resolved
Description
When st.Symbolized or st.SymbolizeFailed returns an error, Worker.one only logs and returns,
leaving the row pending with its old next_try. Drain loops while DueDumps returns rows, so a
persistent failure (for example a \u0000 in a gdb or unsquashfs message, which Postgres refuses in
jsonb or text) re-runs gdb on the same dump in a tight loop indefinitely.
Code

service/internal/crashes/symbolize/worker.go[R126-130]

+		sig, err := st.Symbolized(ctx, d.EventID, res.Frames, res.Sources, w.now())
+		if err != nil {
+			w.Log.Error("crashes: symbolized, not stored", "id", d.EventID, "err", err)
+			return
+		}
Evidence
DueDumps picks rows where status is pending and next_try <= now. Only SymbolizeFailed moves
next_try, and it is not called when Symbolized fails. Drain's loop runs again at once and gets
the same row back. The sources map holds raw error and gdb text (libraries_error, resolve_error,
gdb_stopped), which is marshalled into jsonb.

service/internal/crashes/symbolize/worker.go[105-147]
service/internal/crashes/symbolized.go[32-39]
service/internal/crashes/symbolized.go[99-155]
service/internal/crashes/symbolize/symbolize.go[72-75]

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

## Issue description
If storing a symbolization (or its failure) keeps failing, the dump stays due and `Drain` re-runs gdb on it with no delay.
## Fix Focus Areas
- service/internal/crashes/symbolize/worker.go[105-147]
- service/internal/crashes/symbolized.go[196-228]
## Recommended Fix
When `Symbolized` fails, call `SymbolizeFailed` so the retry counter and backoff apply. Have `Drain` remember which event IDs it has already handled in this pass and stop when a batch holds nothing new. Strip NUL bytes from the reason and from the sources strings before writing them to Postgres.

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


View review recommended (1)
7. Unrelated majestic crashes merge into one bug ✓ Resolved
Description
userSigFrames drops every frame named ?, including library frames that carry only a module. When
no unwound or probable frame has a name (static functions in a vendor library, a rootfs that wasn't
found, a thread with no majestic frames), Trace.Signature falls back to class + Reason alone.
Every such crash, on any SoC, build or library, is then refiled under one SIGSEGV (NULL pointer)
signature, and the provisional module+offset identity is replaced by something less specific; triage
and the first-reporter stars go to the wrong bug.
Code

service/internal/crashes/symbolized.go[R67-78]

+func userSigFrames(frames []Frame) []Frame {
+	var out []Frame
+	for _, f := range frames {
+		if f.Probable || f.Fn == "?" || userGlue[f.Fn] {
+			continue
+		}
+		out = append(out, f)
+	}
+	if len(out) < 2 {
+		for _, f := range frames {
+			if !f.Probable || f.Fn == "?" || userGlue[f.Fn] {
+				continue
Evidence
Signature() writes the reason only when frames (after userSigFrames) is empty. namer.frame
returns Fn: "?" for library frames it can't name and for addresses outside any module.
Symbolized always refiles under the new signature and deletes or merges the provisional one, so
the more specific signature is lost.

service/internal/crashes/parse.go[103-125]
service/internal/crashes/symbolize/symbolize.go[247-265]
service/internal/crashes/symbolized.go[122-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
When every frame is `?`, the symbolized signature is just the signal reason, so distinct crashes collapse into one.
## Fix Focus Areas
- service/internal/crashes/symbolized.go[67-90]
- service/internal/crashes/symbolized.go[122-145]
## Recommended Fix
In `userSigFrames`, when the result is empty, fall back to the unnamed frames with their module (`? [libfoo.so]`). Or have `Symbolized` keep the event under its provisional signature (mark it done without refiling) when the backtrace has no named frame.

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



Informational

8. A checksum file can be unpacked as the rootfs ✓ Resolved
Description
rootfsOf takes the first tar member whose base name starts with rootfs.squashfs, which also
matches rootfs.squashfs..md5sum. When the checksum entry comes before the image, the 40-byte
checksum is handed to unsquashfs, which fails, and library frames for that firmware build are
never named.
Code

service/internal/crashes/symbolize/sources.go[R260-262]

+		if h.Typeflag == tar.TypeReg && strings.HasPrefix(filepath.Base(h.Name), "rootfs.squashfs") {
+			return writeAtomic(dest, tr, 256<<20)
+		}
Evidence
The existing firmware tarball reader explicitly skips .md5sum members, and its test fixture puts
rootfs.squashfs..md5sum beside the image. The new matcher does not exclude them.

service/internal/firmware/build.go[338-341]
service/internal/firmware/firmware_test.go[167-167]
service/internal/crashes/symbolize/sources.go[241-264]

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

## Issue description
`rootfsOf` can pick `rootfs.squashfs.<soc>.md5sum` as the squashfs image.
## Fix Focus Areas
- service/internal/crashes/symbolize/sources.go[260-262]
## Recommended Fix
Add `&& !strings.HasSuffix(h.Name, ".md5sum")` to the match. Also accept `h.Typeflag == 0`, as `firmware/build.go` does.

ⓘ 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 show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread service/internal/crashes/store.go
Comment thread service/internal/crashes/symbolized.go Outdated
Comment thread service/internal/crashes/symbolize/symbolize.go Outdated
Comment thread service/internal/crashes/symbolized.go
Comment thread service/internal/crashes/symbolize/scan.go
Comment thread service/internal/crashes/symbolize/worker.go Outdated
Comment thread service/internal/crashes/symbolized.go
Comment thread service/internal/crashes/symbolize/sources.go Outdated
- The public list and a signature's public page never count majestic's
  events, whatever they are merged into; a kernel signature and majestic's
  cannot be merged.
- A triage decided under a provisional signature goes with the crash to
  the bug it is refiled under when that one is undecided; a crash found
  bogus while it waited is not refiled.
- A backtrace with no frame of majestic's own leaves the crash under its
  provisional signature: strlen alone would make every caller one bug.
- Events leaving one provisional signature lock it, so the last one out
  deletes it.
- What gdb, unsquashfs or a library's symbols say is stripped of NULs and
  invalid UTF-8 before PostgreSQL sees it; a dump whose result cannot be
  stored counts as a failed try, and the drain stops until the next tick.
- A scan start off the stack returns nothing (a crafted SP panicked it),
  and a panic in the symbolizer is that dump's failure, not the role's.
- A firmware build that cannot be had right now is waited for; the last
  try makes the backtrace without its libraries.
- The rootfs image is rootfs.squashfs[.<soc>], never its .md5sum.
@openipc-ai

Copy link
Copy Markdown
Collaborator Author

Addressed Qodo's review in a4365d5, with a test for each:

  1. Private data via merges: the public ranking and /api/v1/crashes/{id} exclude kind = 'signal' events at the event level. Decide also refuses to merge across classes, in either direction.
  2. Triage lost on refile: when the provisional signature carries a decision and the root it moves to is open, the decision is copied there, with a crash_triage row by symbolizer. A crash marked bogus before symbolization is not refiled.
  3. Crafted SP: Scan returns nothing for a start outside the stack slice. Worker also turns a panic in the symbolizer into a failed try for that dump.
  4. Transient library failure: a Rootfs error other than ErrNoBuild fails the try and it is retried. Only the last try (crashes.LastTry) settles for a backtrace without libraries.
  5. Parallel refiling: Symbolized locks the provisional signature row before refiling, so the last event to leave deletes it. Tested with four concurrent refiles.
  6. Spin: frames, sources and errors are stripped of NUL and invalid UTF-8. A result that can't be stored counts as a failed try, and a store failure stops the drain until the next tick.
  7. Over-merging: a crash is refiled only when its signature frames include one of majestic's own (Module == ""). Otherwise it keeps the more specific provisional identity (module, offset, build).
  8. Checksum as rootfs: the image must match ^rootfs\.squashfs(\.<soc>)?$.

The redactor took OpenIPC's own version, 2.6.10.05, for a dotted quad and
replaced it in meta.json with a keyed hash, so a maintainer could not read
which firmware a camera ran. An octet is never written with a leading
zero; now the pattern refuses one. The triage page also says it holds
majestic's crashes beside the kernel's.
@openipc-ai
openipc-ai merged commit 7f3e551 into master Oct 9, 2026
4 checks passed
@openipc-ai
openipc-ai deleted the majestic-crashes branch October 9, 2026 04:23
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.

1 participant