Das Vorschaubild entstand in einer nackten Goroutine. Chis Recoverer haengt
am Anfragebehandler und sieht davon nichts -- eine Panik im Bilddekodierer
nahm den ganzen Go-Prozess mit, ausgeloest durch einen gewoehnlichen
Upload. Die Bibliothek ist seit ef60497 aktuell; das schloss **diese**
Luecke. Dies hier schliesst die **Gattung**.
pkg/bg mit zwei Funktionen, und der Unterschied ist keine Feinheit:
* bg.Go(name, fn) -- Goroutine, die eine Panik ueberlebt
* bg.Run(name, fn) -- dasselbe ohne eigene Goroutine, fuer Schleifen
Faengt man eine Panik um den **ganzen** Arbeiter ab, ueberlebt der Prozess
-- aber der Arbeiter ist tot, und von da an wird still keine Datei mehr
erkannt. Der OCR-Arbeiter sichert deshalb jeden **Auftrag** einzeln ab.
Umgestellt: Vorschaubild, Bestand nachtragen, Freigabe-Zaehler,
Speicherbereinigung, OCR-Arbeiter. Eine nackte Goroutine bleibt mit
Begruendung im Code -- die Signalbehandlung; ein recover braechte dort
nichts, weil main ohnehin wartete.
Aufgefangene Paniken werden mit Stapel und Herkunft protokolliert. Vier
Tests, darunter der wichtige: Eine Schleife laeuft nach einer Panik im
dritten Durchgang weiter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
171 lines
7.3 KiB
Markdown
171 lines
7.3 KiB
Markdown
# Bestandsaufnahme, 12.09.2026
|
||
|
||
Einmal durch die ganze Cloud: Code, Sicherheit, offene Enden. Geordnet nach
|
||
dem, was passieren kann — nicht nach Aufwand.
|
||
|
||
Was hier steht, ist **gemessen**, nicht geschätzt: Routen aus `main.go`
|
||
extrahiert und gegen die Rechteprüfungen in den Handlern abgeglichen,
|
||
`govulncheck` über den ganzen Baum, Zeilen und Testumfang je Paket gezählt.
|
||
|
||
---
|
||
|
||
## 1 · Ein Bild-Upload kann den Prozess töten
|
||
|
||
**Schwere: hoch. Jeder Kontoinhaber, normale Bedienung.**
|
||
|
||
`cmd/aether/main.go:702` erzeugt das Vorschaubild in einer **nackten
|
||
Goroutine**:
|
||
|
||
```go
|
||
go func() {
|
||
…
|
||
thumbnails.Generate(ctx, st, …) // dekodiert eine hochgeladene Datei
|
||
}()
|
||
```
|
||
|
||
Chis `Recoverer` hängt am Anfragebehandler. Eine Panik in einer *eigenen*
|
||
Goroutine fängt er nicht — sie nimmt den **ganzen Go-Prozess** mit. Docker
|
||
startet ihn neu (`restart: unless-stopped`), aber jeder gerade laufende Upload
|
||
stirbt mit, und wer die Datei noch einmal hochlädt, wiederholt das Spiel.
|
||
|
||
Die passende Lücke war offen: **GO-2026-5061**, Panik im WebP-Dekodierer bei
|
||
unstimmiger Alphakanal-Größe. Behoben am 12.09.2026 durch `x/image` 0.42 → 0.45
|
||
(`ef60497`).
|
||
|
||
**Damit ist die Hälfte erledigt, nicht das Problem.** Die Bauweise bleibt: Der
|
||
nächste Dekodierer-Fehler trifft genauso, und Bilddekodierer sind eine Gattung,
|
||
in der regelmäßig etwas gefunden wird. Vier weitere Goroutinen dieser Art gibt
|
||
es: `internal/sharing/handler.go:232` und `main.go:185` (beide nur Datenbank,
|
||
harmlos), `internal/gc/gc.go:257`, und der OCR-Arbeiter (`go worker.Run(ctx)`),
|
||
der Fremdprogramme aufruft und deren Ausgabe liest.
|
||
|
||
**✅ Erledigt am 12.09.2026.** `pkg/bg` mit zwei Funktionen:
|
||
|
||
* `bg.Go(name, fn)` – startet eine Goroutine, die eine Panik überlebt.
|
||
* `bg.Run(name, fn)` – dasselbe ohne eigene Goroutine, für Schleifen.
|
||
|
||
Der Unterschied ist keine Feinheit: Fängt man eine Panik um den **ganzen**
|
||
Arbeiter herum ab, überlebt der Prozess – aber der Arbeiter ist tot, und von da
|
||
an wird still keine Datei mehr erkannt. Der OCR-Arbeiter sichert deshalb jeden
|
||
**Auftrag** einzeln ab; ein unverdauliches Dokument kostet dann genau sich
|
||
selbst.
|
||
|
||
Umgestellt: Vorschaubild, Bestand nachtragen, Freigabe-Zähler,
|
||
Speicherbereinigung, OCR-Arbeiter. Eine nackte Goroutine bleibt mit Begründung
|
||
im Code – die Signalbehandlung beim Herunterfahren; ein recover brächte dort
|
||
nichts, weil `main` ohnehin wartete, und sie fasst keine fremden Daten an.
|
||
|
||
Eine aufgefangene Panik wird mit Stapel und Herkunftsname protokolliert. Sie
|
||
ist ein Fehler und bleibt einer – sie reißt nur nichts mehr mit. Vier Tests in
|
||
`pkg/bg/bg_test.go`, darunter der, auf den es ankommt: Eine Schleife läuft nach
|
||
einer Panik im dritten Durchgang weiter.
|
||
|
||
> Der Unterschied ist wichtig: Die Version zu heben schloss **diese** Lücke.
|
||
> Der `recover` schließt die **Gattung**.
|
||
|
||
---
|
||
|
||
## 2 · Beschädigte Downloads werden erkannt und verschwiegen
|
||
|
||
**Schwere: mittel. Datenintegrität, still.**
|
||
|
||
`internal/files/handler.go:496` rechnet beim Ausliefern die Prüfsumme mit —
|
||
genau so, wie es die Projektregel verlangt („SHA-256: beim Upload berechnen,
|
||
beim Download verifizieren"). Nur passiert bei einer Abweichung nichts:
|
||
|
||
```go
|
||
actual := hex.EncodeToString(h256.Sum(nil))
|
||
if actual != f.Checksum {
|
||
// Log corruption; headers already sent, can't change status.
|
||
// TODO: alert / observability hook
|
||
_ = actual
|
||
}
|
||
```
|
||
|
||
Kein Logeintrag, kein Zähler, keine Meldung. Der Kommentar sagt, was fehlt.
|
||
|
||
Dass der Statuscode nicht mehr zu ändern ist, stimmt — die Kopfzeilen sind
|
||
längst draußen. Aber **still** muss es deshalb nicht sein. Ein `log.Error()`
|
||
mit Datei-ID, erwarteter und tatsächlicher Summe kostet nichts und ist der
|
||
Unterschied zwischen „ich habe eine kaputte Datei" und „ich habe eine kaputte
|
||
Datei und weiß es seit drei Monaten nicht".
|
||
|
||
Zweitens ließe sich die Verbindung abbrechen, statt sie sauber zu beenden: Der
|
||
Browser zeigt dann einen abgebrochenen Download statt einer heilen Datei mit
|
||
falschem Inhalt. Das ist die ehrlichere Ausgabe.
|
||
|
||
**Vorschlag:** Logeintrag jetzt, Abbruch dazu. Beides zusammen unter zehn Zeilen.
|
||
|
||
---
|
||
|
||
## 3 · Vier Pakete ohne einen einzigen Go-Test
|
||
|
||
**Schwere: mittel. Nicht was heute kaputt ist, sondern was morgen still kaputtgeht.**
|
||
|
||
| Paket | Zeilen | Test | Aussenpruefung |
|
||
|---|---:|---:|---|
|
||
| `internal/tags` | 1132 | **0** | keine |
|
||
| `internal/documents` (OCR, Volltext) | 1008 | **0** | keine |
|
||
| `internal/sharing` | 382 | **0** | Python-Selbsttest, 24 Prüfungen |
|
||
| `internal/rescan` | 479 | **0** | Python-Selbsttest, 20 Prüfungen |
|
||
| `internal/audit` | 207 | **0** | keine |
|
||
| `internal/thumbnails` | 113 | **0** | keine |
|
||
|
||
Bei `sharing` und `rescan` deckt wenigstens ein Selbsttest die Außenseite ab.
|
||
Bei **`documents` und `tags` gar nichts** — und `documents` ist das Stück, das
|
||
aus `scan_0042.pdf` ein auffindbares Dokument macht. Genau der Grund, aus dem
|
||
Aether das Rechnungsarchiv ist.
|
||
|
||
**Vorschlag:** Nicht auf Abdeckung zielen, sondern auf die Stellen, an denen
|
||
ein Fehler still bleibt: Beim OCR die Warteschlange (Versuche, Aufgeben,
|
||
Wiederaufnahme nach hartem Abbruch), bei den Etiketten die Rechteprüfung —
|
||
Etiketten sind je Konto, und das prüft heute nichts nach.
|
||
|
||
---
|
||
|
||
## 4 · Was in Ordnung ist
|
||
|
||
Der Vollständigkeit halber, weil eine Liste nur aus Mängeln ein falsches Bild
|
||
gibt:
|
||
|
||
* **Alle 27 Admin-Routen prüfen serverseitig auf Adminrecht.** Zwei
|
||
Ausnahmen sind Absicht und dokumentiert: `GET /admin/settings` liest jeder
|
||
Angemeldete (Theming — und es liegen dort nur sechs Branding-Schlüssel,
|
||
nachgesehen), `GET /ocr` ist der eigene Erkennungsstand ohne Adminrecht.
|
||
* **Kein einziges zusammengesetztes SQL.** Alles parametrisiert.
|
||
* **Inline-Auslieferung nur über eine Positivliste** harmloser Typen, mit
|
||
`sandbox`, und `allow-scripts` ausschließlich für PDF.
|
||
* **Ratenbegrenzung an jeder unangemeldeten Tür**, die eines braucht: Anmeldung
|
||
(20/min), WebDAV, CalDAV, Freigabelinks (je 10 Fehlversuche / 15 Minuten).
|
||
Die übrigen offenen Wege (`/health`, `/branding`, `/manifest.webmanifest`,
|
||
`/api/v1/public/config`) geben nichts preis.
|
||
* **Kein `dangerouslySetInnerHTML`, kein `eval`** im Frontend.
|
||
* **Sechs Selbsttest-Suiten, 220 Prüfungen** gegen die laufende Instanz, dazu
|
||
37 Frontend-Tests und die Go-Tests.
|
||
|
||
---
|
||
|
||
## 5 · Kleinere offene Enden
|
||
|
||
* **`internal/groups` ist ein leerer Stub** (0 Zeilen). Gruppenordner liegen in
|
||
`files/groupfolder.go`. Entweder das Verzeichnis weg oder der Code dorthin —
|
||
ein leeres Paket im Baum ist eine Frage, die jeder einmal stellt.
|
||
* **`favicon_storage_key`** steht als leere Zeile in den Einstellungen; die
|
||
Funktion heißt seit dem App-Symbol anders. Schadet nichts, wird ignoriert.
|
||
* **Die Fehlversuchssperren liegen im Arbeitsspeicher.** Ein Neustart des
|
||
Containers hebt jede Sperre auf. Für den Hausgebrauch vertretbar; wer es
|
||
anders will, braucht eine Tabelle.
|
||
* **Frontend ohne Komponententests.** `vitest` läuft jetzt (37 Tests), aber nur
|
||
über reine Funktionen — für DOM-Tests fehlt `jsdom` in den devDependencies.
|
||
|
||
---
|
||
|
||
## Reihenfolge, die ich vorschlagen würde
|
||
|
||
1. ~~`recover` in die Hintergrund-Goroutinen~~ ✅ 12.09.2026
|
||
2. **Prüfsummen-Abweichung melden** (Punkt 2). Zehn Zeilen für eine Auskunft,
|
||
die es sonst nie gibt.
|
||
3. **Tests für `documents`** (Punkt 3). Das Paket, dessen stilles Versagen am
|
||
teuersten ist: Die Suche findet dann einfach nichts, und das sieht aus wie
|
||
„ist nicht drin".
|