Skip to content

Commit 91acbcc

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. A 'locked' boolean variable tracks lock state to prevent double-unlock if an error occurs during blob copying (when the lock is released). The deferred unlock only fires if 'locked' is true. 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 91acbcc

File tree

1 file changed

+19
-4
lines changed

1 file changed

+19
-4
lines changed

common/pkg/libartifact/store/store.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +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()
222+
locked := true
223223
defer func() {
224224
if locked {
225225
as.lock.Unlock()
@@ -297,10 +297,10 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []li
297297
}
298298
defer imageDest.Close()
299299

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

305305
// ImageDestination, in general, requires the caller to write a full image; here we may write only the added layers.
306306
// This works for the oci/layout transport we hard-code.
@@ -356,9 +356,24 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []li
356356
artifactManifest.Layers = append(artifactManifest.Layers, newLayer)
357357
}
358358

359+
// Reacquire the lock before committing changes
359360
as.lock.Lock()
360361
locked = true
361362

363+
// Reload artifacts to check if another process added the same artifact
364+
// while we were copying blobs (without the lock)
365+
if !options.Append {
366+
updatedArtifacts, err := as.getArtifacts(ctx, nil)
367+
if err != nil {
368+
return nil, err
369+
}
370+
// Check if artifact now exists (conflict with concurrent add)
371+
_, _, err = updatedArtifacts.GetByNameOrDigest(dest)
372+
if err == nil {
373+
return nil, fmt.Errorf("%s: %w", dest, libartTypes.ErrArtifactAlreadyExists)
374+
}
375+
}
376+
362377
rawData, err := json.Marshal(artifactManifest)
363378
if err != nil {
364379
return nil, err

0 commit comments

Comments
 (0)