bug(native): untracked backup overwrite can change another game's owned save path

Aperta
#138 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@Seagulls ci sta già lavorando.

Dal 30/10/2026.

  • #141 di @Seagulls — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
go, sqlite

Direzione di ricerca

Start with internal/api/routes_backup.go and trace the /api/backup/restore flow, especially the order of CheckRestoreTarget, extraction, and TrackGame. Review the ownership predicate proposed in PR #131 and run the three regression cases linked in the issue. Done means conflicting primary, named, and nested roots are refused before extraction with owner bytes unchanged, while valid unclaimed imports still work.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Summary

A portable v2 backup for an untracked game can be imported with mode: "overwrite" into a folder already owned by a different tracked game. The importer extracts archived bytes before it establishes authoritative game/root ownership. An eventual registration refusal is too late: the tracked owner's live files have already changed.

Affected source: Liquid-co/OpenSave:dev-public@a0f6032b8ba36da25e3e0e5e90c5ace9ba0c1fc7 on Linux amd64. The result is confirmed with synthetic files and actual localhost native HTTP export/restore plus SQLite. No real game saves were used.

Related but distinct: upstream #130 / PR #131 correct ordinary registration ownership, but a backup import must enforce that check before extracting. This does not duplicate snapshot-ID collision reports.

Independent reproduction

Public Go regression · Reproduction guide

git checkout repro/native-root-lineage-import-20261030
go test -count=1 -race -v ./internal/api -run '^TestRegressionUntrackedImportCannotOverwriteOwnedRoot$'

Three separate test cases exercise the exact primary, exact named config, and nested under named config save-root scenarios:

  1. Export an untracked synthetic backup containing ARCHIVED-OLD at a chosen temporary location.
  2. Track a different game as native owner of that exact or enclosing save root. Replace its live slot.sav contents with OWNER-LIVE.
  3. Call POST /api/backup/restore with {sourcePath: archive, mode: "overwrite"}.
  4. Read the owner's actual file bytes and verify a conflicting second native game was not registered.
Scenario Required result Stock dev-public
Destination equals another game's primary root Preserve OWNER-LIVE; refuse/skip slot.sav overwritten with archived bytes
Destination equals another game's mapped named root Preserve OWNER-LIVE; refuse/skip slot.sav overwritten
Destination nested inside another game's mapped named root Preserve OWNER-LIVE; refuse/skip slot.sav overwritten

In the primary case a subsequent registration can fail after extraction. For named/nested roots, conflicting registration also occurs on stock in the observed fixture. The test's decisive safety assertion is that the owner's file bytes remain unchanged.

Root cause

internal/api/routes_backup.go performs CheckRestoreTarget (directory suitability), then snapshot.UnzipRoots (filesystem mutation), then TrackGame (ownership admission). The imported game's own write gate cannot stand in for the existing other game's ownership.

Bounded suggested fix

For untracked overwrite imports, perform authoritative path-ownership refusal before any mutation, including primary/named/nested roots, resolvable aliases and inventory-read failure. Reuse the transactional ownership predicate proposed in PR #131, and hold a bounded native admission/claim through extraction and registration to avoid time-of-check/time-of-use races. Preserve valid unclaimed imports, tracked-game restore semantics, and the existing pre-import safety backup.

Do not introduce a new save-sync engine, a permanent managed-game class or a general first-copy coordinator.

Acceptance criteria

  • All three exact-stock tests fail for the intended overwrite and pass with original owner bytes preserved; no second owner is registered.
  • Physical path aliases, unreadable inventory, and simultaneous native registration/root edits fail closed before extraction.
  • Valid unclaimed untracked imports and existing tracked-game imports continue to work.
  • A returned extraction/registration error restores the pre-import safety copy rather than reporting a successful import; distinguish this from unverified power-loss/SIGKILL durability.
  • Linux Go/race tests and supported platform builds pass; native Windows lock semantics should be checked separately if used.

Evidence limit: Reproduced native writes to disposable files only. No evidence of real user-save loss, deployed exploitation, or a complete crash-restart durability guarantee.


Independent public reproducer and original report: https://github.com/Seagulls/OpenSave/issues/15.

Lingua principale
Go
Stelle
225
Fork
9
Merge medio
1g 23h
PR unite (30g)
76

Preparare l'ambiente

  • Include un Dockerfile o un file Docker Compose
  • Nessun modello di pull request
  • Nessuna guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di Liquid-co/OpenSave

Tutte le issue di Liquid-co/OpenSave

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.