Skip to content

Commit 0ff6005

Browse files
Bryan C. Millsgopherbot
Bryan C. Mills
authored andcommitted
ssh/test: set a timeout and WaitDelay on sshd subcommands
This uses a copy of testenv.Command copied from the main repo, with light edits to allow the testenv helpers to build with Go 1.19. The testenv helper revealed an exec.Command leak in TestCertLogin, so we also fix that leak and simplify server cleanup using testing.T.Cleanup. For golang/go#60099. Fixes golang/go#60343. Change-Id: I7f79fcdb559498b987ee7689972ac53b83870aaf Reviewed-on: https://go-review.googlesource.com/c/crypto/+/496935 Auto-Submit: Bryan Mills <bcmills@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Roland Shoemaker <roland@golang.org> Run-TryBot: Bryan Mills <bcmills@google.com>
1 parent 8e447d8 commit 0ff6005

11 files changed

+182
-69
lines changed

internal/testenv/exec.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package testenv
6+
7+
import (
8+
"context"
9+
"os"
10+
"os/exec"
11+
"reflect"
12+
"strconv"
13+
"testing"
14+
"time"
15+
)
16+
17+
// CommandContext is like exec.CommandContext, but:
18+
// - skips t if the platform does not support os/exec,
19+
// - sends SIGQUIT (if supported by the platform) instead of SIGKILL
20+
// in its Cancel function
21+
// - if the test has a deadline, adds a Context timeout and WaitDelay
22+
// for an arbitrary grace period before the test's deadline expires,
23+
// - fails the test if the command does not complete before the test's deadline, and
24+
// - sets a Cleanup function that verifies that the test did not leak a subprocess.
25+
func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd {
26+
t.Helper()
27+
28+
var (
29+
cancelCtx context.CancelFunc
30+
gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging)
31+
)
32+
33+
if t, ok := t.(interface {
34+
testing.TB
35+
Deadline() (time.Time, bool)
36+
}); ok {
37+
if td, ok := t.Deadline(); ok {
38+
// Start with a minimum grace period, just long enough to consume the
39+
// output of a reasonable program after it terminates.
40+
gracePeriod = 100 * time.Millisecond
41+
if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
42+
scale, err := strconv.Atoi(s)
43+
if err != nil {
44+
t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err)
45+
}
46+
gracePeriod *= time.Duration(scale)
47+
}
48+
49+
// If time allows, increase the termination grace period to 5% of the
50+
// test's remaining time.
51+
testTimeout := time.Until(td)
52+
if gp := testTimeout / 20; gp > gracePeriod {
53+
gracePeriod = gp
54+
}
55+
56+
// When we run commands that execute subprocesses, we want to reserve two
57+
// grace periods to clean up: one for the delay between the first
58+
// termination signal being sent (via the Cancel callback when the Context
59+
// expires) and the process being forcibly terminated (via the WaitDelay
60+
// field), and a second one for the delay becween the process being
61+
// terminated and and the test logging its output for debugging.
62+
//
63+
// (We want to ensure that the test process itself has enough time to
64+
// log the output before it is also terminated.)
65+
cmdTimeout := testTimeout - 2*gracePeriod
66+
67+
if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout {
68+
// Either ctx doesn't have a deadline, or its deadline would expire
69+
// after (or too close before) the test has already timed out.
70+
// Add a shorter timeout so that the test will produce useful output.
71+
ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout)
72+
}
73+
}
74+
}
75+
76+
cmd := exec.CommandContext(ctx, name, args...)
77+
// Set the Cancel and WaitDelay fields only if present (go 1.20 and later).
78+
// TODO: When Go 1.19 is no longer supported, remove this use of reflection
79+
// and instead set the fields directly.
80+
if cmdCancel := reflect.ValueOf(cmd).Elem().FieldByName("Cancel"); cmdCancel.IsValid() {
81+
cmdCancel.Set(reflect.ValueOf(func() error {
82+
if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded {
83+
// The command timed out due to running too close to the test's deadline.
84+
// There is no way the test did that intentionally — it's too close to the
85+
// wire! — so mark it as a test failure. That way, if the test expects the
86+
// command to fail for some other reason, it doesn't have to distinguish
87+
// between that reason and a timeout.
88+
t.Errorf("test timed out while running command: %v", cmd)
89+
} else {
90+
// The command is being terminated due to ctx being canceled, but
91+
// apparently not due to an explicit test deadline that we added.
92+
// Log that information in case it is useful for diagnosing a failure,
93+
// but don't actually fail the test because of it.
94+
t.Logf("%v: terminating command: %v", ctx.Err(), cmd)
95+
}
96+
return cmd.Process.Signal(Sigquit)
97+
}))
98+
}
99+
if cmdWaitDelay := reflect.ValueOf(cmd).Elem().FieldByName("WaitDelay"); cmdWaitDelay.IsValid() {
100+
cmdWaitDelay.Set(reflect.ValueOf(gracePeriod))
101+
}
102+
103+
t.Cleanup(func() {
104+
if cancelCtx != nil {
105+
cancelCtx()
106+
}
107+
if cmd.Process != nil && cmd.ProcessState == nil {
108+
t.Errorf("command was started, but test did not wait for it to complete: %v", cmd)
109+
}
110+
})
111+
112+
return cmd
113+
}
114+
115+
// Command is like exec.Command, but applies the same changes as
116+
// testenv.CommandContext (with a default Context).
117+
func Command(t testing.TB, name string, args ...string) *exec.Cmd {
118+
t.Helper()
119+
return CommandContext(t, context.Background(), name, args...)
120+
}

