diff --git a/daemon/monitor.go b/daemon/monitor.go index 6dd5dbab15..1897c533e3 100644 --- a/daemon/monitor.go +++ b/daemon/monitor.go @@ -179,7 +179,7 @@ func (daemon *Daemon) ProcessEvent(id string, e libcontainerdtypes.EventType, ei // Remove the exec command from the container's store only and not the // daemon's store so that the exec command can be inspected. Remove it // before mutating execConfig to maintain the invariant that - // c.ExecCommands only contain execs in the Running state. + // c.ExecCommands only contains execs that have not exited. c.ExecCommands.Delete(execConfig.ID) execConfig.ExitCode = &ec @@ -195,15 +195,25 @@ func (daemon *Daemon) ProcessEvent(id string, e libcontainerdtypes.EventType, ei exitCode = ec - go func() { - if _, err := execConfig.Process.Delete(context.Background()); err != nil { - logrus.WithFields(logrus.Fields{ - logrus.ErrorKey: err, - "container": ei.ContainerID, - "process": ei.ProcessID, - }).Warn("failed to delete process") - } - }() + // If the exec failed at start in such a way that containerd + // publishes an exit event for it, we will race processing the event + // with daemon.ContainerExecStart() removing the exec from + // c.ExecCommands. If we win the race, we will find that there is no + // process to clean up. (And ContainerExecStart will clobber the + // exit code we set.) Prevent a nil-dereferenc panic in that + // situation to restore the status quo where this is merely a + // logical race condition. + if execConfig.Process != nil { + go func() { + if _, err := execConfig.Process.Delete(context.Background()); err != nil { + logrus.WithFields(logrus.Fields{ + logrus.ErrorKey: err, + "container": ei.ContainerID, + "process": ei.ProcessID, + }).Warn("failed to delete process") + } + }() + } } attributes := map[string]string{ "execID": ei.ProcessID,