Skip to content
This repository was archived by the owner on Jul 31, 2025. It is now read-only.

Commit ac7a57e

Browse files
committed
Fix race condition in atomic update query
- Use Create and rely on unique constraint to stop inserts of duplicate versions - Add support for postgres and sqlite when checking for unique constraint errors - Make one query per role within the transaction to check for existing rows with versions higher than those being inserted - Check for duplicate versions in the update list before touching the database Signed-off-by: Jonny Stoten <jonny.stoten@docker.com>
1 parent c7ef1f8 commit ac7a57e

File tree

1 file changed

+71
-32
lines changed

1 file changed

+71
-32
lines changed

‎server/storage/sqldb.go‎

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
"github.com/go-sql-driver/mysql"
1111
"github.com/jinzhu/gorm"
12+
"github.com/lib/pq"
13+
"github.com/mattn/go-sqlite3"
1214
"github.com/sirupsen/logrus"
1315
"github.com/theupdateframework/notary/tuf/data"
1416
)
@@ -31,7 +33,7 @@ func NewSQLStorage(dialect string, args ...interface{}) (*SQLStorage, error) {
3133
}
3234

3335
// translateOldVersionError captures DB errors, and attempts to translate
34-
// duplicate entry - currently only supports MySQL and Sqlite3
36+
// duplicate entry
3537
func translateOldVersionError(err error) error {
3638
switch err := err.(type) {
3739
case *mysql.MySQLError:
@@ -41,6 +43,16 @@ func translateOldVersionError(err error) error {
4143
if err.Number == 1022 || err.Number == 1062 {
4244
return ErrOldVersion{}
4345
}
46+
case pq.Error:
47+
// https://www.postgresql.org/docs/10/errcodes-appendix.html
48+
// 23505 = unique_violation
49+
if err.Code == "23505" {
50+
return ErrOldVersion{}
51+
}
52+
case sqlite3.Error:
53+
if err.ExtendedCode == sqlite3.ErrConstraintUnique {
54+
return ErrOldVersion{}
55+
}
4456
}
4557
return err
4658
}
@@ -53,8 +65,10 @@ func (db *SQLStorage) UpdateCurrent(gun data.GUN, update MetaUpdate) error {
5365
exists := db.Where("gun = ? and role = ? and version >= ?",
5466
gun.String(), update.Role.String(), update.Version).First(&TUFFile{})
5567

56-
if !exists.RecordNotFound() {
68+
if exists.Error == nil {
5769
return ErrOldVersion{}
70+
} else if !exists.RecordNotFound() {
71+
return exists.Error
5872
}
5973

6074
// only take out the transaction once we're about to start writing
@@ -113,50 +127,59 @@ func (db *SQLStorage) getTransaction() (*gorm.DB, rollback, error) {
113127

114128
// UpdateMany atomically updates many TUF records in a single transaction
115129
func (db *SQLStorage) UpdateMany(gun data.GUN, updates []MetaUpdate) error {
130+
if !allUpdatesUnique(updates) {
131+
// We would fail with a unique constraint violation later, so just bail out now
132+
return ErrOldVersion{}
133+
}
134+
135+
minVersionsByRole := make(map[data.RoleName]int)
136+
for _, u := range updates {
137+
cur, ok := minVersionsByRole[u.Role]
138+
if !ok || u.Version < cur {
139+
minVersionsByRole[u.Role] = u.Version
140+
}
141+
}
142+
143+
for role, minVersion := range minVersionsByRole {
144+
// If there are any files with version equal or higher than the minimum
145+
// version we're trying to insert, bail out now
146+
exists := db.Where("gun = ? and role = ? and version >= ?",
147+
gun.String(), role.String(), minVersion).First(&TUFFile{})
148+
149+
if exists.Error == nil {
150+
return ErrOldVersion{}
151+
} else if !exists.RecordNotFound() {
152+
return exists.Error
153+
}
154+
}
155+
116156
tx, rb, err := db.getTransaction()
117157
if err != nil {
118158
return err
119159
}
120-
var (
121-
query *gorm.DB
122-
added = make(map[uint]bool)
123-
)
160+
124161
if err := func() error {
125162
for _, update := range updates {
126-
// This looks like the same logic as UpdateCurrent, but if we just
127-
// called, version ordering in the updates list must be enforced
128-
// (you cannot insert the version 2 before version 1). And we do
129-
// not care about monotonic ordering in the updates.
130-
query = db.Where("gun = ? and role = ? and version >= ?",
131-
gun.String(), update.Role.String(), update.Version).First(&TUFFile{})
132-
133-
if !query.RecordNotFound() {
134-
return ErrOldVersion{}
135-
}
136-
137-
var row TUFFile
138163
checksum := sha256.Sum256(update.Data)
139164
hexChecksum := hex.EncodeToString(checksum[:])
140-
query = tx.Where(map[string]interface{}{
141-
"gun": gun.String(),
142-
"role": update.Role.String(),
143-
"version": update.Version,
144-
}).Attrs("data", update.Data).Attrs("sha256", hexChecksum).FirstOrCreate(&row)
145-
146-
if query.Error != nil {
147-
return translateOldVersionError(query.Error)
148-
}
149-
// it's previously been added, which means it's a duplicate entry
150-
// in the same transaction
151-
if _, ok := added[row.ID]; ok {
152-
return ErrOldVersion{}
165+
166+
result := tx.Create(&TUFFile{
167+
Gun: gun.String(),
168+
Role: update.Role.String(),
169+
Version: update.Version,
170+
Data: update.Data,
171+
SHA256: hexChecksum,
172+
})
173+
174+
if result.Error != nil {
175+
return translateOldVersionError(result.Error)
153176
}
177+
154178
if update.Role == data.CanonicalTimestampRole {
155179
if err := db.writeChangefeed(tx, gun, update.Version, hexChecksum); err != nil {
156180
return err
157181
}
158182
}
159-
added[row.ID] = true
160183
}
161184
return nil
162185
}(); err != nil {
@@ -165,6 +188,22 @@ func (db *SQLStorage) UpdateMany(gun data.GUN, updates []MetaUpdate) error {
165188
return tx.Commit().Error
166189
}
167190

191+
func allUpdatesUnique(updates []MetaUpdate) bool {
192+
type roleVersion struct {
193+
Role data.RoleName
194+
Version int
195+
}
196+
roleVersions := make(map[roleVersion]bool)
197+
for _, u := range updates {
198+
rv := roleVersion{u.Role, u.Version}
199+
if roleVersions[rv] {
200+
return false
201+
}
202+
roleVersions[rv] = true
203+
}
204+
return true
205+
}
206+
168207
func (db *SQLStorage) writeChangefeed(tx *gorm.DB, gun data.GUN, version int, checksum string) error {
169208
c := &SQLChange{
170209
GUN: gun.String(),

0 commit comments

Comments
 (0)