Browse code

Forbid client piping to tty enabled container

Forbid `docker run -t` with a redirected stdin (such as `echo test |
docker run -ti busybox cat`). Forbid `docker exec -t` with a redirected
stdin. Forbid `docker attach` with a redirect stdin toward a tty enabled
container.

Signed-off-by: Arnaud Porterie <arnaud.porterie@docker.com>

Arnaud Porterie authored on 2014/12/06 09:50:56
Showing 10 changed files
... ...
@@ -3,6 +3,7 @@ package client
3 3
 import (
4 4
 	"crypto/tls"
5 5
 	"encoding/json"
6
+	"errors"
6 7
 	"fmt"
7 8
 	"io"
8 9
 	"net"
... ...
@@ -104,6 +105,16 @@ func (cli *DockerCli) LoadConfigFile() (err error) {
104 104
 	return err
105 105
 }
106 106
 
107
+func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
108
+	// In order to attach to a container tty, input stream for the client must
109
+	// be a tty itself: redirecting or piping the client standard input is
110
+	// incompatible with `docker run -t`, `docker exec -t` or `docker attach`.
111
+	if ttyMode && attachStdin && !cli.isTerminalIn {
112
+		return errors.New("cannot enable tty mode on non tty input")
113
+	}
114
+	return nil
115
+}
116
+
107 117
 func NewDockerCli(in io.ReadCloser, out, err io.Writer, key libtrust.PrivateKey, proto, addr string, tlsConfig *tls.Config) *DockerCli {
108 118
 	var (
109 119
 		inFd          uintptr
... ...
@@ -1974,6 +1974,10 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
1974 1974
 		tty    = config.GetBool("Tty")
1975 1975
 	)
1976 1976
 
1977
+	if err := cli.CheckTtyInput(!*noStdin, tty); err != nil {
1978
+		return err
1979
+	}
1980
+
1977 1981
 	if tty && cli.isTerminalOut {
1978 1982
 		if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil {
1979 1983
 			log.Debugf("Error monitoring TTY size: %s", err)
... ...
@@ -2288,7 +2292,11 @@ func (cli *DockerCli) CmdRun(args ...string) error {
2288 2288
 		return nil
2289 2289
 	}
2290 2290
 
2291
-	if *flDetach {
2291
+	if !*flDetach {
2292
+		if err := cli.CheckTtyInput(config.AttachStdin, config.Tty); err != nil {
2293
+			return err
2294
+		}
2295
+	} else {
2292 2296
 		if fl := cmd.Lookup("attach"); fl != nil {
2293 2297
 			flAttach = fl.Value.(*opts.ListOpts)
2294 2298
 			if flAttach.Len() != 0 {
... ...
@@ -2600,7 +2608,11 @@ func (cli *DockerCli) CmdExec(args ...string) error {
2600 2600
 		return nil
2601 2601
 	}
2602 2602
 
2603
-	if execConfig.Detach {
2603
+	if !execConfig.Detach {
2604
+		if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
2605
+			return err
2606
+		}
2607
+	} else {
2604 2608
 		if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execConfig, false)); err != nil {
2605 2609
 			return err
2606 2610
 		}
... ...
@@ -20,6 +20,9 @@ container, or `CTRL-\` to get a stacktrace of the Docker client when it quits.
20 20
 When you detach from a container the exit code will be returned to
21 21
 the client.
22 22
 
23
+It is forbidden to redirect the standard input of a docker attach command while
24
+attaching to a tty-enabled container (i.e.: launched with -t`).
25
+
23 26
 # OPTIONS
24 27
 **--no-stdin**=*true*|*false*
25 28
    Do not attach STDIN. The default is *false*.
... ...
@@ -31,5 +31,8 @@ container is unpaused, and then run
31 31
 **-t**, **--tty**=*true*|*false*
32 32
    Allocate a pseudo-TTY. The default is *false*.
33 33
 
34
+The **-t** option is incompatible with a redirection of the docker client
35
+standard input.
36
+
34 37
 # HISTORY
35 38
 November 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
... ...
@@ -267,6 +267,9 @@ outside of a container on the host.
267 267
 input of any container. This can be used, for example, to run a throwaway
268 268
 interactive shell. The default is value is false.
269 269
 
270
+The **-t** option is incompatible with a redirection of the docker client
271
+standard input.
272
+
270 273
 **-u**, **--user**=""
271 274
    Username or UID
272 275
 
... ...
@@ -94,9 +94,10 @@ specify to which of the three standard streams (`STDIN`, `STDOUT`,
94 94
 
95 95
     $ sudo docker run -a stdin -a stdout -i -t ubuntu /bin/bash
96 96
 
97
-For interactive processes (like a shell) you will typically want a tty
98
-as well as persistent standard input (`STDIN`), so you'll use `-i -t`
99
-together in most interactive cases.
97
+For interactive processes (like a shell), you must use `-i -t` together in
98
+order to allocate a tty for the container process. Specifying `-t` is however
99
+forbidden when the client standard output is redirected or pipe, such as in:
100
+`echo test | docker run -i busybox cat`.
100 101
 
101 102
 ## Container identification
102 103
 
... ...
@@ -87,3 +87,50 @@ func TestAttachMultipleAndRestart(t *testing.T) {
87 87
 
88 88
 	logDone("attach - multiple attach")
89 89
 }
90
+
91
+func TestAttachTtyWithoutStdin(t *testing.T) {
92
+	defer deleteAllContainers()
93
+
94
+	cmd := exec.Command(dockerBinary, "run", "-d", "-ti", "busybox")
95
+	out, _, err := runCommandWithOutput(cmd)
96
+	if err != nil {
97
+		t.Fatalf("failed to start container: %v (%v)", out, err)
98
+	}
99
+
100
+	id := strings.TrimSpace(out)
101
+	if err := waitRun(id); err != nil {
102
+		t.Fatal(err)
103
+	}
104
+
105
+	defer func() {
106
+		cmd := exec.Command(dockerBinary, "kill", id)
107
+		if out, _, err := runCommandWithOutput(cmd); err != nil {
108
+			t.Fatalf("failed to kill container: %v (%v)", out, err)
109
+		}
110
+	}()
111
+
112
+	done := make(chan struct{})
113
+	go func() {
114
+		defer close(done)
115
+
116
+		cmd := exec.Command(dockerBinary, "attach", id)
117
+		if _, err := cmd.StdinPipe(); err != nil {
118
+			t.Fatal(err)
119
+		}
120
+
121
+		expected := "cannot enable tty mode"
122
+		if out, _, err := runCommandWithOutput(cmd); err == nil {
123
+			t.Fatal("attach should have failed")
124
+		} else if !strings.Contains(out, expected) {
125
+			t.Fatal("attach failed with error %q: expected %q", out, expected)
126
+		}
127
+	}()
128
+
129
+	select {
130
+	case <-done:
131
+	case <-time.After(attachWait):
132
+		t.Fatal("attach is running but should have failed")
133
+	}
134
+
135
+	logDone("attach - forbid piped stdin to tty enabled container")
136
+}
... ...
@@ -273,7 +273,7 @@ func TestExecTtyCloseStdin(t *testing.T) {
273 273
 		t.Fatal(out, err)
274 274
 	}
275 275
 
276
-	cmd = exec.Command(dockerBinary, "exec", "-it", "exec_tty_stdin", "cat")
276
+	cmd = exec.Command(dockerBinary, "exec", "-i", "exec_tty_stdin", "cat")
277 277
 	stdinRw, err := cmd.StdinPipe()
278 278
 	if err != nil {
279 279
 		t.Fatal(err)
... ...
@@ -304,3 +304,50 @@ func TestExecTtyCloseStdin(t *testing.T) {
304 304
 
305 305
 	logDone("exec - stdin is closed properly with tty enabled")
306 306
 }
307
+
308
+func TestExecTtyWithoutStdin(t *testing.T) {
309
+	defer deleteAllContainers()
310
+
311
+	cmd := exec.Command(dockerBinary, "run", "-d", "-ti", "busybox")
312
+	out, _, err := runCommandWithOutput(cmd)
313
+	if err != nil {
314
+		t.Fatalf("failed to start container: %v (%v)", out, err)
315
+	}
316
+
317
+	id := strings.TrimSpace(out)
318
+	if err := waitRun(id); err != nil {
319
+		t.Fatal(err)
320
+	}
321
+
322
+	defer func() {
323
+		cmd := exec.Command(dockerBinary, "kill", id)
324
+		if out, _, err := runCommandWithOutput(cmd); err != nil {
325
+			t.Fatalf("failed to kill container: %v (%v)", out, err)
326
+		}
327
+	}()
328
+
329
+	done := make(chan struct{})
330
+	go func() {
331
+		defer close(done)
332
+
333
+		cmd := exec.Command(dockerBinary, "exec", "-ti", id, "true")
334
+		if _, err := cmd.StdinPipe(); err != nil {
335
+			t.Fatal(err)
336
+		}
337
+
338
+		expected := "cannot enable tty mode"
339
+		if out, _, err := runCommandWithOutput(cmd); err == nil {
340
+			t.Fatal("exec should have failed")
341
+		} else if !strings.Contains(out, expected) {
342
+			t.Fatal("exec failed with error %q: expected %q", out, expected)
343
+		}
344
+	}()
345
+
346
+	select {
347
+	case <-done:
348
+	case <-time.After(3 * time.Second):
349
+		t.Fatal("exec is running but should have failed")
350
+	}
351
+
352
+	logDone("exec - forbid piped stdin to tty enabled container")
353
+}
... ...
@@ -2742,3 +2742,32 @@ func TestRunPortFromDockerRangeInUse(t *testing.T) {
2742 2742
 
2743 2743
 	logDone("run - find another port if port from autorange already bound")
2744 2744
 }
2745
+
2746
+func TestRunTtyWithPipe(t *testing.T) {
2747
+	defer deleteAllContainers()
2748
+
2749
+	done := make(chan struct{})
2750
+	go func() {
2751
+		defer close(done)
2752
+
2753
+		cmd := exec.Command(dockerBinary, "run", "-ti", "busybox", "true")
2754
+		if _, err := cmd.StdinPipe(); err != nil {
2755
+			t.Fatal(err)
2756
+		}
2757
+
2758
+		expected := "cannot enable tty mode"
2759
+		if out, _, err := runCommandWithOutput(cmd); err == nil {
2760
+			t.Fatal("run should have failed")
2761
+		} else if !strings.Contains(out, expected) {
2762
+			t.Fatal("run failed with error %q: expected %q", out, expected)
2763
+		}
2764
+	}()
2765
+
2766
+	select {
2767
+	case <-done:
2768
+	case <-time.After(3 * time.Second):
2769
+		t.Fatal("container is running but should have failed")
2770
+	}
2771
+
2772
+	logDone("run - forbid piped stdin with tty")
2773
+}
... ...
@@ -15,6 +15,7 @@ import (
15 15
 	"github.com/docker/docker/pkg/term"
16 16
 	"github.com/docker/docker/utils"
17 17
 	"github.com/docker/libtrust"
18
+	"github.com/kr/pty"
18 19
 )
19 20
 
20 21
 func closeWrap(args ...io.Closer) error {
... ...
@@ -162,72 +163,20 @@ func TestRunDisconnect(t *testing.T) {
162 162
 	})
163 163
 }
164 164
 
165
-// Expected behaviour: the process stay alive when the client disconnects
166
-// but the client detaches.
167
-func TestRunDisconnectTty(t *testing.T) {
168
-
169
-	stdin, stdinPipe := io.Pipe()
165
+// TestRunDetach checks attaching and detaching with the escape sequence.
166
+func TestRunDetach(t *testing.T) {
170 167
 	stdout, stdoutPipe := io.Pipe()
171
-	key, err := libtrust.GenerateECP256PrivateKey()
168
+	cpty, tty, err := pty.Open()
172 169
 	if err != nil {
173 170
 		t.Fatal(err)
174 171
 	}
175 172
 
176
-	cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
177
-	defer cleanup(globalEngine, t)
178
-
179
-	c1 := make(chan struct{})
180
-	go func() {
181
-		defer close(c1)
182
-		// We're simulating a disconnect so the return value doesn't matter. What matters is the
183
-		// fact that CmdRun returns.
184
-		if err := cli.CmdRun("-i", "-t", unitTestImageID, "/bin/cat"); err != nil {
185
-			log.Debugf("Error CmdRun: %s", err)
186
-		}
187
-	}()
188
-
189
-	container := waitContainerStart(t, 10*time.Second)
190
-
191
-	state := setRaw(t, container)
192
-	defer unsetRaw(t, container, state)
193
-
194
-	// Client disconnect after run -i should keep stdin out in TTY mode
195
-	setTimeout(t, "Read/Write assertion timed out", 2*time.Second, func() {
196
-		if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
197
-			t.Fatal(err)
198
-		}
199
-	})
200
-
201
-	// Close pipes (simulate disconnect)
202
-	if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
203
-		t.Fatal(err)
204
-	}
205
-
206
-	// wait for CmdRun to return
207
-	setTimeout(t, "Waiting for CmdRun timed out", 5*time.Second, func() {
208
-		<-c1
209
-	})
210
-
211
-	// In tty mode, we expect the process to stay alive even after client's stdin closes.
212
-
213
-	// Give some time to monitor to do his thing
214
-	container.WaitStop(500 * time.Millisecond)
215
-	if !container.IsRunning() {
216
-		t.Fatalf("/bin/cat should  still be running after closing stdin (tty mode)")
217
-	}
218
-}
219
-
220
-// TestRunDetach checks attaching and detaching with the escape sequence.
221
-func TestRunDetach(t *testing.T) {
222
-
223
-	stdin, stdinPipe := io.Pipe()
224
-	stdout, stdoutPipe := io.Pipe()
225 173
 	key, err := libtrust.GenerateECP256PrivateKey()
226 174
 	if err != nil {
227 175
 		t.Fatal(err)
228 176
 	}
229 177
 
230
-	cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
178
+	cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
231 179
 	defer cleanup(globalEngine, t)
232 180
 
233 181
 	ch := make(chan struct{})
... ...
@@ -242,22 +191,22 @@ func TestRunDetach(t *testing.T) {
242 242
 	defer unsetRaw(t, container, state)
243 243
 
244 244
 	setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
245
-		if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
245
+		if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil {
246 246
 			t.Fatal(err)
247 247
 		}
248 248
 	})
249 249
 
250 250
 	setTimeout(t, "Escape sequence timeout", 5*time.Second, func() {
251
-		stdinPipe.Write([]byte{16})
251
+		cpty.Write([]byte{16})
252 252
 		time.Sleep(100 * time.Millisecond)
253
-		stdinPipe.Write([]byte{17})
253
+		cpty.Write([]byte{17})
254 254
 	})
255 255
 
256 256
 	// wait for CmdRun to return
257 257
 	setTimeout(t, "Waiting for CmdRun timed out", 15*time.Second, func() {
258 258
 		<-ch
259 259
 	})
260
-	closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
260
+	closeWrap(cpty, stdout, stdoutPipe)
261 261
 
262 262
 	time.Sleep(500 * time.Millisecond)
263 263
 	if !container.IsRunning() {
... ...
@@ -271,14 +220,18 @@ func TestRunDetach(t *testing.T) {
271 271
 
272 272
 // TestAttachDetach checks that attach in tty mode can be detached using the long container ID
273 273
 func TestAttachDetach(t *testing.T) {
274
-	stdin, stdinPipe := io.Pipe()
275 274
 	stdout, stdoutPipe := io.Pipe()
275
+	cpty, tty, err := pty.Open()
276
+	if err != nil {
277
+		t.Fatal(err)
278
+	}
279
+
276 280
 	key, err := libtrust.GenerateECP256PrivateKey()
277 281
 	if err != nil {
278 282
 		t.Fatal(err)
279 283
 	}
280 284
 
281
-	cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
285
+	cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
282 286
 	defer cleanup(globalEngine, t)
283 287
 
284 288
 	ch := make(chan struct{})
... ...
@@ -309,9 +262,13 @@ func TestAttachDetach(t *testing.T) {
309 309
 	state := setRaw(t, container)
310 310
 	defer unsetRaw(t, container, state)
311 311
 
312
-	stdin, stdinPipe = io.Pipe()
313 312
 	stdout, stdoutPipe = io.Pipe()
314
-	cli = client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
313
+	cpty, tty, err = pty.Open()
314
+	if err != nil {
315
+		t.Fatal(err)
316
+	}
317
+
318
+	cli = client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
315 319
 
316 320
 	ch = make(chan struct{})
317 321
 	go func() {
... ...
@@ -324,7 +281,7 @@ func TestAttachDetach(t *testing.T) {
324 324
 	}()
325 325
 
326 326
 	setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
327
-		if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
327
+		if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil {
328 328
 			if err != io.ErrClosedPipe {
329 329
 				t.Fatal(err)
330 330
 			}
... ...
@@ -332,9 +289,9 @@ func TestAttachDetach(t *testing.T) {
332 332
 	})
333 333
 
334 334
 	setTimeout(t, "Escape sequence timeout", 5*time.Second, func() {
335
-		stdinPipe.Write([]byte{16})
335
+		cpty.Write([]byte{16})
336 336
 		time.Sleep(100 * time.Millisecond)
337
-		stdinPipe.Write([]byte{17})
337
+		cpty.Write([]byte{17})
338 338
 	})
339 339
 
340 340
 	// wait for CmdRun to return
... ...
@@ -342,7 +299,7 @@ func TestAttachDetach(t *testing.T) {
342 342
 		<-ch
343 343
 	})
344 344
 
345
-	closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
345
+	closeWrap(cpty, stdout, stdoutPipe)
346 346
 
347 347
 	time.Sleep(500 * time.Millisecond)
348 348
 	if !container.IsRunning() {
... ...
@@ -356,14 +313,18 @@ func TestAttachDetach(t *testing.T) {
356 356
 
357 357
 // TestAttachDetachTruncatedID checks that attach in tty mode can be detached
358 358
 func TestAttachDetachTruncatedID(t *testing.T) {
359
-	stdin, stdinPipe := io.Pipe()
360 359
 	stdout, stdoutPipe := io.Pipe()
360
+	cpty, tty, err := pty.Open()
361
+	if err != nil {
362
+		t.Fatal(err)
363
+	}
364
+
361 365
 	key, err := libtrust.GenerateECP256PrivateKey()
362 366
 	if err != nil {
363 367
 		t.Fatal(err)
364 368
 	}
365 369
 
366
-	cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
370
+	cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
367 371
 	defer cleanup(globalEngine, t)
368 372
 
369 373
 	// Discard the CmdRun output
... ...
@@ -379,9 +340,13 @@ func TestAttachDetachTruncatedID(t *testing.T) {
379 379
 	state := setRaw(t, container)
380 380
 	defer unsetRaw(t, container, state)
381 381
 
382
-	stdin, stdinPipe = io.Pipe()
383 382
 	stdout, stdoutPipe = io.Pipe()
384
-	cli = client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
383
+	cpty, tty, err = pty.Open()
384
+	if err != nil {
385
+		t.Fatal(err)
386
+	}
387
+
388
+	cli = client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
385 389
 
386 390
 	ch := make(chan struct{})
387 391
 	go func() {
... ...
@@ -394,7 +359,7 @@ func TestAttachDetachTruncatedID(t *testing.T) {
394 394
 	}()
395 395
 
396 396
 	setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
397
-		if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
397
+		if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil {
398 398
 			if err != io.ErrClosedPipe {
399 399
 				t.Fatal(err)
400 400
 			}
... ...
@@ -402,16 +367,16 @@ func TestAttachDetachTruncatedID(t *testing.T) {
402 402
 	})
403 403
 
404 404
 	setTimeout(t, "Escape sequence timeout", 5*time.Second, func() {
405
-		stdinPipe.Write([]byte{16})
405
+		cpty.Write([]byte{16})
406 406
 		time.Sleep(100 * time.Millisecond)
407
-		stdinPipe.Write([]byte{17})
407
+		cpty.Write([]byte{17})
408 408
 	})
409 409
 
410 410
 	// wait for CmdRun to return
411 411
 	setTimeout(t, "Waiting for CmdAttach timed out", 15*time.Second, func() {
412 412
 		<-ch
413 413
 	})
414
-	closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
414
+	closeWrap(cpty, stdout, stdoutPipe)
415 415
 
416 416
 	time.Sleep(500 * time.Millisecond)
417 417
 	if !container.IsRunning() {
... ...
@@ -425,14 +390,18 @@ func TestAttachDetachTruncatedID(t *testing.T) {
425 425
 
426 426
 // Expected behaviour, the process stays alive when the client disconnects
427 427
 func TestAttachDisconnect(t *testing.T) {
428
-	stdin, stdinPipe := io.Pipe()
429 428
 	stdout, stdoutPipe := io.Pipe()
429
+	cpty, tty, err := pty.Open()
430
+	if err != nil {
431
+		t.Fatal(err)
432
+	}
433
+
430 434
 	key, err := libtrust.GenerateECP256PrivateKey()
431 435
 	if err != nil {
432 436
 		t.Fatal(err)
433 437
 	}
434 438
 
435
-	cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
439
+	cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
436 440
 	defer cleanup(globalEngine, t)
437 441
 
438 442
 	go func() {
... ...
@@ -470,12 +439,12 @@ func TestAttachDisconnect(t *testing.T) {
470 470
 	}()
471 471
 
472 472
 	setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
473
-		if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
473
+		if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil {
474 474
 			t.Fatal(err)
475 475
 		}
476 476
 	})
477 477
 	// Close pipes (client disconnects)
478
-	if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
478
+	if err := closeWrap(cpty, stdout, stdoutPipe); err != nil {
479 479
 		t.Fatal(err)
480 480
 	}
481 481