What changed, and why it matters
This commit adds a safety check to LND's database migration system. Before running any migration, it now verifies that the list of migration steps matches the declared 'latest version' number. The change prevents a specific misconfiguration where a migration set claims to be at version 5 but contains no actual migration steps, which previously would have passed validation silently. It is a defensive hardening fix rather than a response to an active attack.
Treat as a low-risk hardening improvement. Review whether any production or test migration sets rely on empty descriptors with a non-zero LatestMigrationVersion, and ensure callers do not depend on SkipMigrations to bypass validation. No urgent patching required absent other context.
Security signals we found
Defensive input validation added to migration execution path
Fail-fast on inconsistent migration metadata
Guard against empty descriptor list with non-zero latest version
No direct memory corruption, injection, or authentication bypass
Evidence from the diff
The patch introduces MigrationSet.validate() in sqldb/v2/migrations.go and calls it at the start of executeMigrations() for both PostgresStore and SqliteStore. The validator enforces: (1) if Descriptors is empty, LatestMigrationVersion must be zero; (2) descriptor versions must be contiguous starting at 1; (3) the last descriptor’s version must equal LatestMigrationVersion. Unit tests cover the new cases, and an existing SQLite internal test is updated to supply a descriptor so it still passes validation.
Changed components
sqldb/v2/migrations.gosqldb/v2/postgres.gosqldb/v2/sqlite.gosqldb/v2/migrations_test.gosqldb/v2/sqlite_internal_test.goInspect captured patch +113 / −2
diff --git a/sqldb/v2/migrations.go b/sqldb/v2/migrations.go
index 56e0f3c..3a92543 100644
--- a/sqldb/v2/migrations.go
+++ b/sqldb/v2/migrations.go
@@ -77,8 +77,8 @@ type MigrationSet struct {
LatestMigrationVersion uint
// Descriptors defines a list of migrations to be applied to the
- // database. Each migration is assigned a version number, determining
- // its execution order.
+ // database. Each migration is assigned a version number that documents
+ // and validates the expected execution order.
// The schema version, tracked by golang-migrate, ensures migrations are
// applied to the correct schema. For migrations involving only schema
// changes, the migration function can be left nil. For custom
@@ -86,6 +86,37 @@ type MigrationSet struct {
Descriptors []MigrationDescriptor
}
+// validate checks that the migration metadata is internally consistent.
+func (m MigrationSet) validate() error {
+ if len(m.Descriptors) == 0 {
+ if m.LatestMigrationVersion != 0 {
+ return fmt.Errorf("latest migration version %d requires "+
+ "at least one descriptor",
+ m.LatestMigrationVersion)
+ }
+
+ return nil
+ }
+
+ for i, descriptor := range m.Descriptors {
+ expectedVersion := i + 1
+ if descriptor.Version != expectedVersion {
+ return fmt.Errorf("migration descriptor version %d is out "+
+ "of order, expected %d", descriptor.Version,
+ expectedVersion)
+ }
+ }
+
+ lastDescriptor := m.Descriptors[len(m.Descriptors)-1]
+ if uint(lastDescriptor.Version) != m.LatestMigrationVersion {
+ return fmt.Errorf("latest migration version %d does not "+
+ "match last descriptor version %d",
+ m.LatestMigrationVersion, lastDescriptor.Version)
+ }
+
+ return nil
+}
+
// MigrationTarget is a functional option that can be passed to applyMigrations
// to specify a target version to migrate to. `currentDbVersion` is the current
// (migration) version of the database, or None if unknown.
diff --git a/sqldb/v2/migrations_test.go b/sqldb/v2/migrations_test.go
index e5bb5f3..67ad1f3 100644
--- a/sqldb/v2/migrations_test.go
+++ b/sqldb/v2/migrations_test.go
@@ -35,3 +35,71 @@ func TestPostgresSchemaReplacements(t *testing.T) {
"CURRENT_TIMESTAMP", string(content),
)
}
+
+// TestMigrationSetValidate verifies that migration descriptors remain aligned
+// with the migration stream metadata.
+func TestMigrationSetValidate(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ set MigrationSet
+ expect string
+ }{
+ {
+ name: "valid descriptors",
+ set: MigrationSet{
+ LatestMigrationVersion: 2,
+ Descriptors: []MigrationDescriptor{
+ {Version: 1},
+ {Version: 2},
+ },
+ },
+ },
+ {
+ name: "descriptor gap",
+ set: MigrationSet{
+ LatestMigrationVersion: 2,
+ Descriptors: []MigrationDescriptor{
+ {Version: 1},
+ {Version: 3},
+ },
+ },
+ expect: "out of order",
+ },
+ {
+ name: "missing descriptors for latest version",
+ set: MigrationSet{
+ LatestMigrationVersion: 1,
+ },
+ expect: "requires at least one descriptor",
+ },
+ {
+ name: "latest version mismatch",
+ set: MigrationSet{
+ LatestMigrationVersion: 3,
+ Descriptors: []MigrationDescriptor{
+ {Version: 1},
+ {Version: 2},
+ },
+ },
+ expect: "does not match",
+ },
+ }
+
+ for _, testCase := range testCases {
+ testCase := testCase
+
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ err := testCase.set.validate()
+ if testCase.expect == "" {
+ require.NoError(t, err)
+ return
+ }
+
+ require.ErrorContains(t, err, testCase.expect)
+ })
+ }
+}
diff --git a/sqldb/v2/postgres.go b/sqldb/v2/postgres.go
index ef60992..833c744 100644
--- a/sqldb/v2/postgres.go
+++ b/sqldb/v2/postgres.go
@@ -228,6 +228,10 @@ func (s *PostgresStore) ExecuteMigrations(set MigrationSet) error {
func (s *PostgresStore) executeMigrations(target MigrationTarget,
set MigrationSet) error {
+ if err := set.validate(); err != nil {
+ return err
+ }
+
dbName, err := getDatabaseNameFromDSN(s.cfg.Dsn)
if err != nil {
return err
diff --git a/sqldb/v2/sqlite.go b/sqldb/v2/sqlite.go
index 255f8e4..7529e1c 100644
--- a/sqldb/v2/sqlite.go
+++ b/sqldb/v2/sqlite.go
@@ -261,6 +261,10 @@ func (s *SqliteStore) ExecuteMigrations(set MigrationSet) error {
func (s *SqliteStore) executeMigrations(target MigrationTarget,
set MigrationSet) error {
+ if err := set.validate(); err != nil {
+ return err
+ }
+
driver, err := sqlite_migrate.WithInstance(
s.DB, &sqlite_migrate.Config{
MigrationsTable: set.TrackingTableName,
diff --git a/sqldb/v2/sqlite_internal_test.go b/sqldb/v2/sqlite_internal_test.go
index 43059fb..07fa24c 100644
--- a/sqldb/v2/sqlite_internal_test.go
+++ b/sqldb/v2/sqlite_internal_test.go
@@ -28,6 +28,10 @@ func TestSqliteProgrammaticMigrationError(t *testing.T) {
err = store.ExecuteMigrations(MigrationSet{
TrackingTableName: "migration_tracker",
LatestMigrationVersion: 1,
+ Descriptors: []MigrationDescriptor{{
+ Name: "programmatic",
+ Version: 1,
+ }},
MakeProgrammaticMigrations: func(*BaseDB) (
map[uint]migrate.ProgrammaticMigrEntry, error) {
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.