Skip to content

Commit f439ffd

Browse files
committed
Fix race condition in concurrent artifact additions
This fixes a race condition where concurrent 'podman artifact add' commands for different artifacts would result in only one artifact being created, without any error messages. The root cause was in the artifact store's Add() method. When the lock was released during blob copying (for performance), concurrent additions could overwrite each other's index entries: - Process A: Lock → Read index → Unlock → Copy blobs → Lock → Commit - Process B: Lock → Read index (missing A) → Unlock → Copy blobs → Lock → Commit - Result: Process B's commit overwrites Process A's entry The fix adds conflict detection after reacquiring the lock. Before committing, we reload the artifact index and verify the artifact name hasn't been added by another process. If a conflict is detected, we return an error rather than silently overwriting the concurrent change. This preserves the performance benefits of concurrent blob copying while preventing the race condition that caused artifacts to be lost. Fixes: containers/podman#27569 Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
1 parent 63be353 commit f439ffd

File tree

1 file changed

+19
-10
lines changed

1 file changed

+19
-10
lines changed

common/pkg/libartifact/store/store.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,8 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []li
218218
return nil, errors.New("append option is not compatible with type option")
219219
}
220220

221-
locked := true
222221
as.lock.Lock()
223-
defer func() {
224-
if locked {
225-
as.lock.Unlock()
226-
}
227-
}()
222+
defer as.lock.Unlock()
228223

229224
// Check if artifact already exists
230225
artifacts, err := as.getArtifacts(ctx, nil)
@@ -297,9 +292,8 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []li
297292
}
298293
defer imageDest.Close()
299294

300-
// Unlock around the actual pull of the blobs.
301-
// This is ugly as hell, but should be safe.
302-
locked = false
295+
// Release the lock during blob copying to allow concurrent operations.
296+
// We'll reacquire it later and check for conflicts before committing.
303297
as.lock.Unlock()
304298

305299
// ImageDestination, in general, requires the caller to write a full image; here we may write only the added layers.
@@ -356,8 +350,23 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []li
356350
artifactManifest.Layers = append(artifactManifest.Layers, newLayer)
357351
}
358352

353+
// Reacquire the lock before committing changes
359354
as.lock.Lock()
360-
locked = true
355+
// Lock will be released by defer at function exit
356+
357+
// Reload artifacts to check if another process added the same artifact
358+
// while we were copying blobs (without the lock)
359+
if !options.Append {
360+
updatedArtifacts, err := as.getArtifacts(ctx, nil)
361+
if err != nil {
362+
return nil, err
363+
}
364+
// Check if artifact now exists (conflict with concurrent add)
365+
_, _, err = updatedArtifacts.GetByNameOrDigest(dest)
366+
if err == nil {
367+
return nil, fmt.Errorf("%s: %w", dest, libartTypes.ErrArtifactAlreadyExists)
368+
}
369+
}
361370

362371
rawData, err := json.Marshal(artifactManifest)
363372
if err != nil {

0 commit comments

Comments
 (0)