Skip to content

Commit 71a015e

Browse files
committed
fix: stop binding SIGPIPE to the root context
The root command binds syscall.SIGPIPE to the cancellation context in cmd/skaffold/app/skaffold.go. Once a process registers SIGPIPE via signal.Notify/NotifyContext, the Go runtime delivers it for every file descriptor, so a SIGPIPE raised when a registry (e.g. mcr.microsoft.com) resets an idle connection mid-build is interpreted as a request to cancel the whole run. Multi-stage builds whose first stage runs longer than the registry's ~60s idle timeout are cancelled with the reason "broken pipe signal received". Stop treating SIGPIPE as cancellation: remove it from NotifyContext and signal.Ignore it instead. Ignoring it (rather than just dropping it) preserves the original reason SIGPIPE was caught in #510 -- broken-pipe writes during shutdown return EPIPE instead of killing the process, so cleanup still completes when skaffold's output pipe is closed. Fixes #10106
1 parent f9beeb7 commit 71a015e

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

cmd/skaffold/app/skaffold.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import (
3333
)
3434

3535
func Run(out, stderr io.Writer) error {
36-
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGPIPE)
36+
ctx, cancel := rootContext()
3737
defer cancel()
3838

3939
catchStackdumpRequests()
@@ -63,3 +63,18 @@ func Run(out, stderr io.Writer) error {
6363
}
6464
return err
6565
}
66+
67+
// rootContext returns the root context for a skaffold invocation, cancelled on interrupt and
68+
// termination signals.
69+
//
70+
// SIGPIPE is intentionally NOT a cancellation trigger; it is ignored instead. Once a process
71+
// registers SIGPIPE with signal.Notify, Go delivers it for every file descriptor, so binding it
72+
// to the cancellation context caused long-running builds to be cancelled whenever a registry
73+
// (e.g. mcr.microsoft.com) reset an idle connection mid-build (#10106). Ignoring it keeps
74+
// broken-pipe writes returning EPIPE without killing the process, which preserves the original
75+
// intent of catching SIGPIPE (#510): a closed output pipe (e.g. `skaffold ... | head`) must not
76+
// abort skaffold before graceful cleanup finishes.
77+
func rootContext() (context.Context, context.CancelFunc) {
78+
signal.Ignore(syscall.SIGPIPE)
79+
return signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
80+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
/*
5+
Copyright 2025 The Skaffold Authors
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package app
21+
22+
import (
23+
"context"
24+
"os/signal"
25+
"syscall"
26+
"testing"
27+
"time"
28+
)
29+
30+
// TestRootContextIgnoresSIGPIPE is a regression test for #10106: a SIGPIPE (raised when a
31+
// registry resets an idle connection during a long build) must not cancel the root context.
32+
// Before the fix SIGPIPE was passed to signal.NotifyContext, so this would cancel the context
33+
// and abort the build.
34+
func TestRootContextIgnoresSIGPIPE(t *testing.T) {
35+
defer signal.Reset(syscall.SIGPIPE)
36+
37+
ctx, cancel := rootContext()
38+
defer cancel()
39+
40+
if err := syscall.Kill(syscall.Getpid(), syscall.SIGPIPE); err != nil {
41+
t.Fatalf("sending SIGPIPE: %v", err)
42+
}
43+
44+
select {
45+
case <-ctx.Done():
46+
t.Fatalf("SIGPIPE cancelled the root context: %v (#10106 regression)", context.Cause(ctx))
47+
case <-time.After(200 * time.Millisecond):
48+
// expected: SIGPIPE is ignored and the context stays alive.
49+
}
50+
}

0 commit comments

Comments
 (0)