internal/testenv/testenv_notunix.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build windows || plan9 || (js && wasm) || wasip1
6+
7+
package testenv
8+
9+
import (
10+
"os"
11+
)
12+
13+
// Sigquit is the signal to send to kill a hanging subprocess.
14+
// On Unix we send SIGQUIT, but on non-Unix we only have os.Kill.
15+
var Sigquit = os.Kill

internal/testenv/testenv_unix.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build unix
6+
7+
package testenv
8+
9+
import (
10+
"syscall"
11+
)
12+
13+
// Sigquit is the signal to send to kill a hanging subprocess.
14+
// Send SIGQUIT to get a stack trace.
15+
var Sigquit = syscall.SIGQUIT

ssh/test/agent_unix_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717

1818
func TestAgentForward(t *testing.T) {
1919
server := newServer(t)
20-
defer server.Shutdown()
2120
conn := server.Dial(clientConfig())
2221
defer conn.Close()
2322

ssh/test/banner_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313

1414
func TestBannerCallbackAgainstOpenSSH(t *testing.T) {
1515
server := newServer(t)
16-
defer server.Shutdown()
1716

1817
clientConf := clientConfig()
1918

ssh/test/cert_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
// Test both logging in with a cert, and also that the certificate presented by an OpenSSH host can be validated correctly
1919
func TestCertLogin(t *testing.T) {
2020
s := newServer(t)
21-
defer s.Shutdown()
2221

2322
// Use a key different from the default.
2423
clientKey := testSigners["dsa"]

ssh/test/dial_unix_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ type dialTester interface {
2424

2525
func testDial(t *testing.T, n, listenAddr string, x dialTester) {
2626
server := newServer(t)
27-
defer server.Shutdown()
2827
sshConn := server.Dial(clientConfig())
2928
defer sshConn.Close()
3029

ssh/test/forward_unix_test.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ type closeWriter interface {
2323

2424
func testPortForward(t *testing.T, n, listenAddr string) {
2525
server := newServer(t)
26-
defer server.Shutdown()
2726
conn := server.Dial(clientConfig())
2827
defer conn.Close()
2928

@@ -120,7 +119,6 @@ func TestPortForwardUnix(t *testing.T) {
120119

121120
func testAcceptClose(t *testing.T, n, listenAddr string) {
122121
server := newServer(t)
123-
defer server.Shutdown()
124122
conn := server.Dial(clientConfig())
125123

126124
sshListener, err := conn.Listen(n, listenAddr)
@@ -162,10 +160,9 @@ func TestAcceptCloseUnix(t *testing.T) {
162160
// Check that listeners exit if the underlying client transport dies.
163161
func testPortForwardConnectionClose(t *testing.T, n, listenAddr string) {
164162
server := newServer(t)
165-
defer server.Shutdown()
166-
conn := server.Dial(clientConfig())
163+
client := server.Dial(clientConfig())
167164

168-
sshListener, err := conn.Listen(n, listenAddr)
165+
sshListener, err := client.Listen(n, listenAddr)
169166
if err != nil {
170167
t.Fatal(err)
171168
}
@@ -184,14 +181,10 @@ func testPortForwardConnectionClose(t *testing.T, n, listenAddr string) {
184181

185182
// It would be even nicer if we closed the server side, but it
186183
// is more involved as the fd for that side is dup()ed.
187-
server.clientConn.Close()
184+
server.lastDialConn.Close()
188185

189-
select {
190-
case <-time.After(1 * time.Second):
191-
t.Errorf("timeout: listener did not close.")
192-
case err := <-quit:
193-
t.Logf("quit as expected (error %v)", err)
194-
}
186+
err = <-quit
187+
t.Logf("quit as expected (error %v)", err)
195188
}
196189

197190
func TestPortForwardConnectionCloseTCP(t *testing.T) {

ssh/test/multi_auth_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ func TestMultiAuth(t *testing.T) {
108108
ctx := newMultiAuthTestCtx(t)
109109

110110
server := newServerForConfig(t, "MultiAuth", map[string]string{"AuthMethods": strings.Join(testCase.authMethods, ",")})
111-
defer server.Shutdown()
112111

113112
clientConfig := clientConfig()
114113
server.setTestPassword(clientConfig.User, ctx.password)

ssh/test/session_test.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525

2626
func TestRunCommandSuccess(t *testing.T) {
2727
server := newServer(t)
28-
defer server.Shutdown()
2928
conn := server.Dial(clientConfig())
3029
defer conn.Close()
3130

@@ -42,7 +41,6 @@ func TestRunCommandSuccess(t *testing.T) {
4241

4342
func TestHostKeyCheck(t *testing.T) {
4443
server := newServer(t)
45-
defer server.Shutdown()
4644

4745
conf := clientConfig()
4846
hostDB := hostKeyDB()
@@ -64,7 +62,6 @@ func TestHostKeyCheck(t *testing.T) {
6462

6563
func TestRunCommandStdin(t *testing.T) {
6664
server := newServer(t)
67-
defer server.Shutdown()
6865
conn := server.Dial(clientConfig())
6966
defer conn.Close()
7067

@@ -87,7 +84,6 @@ func TestRunCommandStdin(t *testing.T) {
8784

8885
func TestRunCommandStdinError(t *testing.T) {
8986
server := newServer(t)
90-
defer server.Shutdown()
9187
conn := server.Dial(clientConfig())
9288
defer conn.Close()
9389

@@ -111,7 +107,6 @@ func TestRunCommandStdinError(t *testing.T) {
111107

112108
func TestRunCommandFailed(t *testing.T) {
113109
server := newServer(t)
114-
defer server.Shutdown()
115110
conn := server.Dial(clientConfig())
116111
defer conn.Close()
117112

@@ -128,7 +123,6 @@ func TestRunCommandFailed(t *testing.T) {
128123

129124
func TestRunCommandWeClosed(t *testing.T) {
130125
server := newServer(t)
131-
defer server.Shutdown()
132126
conn := server.Dial(clientConfig())
133127
defer conn.Close()
134128

@@ -148,7 +142,6 @@ func TestRunCommandWeClosed(t *testing.T) {
148142

149143
func TestFuncLargeRead(t *testing.T) {
150144
server := newServer(t)
151-
defer server.Shutdown()
152145
conn := server.Dial(clientConfig())
153146
defer conn.Close()
154147

@@ -180,7 +173,6 @@ func TestFuncLargeRead(t *testing.T) {
180173

181174
func TestKeyChange(t *testing.T) {
182175
server := newServer(t)
183-
defer server.Shutdown()
184176
conf := clientConfig()
185177
hostDB := hostKeyDB()
186178
conf.HostKeyCallback = hostDB.Check
@@ -227,7 +219,6 @@ func TestValidTerminalMode(t *testing.T) {
227219
t.Skipf("skipping on %s", runtime.GOOS)
228220
}
229221
server := newServer(t)
230-
defer server.Shutdown()
231222
conn := server.Dial(clientConfig())
232223
defer conn.Close()
233224

@@ -292,7 +283,6 @@ func TestWindowChange(t *testing.T) {
292283
t.Skipf("skipping on %s", runtime.GOOS)
293284
}
294285
server := newServer(t)
295-
defer server.Shutdown()
296286
conn := server.Dial(clientConfig())
297287
defer conn.Close()
298288

@@ -340,7 +330,6 @@ func TestWindowChange(t *testing.T) {
340330

341331
func testOneCipher(t *testing.T, cipher string, cipherOrder []string) {
342332
server := newServer(t)
343-
defer server.Shutdown()
344333
conf := clientConfig()
345334
conf.Ciphers = []string{cipher}
346335
// Don't fail if sshd doesn't have the cipher.
@@ -399,7 +388,6 @@ func TestMACs(t *testing.T) {
399388
for _, mac := range macOrder {
400389
t.Run(mac, func(t *testing.T) {
401390
server := newServer(t)
402-
defer server.Shutdown()
403391
conf := clientConfig()
404392
conf.MACs = []string{mac}
405393
// Don't fail if sshd doesn't have the MAC.
@@ -425,7 +413,6 @@ func TestKeyExchanges(t *testing.T) {
425413
for _, kex := range kexOrder {
426414
t.Run(kex, func(t *testing.T) {
427415
server := newServer(t)
428-
defer server.Shutdown()
429416
conf := clientConfig()
430417
// Don't fail if sshd doesn't have the kex.
431418
conf.KeyExchanges = append([]string{kex}, kexOrder...)
@@ -460,8 +447,6 @@ func TestClientAuthAlgorithms(t *testing.T) {
460447
} else {
461448
t.Errorf("failed for key %q", key)
462449
}
463-
464-
server.Shutdown()
465450
})
466451
}
467452
}

0 commit comments

Comments
 (0)