Skip to content

Commit 5db255f

Browse files
committed
net/http/cgi: kill child CGI process on copy error
Fixes #7196 LGTM=rsc R=golang-codereviews, rsc CC=golang-codereviews, iant https://golang.org/cl/69970052
1 parent a766277 commit 5db255f

File tree

2 files changed

+104
-1
lines changed

2 files changed

+104
-1
lines changed

src/pkg/net/http/cgi/host.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
214214
internalError(err)
215215
return
216216
}
217+
if hook := testHookStartProcess; hook != nil {
218+
hook(cmd.Process)
219+
}
217220
defer cmd.Wait()
218221
defer stdoutRead.Close()
219222

@@ -292,6 +295,13 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
292295
_, err = io.Copy(rw, linebody)
293296
if err != nil {
294297
h.printf("cgi: copy error: %v", err)
298+
// And kill the child CGI process so we don't hang on
299+
// the deferred cmd.Wait above if the error was just
300+
// the client (rw) going away. If it was a read error
301+
// (because the child died itself), then the extra
302+
// kill of an already-dead process is harmless (the PID
303+
// won't be reused until the Wait above).
304+
cmd.Process.Kill()
295305
}
296306
}
297307

@@ -348,3 +358,5 @@ func upperCaseAndUnderscore(r rune) rune {
348358
// TODO: other transformations in spec or practice?
349359
return r
350360
}
361+
362+
var testHookStartProcess func(*os.Process) // nil except for some tests

src/pkg/net/http/cgi/matryoshka_test.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@
99
package cgi
1010

1111
import (
12+
"bytes"
13+
"errors"
1214
"fmt"
15+
"io"
1316
"net/http"
17+
"net/http/httptest"
1418
"os"
1519
"testing"
20+
"time"
1621
)
1722

1823
// This test is a CGI host (testing host.go) that runs its own binary
@@ -51,7 +56,78 @@ func TestHostingOurselves(t *testing.T) {
5156
}
5257
}
5358

54-
// Test that a child handler only writing headers works.
59+
type customWriterRecorder struct {
60+
w io.Writer
61+
*httptest.ResponseRecorder
62+
}
63+
64+
func (r *customWriterRecorder) Write(p []byte) (n int, err error) {
65+
return r.w.Write(p)
66+
}
67+
68+
type limitWriter struct {
69+
w io.Writer
70+
n int
71+
}
72+
73+
func (w *limitWriter) Write(p []byte) (n int, err error) {
74+
if len(p) > w.n {
75+
p = p[:w.n]
76+
}
77+
if len(p) > 0 {
78+
n, err = w.w.Write(p)
79+
w.n -= n
80+
}
81+
if w.n == 0 {
82+
err = errors.New("past write limit")
83+
}
84+
return
85+
}
86+
87+
// If there's an error copying the child's output to the parent, test
88+
// that we kill the child.
89+
func TestKillChildAfterCopyError(t *testing.T) {
90+
defer func() { testHookStartProcess = nil }()
91+
proc := make(chan *os.Process, 1)
92+
testHookStartProcess = func(p *os.Process) {
93+
proc <- p
94+
}
95+
96+
h := &Handler{
97+
Path: os.Args[0],
98+
Root: "/test.go",
99+
Args: []string{"-test.run=TestBeChildCGIProcess"},
100+
}
101+
req, _ := http.NewRequest("GET", "http://example.com/test.cgi?write-forever=1", nil)
102+
rec := httptest.NewRecorder()
103+
var out bytes.Buffer
104+
const writeLen = 50 << 10
105+
rw := &customWriterRecorder{&limitWriter{&out, writeLen}, rec}
106+
107+
donec := make(chan bool, 1)
108+
go func() {
109+
h.ServeHTTP(rw, req)
110+
donec <- true
111+
}()
112+
113+
select {
114+
case <-donec:
115+
if out.Len() != writeLen || out.Bytes()[0] != 'a' {
116+
t.Errorf("unexpected output: %q", out.Bytes())
117+
}
118+
case <-time.After(5 * time.Second):
119+
t.Errorf("timeout. ServeHTTP hung and didn't kill the child process?")
120+
select {
121+
case p := <-proc:
122+
p.Kill()
123+
t.Logf("killed process")
124+
default:
125+
t.Logf("didn't kill process")
126+
}
127+
}
128+
}
129+
130+
// Test that a child handler writing only headers works.
55131
func TestChildOnlyHeaders(t *testing.T) {
56132
h := &Handler{
57133
Path: os.Args[0],
@@ -67,6 +143,15 @@ func TestChildOnlyHeaders(t *testing.T) {
67143
}
68144
}
69145

146+
type neverEnding byte
147+
148+
func (b neverEnding) Read(p []byte) (n int, err error) {
149+
for i := range p {
150+
p[i] = byte(b)
151+
}
152+
return len(p), nil
153+
}
154+
70155
// Note: not actually a test.
71156
func TestBeChildCGIProcess(t *testing.T) {
72157
if os.Getenv("REQUEST_METHOD") == "" {
@@ -79,6 +164,12 @@ func TestBeChildCGIProcess(t *testing.T) {
79164
if req.FormValue("no-body") == "1" {
80165
return
81166
}
167+
if req.FormValue("write-forever") == "1" {
168+
io.Copy(rw, neverEnding('a'))
169+
for {
170+
time.Sleep(5 * time.Second) // hang forever, until killed
171+
}
172+
}
82173
fmt.Fprintf(rw, "test=Hello CGI-in-CGI\n")
83174
for k, vv := range req.Form {
84175
for _, v := range vv {

0 commit comments

Comments
 (0)