sqldb: guard native-SQL migration path against silent downgrades#10964
sqldb: guard native-SQL migration path against silent downgrades#10964kpshukla wants to merge 2 commits into
Conversation
🔴 PR Severity: CRITICAL
🔴 Critical (1 file)
🟢 Low (2 files)
AnalysisThe only non-test, non-doc file touched is Total non-test/non-doc lines changed (40) and file count (1) are both well below the bump thresholds (>500 lines / >20 files), so no additional severity bump applies — the CRITICAL classification here comes solely from the migration-file rule, not from size. Given this touches migration logic, recommend review from someone familiar with the To override, add a |
ApplyMigrations skips any migration whose Version is <= the database's tracked migration_tracker version. If that tracked version is higher than anything the running binary's migrationConfig knows about (e.g. an older lnd binary started against a database already migrated forward by a newer release), every migration in the loop gets skipped silently and lnd proceeds to operate against a schema it doesn't understand, risking data corruption on writes. Add an explicit guard that compares the tracked version against the highest version known to the binary and fails with the new ErrDBDowngrade sentinel error instead of silently skipping. Fixes lightningnetwork#10781 Tested: go build/vet clean; go test ./... in the sqldb module passes for all SQLite-backed cases, including the new TestApplyMigrationsRefusesDowngrade (Postgres-backed cases in the same suite require a local Docker daemon, unavailable in this environment, and are unaffected by this change).
648e878 to
cf328f9
Compare
Change Description
Fixes #10781.
sqldb.ApplyMigrationsreads the migration version tracked in themigration_trackertable and skips every configured migration whoseVersionis<= currentVersion. There is no check for the reverse case:if
currentVersionis higher than anything the running binary'smigrationConfigknows about, every migration is silently skipped andlndproceeds to operate against a schema it doesn't understand.This can happen if a user upgrades to a newer
lndrelease (whichadvances
migration_trackerforward), then accidentally relaunches anolder binary against the same database. The older binary has no way of
knowing it's behind, and unlike
channeldb's kvdb meta store (whichalready guards against this via
ErrDBReversion), the native-SQL storehad no equivalent protection.
Note:
sqldb/v2/migrations.go(an in-progress, not-yet-wired-inpackage) already implements the same kind of guard for schema-level
downgrades (
ErrMigrationDowngrade). This PR ports that idiom to thecurrently-in-use
sqldbpackage'smigration_tracker-level versioncheck, which is what the linked issue is specifically about.
Fix
Added
ErrDBDowngradeand a check inApplyMigrationsthat comparescurrentVersionagainst the highest version the binary'smigrationConfigcontains (len(migrations), which is safe because thefunction already validates that migration versions are numbered
contiguously from 1). If the database is ahead,
ApplyMigrationsnowfails loudly instead of silently skipping every migration.
Scope note
The issue also proposed a second, complementary change: bumping
channeldb's mandatory kvdb version with a no-op entry, so that even apre-fix binary refuses to start against a database a newer release
already touched (closing the gap for binaries that predate this PR).
I intentionally left that out of this PR — it affects every
lndnodeon upgrade (not just native-SQL-store users) and "spends" a kvdb version
slot permanently, which felt like a call best left to a maintainer
rather than bundled unilaterally into a bug-fix PR. Happy to follow up
with that change if maintainers want it.
Testing
go build ./...andgo vet ./...clean in thesqldbmodule.TestApplyMigrationsRefusesDowngrade: migrates a fresh databaseforward using a 3-entry migration list (simulating a "newer" binary),
then reopens the same database file/instance and calls
ApplyAllMigrationswith only a 2-entry list (simulating an "older"binary). Asserts
ErrDBDowngradeis returned and that the trackedmigration version is left untouched. Covered for both SQLite and
Postgres backends.
TestMigrations,TestCustomMigration,TestMigrationSucceedsAfterDirtyStateMigrationFailure19RC1,TestMigrationConfigConsistency) plus the new test undergo test -race. All SQLite-backed and non-DB cases pass. Postgres-backed casesin this suite require a local Docker daemon, which wasn't available in
my dev environment (
docker infoconfirms), so those specificsubtests couldn't be locally exercised — this is a pre-existing
environment limitation, not something introduced by this change, and
CI should cover the Postgres path.
gofmt -lclean; all added lines respect the repo's 80-columnconvention.
Pull Request Checklist
Testing
Code Style and Documentation
📝 Please see our Contribution Guidelines for further guidance.