fix: hold startMx on the start path actually exercised

startMx exists to serialize ContainerStart calls because the engine
assigns published ports from ranges non-atomically. It is held by the
plan executor's execStartContainer — a nearly dead code path (plans
only emit OpStartContainer for paused/dead containers) — and NOT by
startServiceContainer, the path every up/start actually runs. The
protection is thus inoperative today: concurrent services publishing
overlapping port ranges can race in the engine.

Take the mutex around the real ContainerStart call and document the
contract on the variable. Starts of different services were already
serialized per-service (replicas start sequentially); this serializes
the API call across services too, matching what the plan engine will
do once the start phase converges (#14081).

Part of #14074 (section C) / #14081 (lot 0).

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2026-08-20 14:47:35 +02:00 committed by Nicolas De loof
parent 06f4f9d622
commit b857135cb9

View file

@ -354,7 +354,11 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro
return ctr, nil
}
// force sequential calls to ContainerStart to prevent race condition in engine assigning ports from ranges
// startMx serializes ContainerStart calls across the whole process: the
// engine allocates published ports from ranges non-atomically, and two
// concurrent starts can be assigned the same port. Every code path calling
// ContainerStart on a service container must hold it (the plan executor's
// execStartContainer and the imperative startServiceContainer both do).
var startMx sync.Mutex
func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
@ -603,7 +607,10 @@ func (s *composeService) startServiceContainer(ctx context.Context, project *typ
eventName := getContainerProgressName(ctr)
s.events.On(newEvent(eventName, api.Working, api.StatusStarting))
if _, err := s.apiClient().ContainerStart(ctx, ctr.ID, client.ContainerStartOptions{}); err != nil {
startMx.Lock()
_, err := s.apiClient().ContainerStart(ctx, ctr.ID, client.ContainerStartOptions{})
startMx.Unlock()
if err != nil {
return err
}