Skip to content

Commit 425edbb

Browse files
authored
Merge pull request kubernetes#94589 from p0lyn0mial/racy-serialize-object
fixes a data race in SerializeObject function
2 parents dafc4a8 + e6f9831 commit 425edbb

File tree

2 files changed

+94
-5
lines changed

2 files changed

+94
-5
lines changed

staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,11 @@ func SerializeObject(mediaType string, encoder runtime.Encoder, hw http.Response
9696
err := encoder.Encode(object, w)
9797
if err == nil {
9898
err = w.Close()
99-
if err == nil {
100-
return
99+
if err != nil {
100+
// we cannot write an error to the writer anymore as the Encode call was successful.
101+
utilruntime.HandleError(fmt.Errorf("apiserver was unable to close cleanly the response writer: %v", err))
101102
}
103+
return
102104
}
103105

104106
// make a best effort to write the object if a failure is detected

staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers_test.go

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"bytes"
2121
"compress/gzip"
2222
"encoding/hex"
23+
"errors"
2324
"fmt"
2425
"io"
2526
"io/ioutil"
@@ -28,7 +29,7 @@ import (
2829
"reflect"
2930
"testing"
3031

31-
"k8s.io/apimachinery/pkg/api/errors"
32+
kerrors "k8s.io/apimachinery/pkg/api/errors"
3233
"k8s.io/apimachinery/pkg/runtime"
3334
"k8s.io/apimachinery/pkg/runtime/schema"
3435
"k8s.io/apimachinery/pkg/util/diff"
@@ -37,6 +38,76 @@ import (
3738
featuregatetesting "k8s.io/component-base/featuregate/testing"
3839
)
3940

41+
func TestSerializeObjectParallel(t *testing.T) {
42+
largePayload := bytes.Repeat([]byte("0123456789abcdef"), defaultGzipThresholdBytes/16+1)
43+
type test struct {
44+
name string
45+
46+
compressionEnabled bool
47+
48+
mediaType string
49+
out []byte
50+
outErrs []error
51+
req *http.Request
52+
statusCode int
53+
object runtime.Object
54+
55+
wantCode int
56+
wantHeaders http.Header
57+
wantBody []byte
58+
}
59+
newTest := func() test {
60+
return test{
61+
name: "compress on gzip",
62+
compressionEnabled: true,
63+
out: largePayload,
64+
mediaType: "application/json",
65+
req: &http.Request{Header: http.Header{
66+
"Accept-Encoding": []string{"gzip"},
67+
}},
68+
wantCode: http.StatusOK,
69+
wantHeaders: http.Header{
70+
"Content-Type": []string{"application/json"},
71+
"Content-Encoding": []string{"gzip"},
72+
"Vary": []string{"Accept-Encoding"},
73+
},
74+
}
75+
}
76+
for i := 0; i < 100; i++ {
77+
ctt := newTest()
78+
t.Run(ctt.name, func(t *testing.T) {
79+
defer func() {
80+
if r := recover(); r != nil {
81+
t.Fatalf("recovered from err %v", r)
82+
}
83+
}()
84+
t.Parallel()
85+
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIResponseCompression, ctt.compressionEnabled)()
86+
87+
encoder := &fakeEncoder{
88+
buf: ctt.out,
89+
errs: ctt.outErrs,
90+
}
91+
if ctt.statusCode == 0 {
92+
ctt.statusCode = http.StatusOK
93+
}
94+
recorder := &fakeResponseRecorder{
95+
ResponseRecorder: httptest.NewRecorder(),
96+
fe: encoder,
97+
errorAfterEncoding: true,
98+
}
99+
SerializeObject(ctt.mediaType, encoder, recorder, ctt.req, ctt.statusCode, ctt.object)
100+
result := recorder.Result()
101+
if result.StatusCode != ctt.wantCode {
102+
t.Fatalf("unexpected code: %v", result.StatusCode)
103+
}
104+
if !reflect.DeepEqual(result.Header, ctt.wantHeaders) {
105+
t.Fatal(diff.ObjectReflectDiff(ctt.wantHeaders, result.Header))
106+
}
107+
})
108+
}
109+
}
110+
40111
func TestSerializeObject(t *testing.T) {
41112
smallPayload := []byte("{test-object,test-object}")
42113
largePayload := bytes.Repeat([]byte("0123456789abcdef"), defaultGzipThresholdBytes/16+1)
@@ -111,7 +182,7 @@ func TestSerializeObject(t *testing.T) {
111182
{
112183
name: "fail to encode object or status with status code",
113184
out: smallPayload,
114-
outErrs: []error{errors.NewNotFound(schema.GroupResource{}, "test"), fmt.Errorf("bad2")},
185+
outErrs: []error{kerrors.NewNotFound(schema.GroupResource{}, "test"), fmt.Errorf("bad2")},
115186
mediaType: "application/json",
116187
req: &http.Request{Header: http.Header{}},
117188
statusCode: http.StatusOK,
@@ -123,7 +194,7 @@ func TestSerializeObject(t *testing.T) {
123194
{
124195
name: "fail to encode object or status with status code and keeps previous error",
125196
out: smallPayload,
126-
outErrs: []error{errors.NewNotFound(schema.GroupResource{}, "test"), fmt.Errorf("bad2")},
197+
outErrs: []error{kerrors.NewNotFound(schema.GroupResource{}, "test"), fmt.Errorf("bad2")},
127198
mediaType: "application/json",
128199
req: &http.Request{Header: http.Header{}},
129200
statusCode: http.StatusNotAcceptable,
@@ -270,10 +341,25 @@ func TestSerializeObject(t *testing.T) {
270341
}
271342
}
272343

344+
type fakeResponseRecorder struct {
345+
*httptest.ResponseRecorder
346+
fe *fakeEncoder
347+
errorAfterEncoding bool
348+
}
349+
350+
func (frw *fakeResponseRecorder) Write(buf []byte) (int, error) {
351+
if frw.errorAfterEncoding && frw.fe.encodeCalled {
352+
return 0, errors.New("returning a requested error")
353+
}
354+
return frw.ResponseRecorder.Write(buf)
355+
}
356+
273357
type fakeEncoder struct {
274358
obj runtime.Object
275359
buf []byte
276360
errs []error
361+
362+
encodeCalled bool
277363
}
278364

279365
func (e *fakeEncoder) Encode(obj runtime.Object, w io.Writer) error {
@@ -284,6 +370,7 @@ func (e *fakeEncoder) Encode(obj runtime.Object, w io.Writer) error {
284370
return err
285371
}
286372
_, err := w.Write(e.buf)
373+
e.encodeCalled = true
287374
return err
288375
}
289376

0 commit comments

Comments
 (0)