restic/restic · error
internal error
Error message
internal error
What it means
removeUnexpectedFiles implements restic's --delete behavior: it removes files in the restore target that are not part of the snapshot. The function asserts its own precondition with panic("internal error") when res.opts.Delete is false, because it must only run when the user explicitly asked for deletion. The only in-tree call site (restorer.go:467-468) is already wrapped in 'if res.opts.Delete', so through public APIs this panic is unreachable; it exists as a defense-in-depth invariant against internal misuse or future refactors.
Source
Thrown at internal/restorer/restorer.go:489
}
if node == nil {
return nil
}
err := res.restoreNodeMetadataTo(node, target, location)
if err == nil {
res.opts.Progress.AddProgress(location, ActionDirRestored, 0, 0)
}
return err
},
})
return restoredFileCount, err
}
func (res *Restorer) removeUnexpectedFiles(ctx context.Context, target, location string, expectedFilenames []string) error {
if !res.opts.Delete {
panic("internal error")
}
entries, err := fs.Readdirnames(fs.NewLocal(), target, fs.O_NOFOLLOW)
if errors.Is(err, os.ErrNotExist) {
return nil
} else if err != nil {
return err
}
keep := map[string]struct{}{}
for _, name := range expectedFilenames {
keep[toComparableFilename(name)] = struct{}{}
}
for _, entry := range entries {
if ctx.Err() != nil {
return ctx.Err()
}View on GitHub (pinned to a80be1478a)
Solutions
- If you use the public API and want deletion, construct restorer.NewRestorer(repo, sn, restorer.Options{Delete: true}) so the guarded call path is legitimate
- If maintaining the internal code, keep the 'if res.opts.Delete' guard at every call site and treat the panic as a CI signal that the invariant broke
- Turn the precondition into a returned error in your fork instead of a panic if untrusted callers can reach it
Example fix
// before (internal/fork misuse)
res := restorer.NewRestorer(repo, sn, restorer.Options{Delete: false})
res.removeUnexpectedFiles(ctx, target, location, names) // panic: internal error
// after
res := restorer.NewRestorer(repo, sn, restorer.Options{Delete: true})
// deletion now happens via the normal RestoreTo flow Defensive patterns
Strategy: validation
Validate before calling
// Public-API users: pass the option instead of calling internals
opts := restorer.Options{Delete: true}
res := restorer.NewRestorer(repo, sn, opts)
// Fork maintainers, before an internal call:
if !res.opts.Delete {
return errors.New("refusing to delete unexpected files: Delete option not set")
} Prevention
- Treat removeUnexpectedFiles as delete-only machinery; only enter it behind an 'if opts.Delete' guard
- Keep preconditions as panics only for truly unreachable states; if your fork makes it reachable, convert it to an error
- Run restore tests with and without --delete when touching restore traversal code
When it happens
Trigger: Calling removeUnexpectedFiles (unexported, so only from within package restorer or a fork) on a Restorer built with Options{Delete: false}; a refactor that removes or bypasses the 'if res.opts.Delete' guard at the restoreNominalDir call site; a fork adding a dry-run or filtered path that reaches the function without the flag.
Common situations: Maintaining a restic fork that restructures restore traversal; backporting the delete feature onto an older code path; adding a new restore mode that forgets to check Delete before cleanup. End users running released restic binaries should never see it.
Related errors
- unknown overwrite behavior
- skipping deletion due to invalid filename: %v
- invalid WriteableFileType
- unknown message type
- unknown message type
AI-assisted analysis of restic/restic@a80be1478a (2026-08-15).
Data as JSON: /api/errors/357c2402783d1b45.
Report an issue: GitHub.