Commit 1ac67bc

Antonio Mika  ·  2026-10-09 10:56:11 -0400 EDT
parent a1973f3
fix(rsync): ensure no leaked routines on failed upload
3 files changed,  +49, -12
+9, -5
......@@ -32,7 +32,6 @@ type muxWriter struct {
3232 multiplex bool
3333 written int64
3434 err error
35- closed bool
3635 }
3736
3837 type muxItem struct {
......@@ -115,11 +114,16 @@ func (m *muxWriter) Flush() error {
115114 return m.err
116115 }
117116
118-// Close flushes and stops the writer goroutine.
117+// errMuxClosed is returned to writers that outlive Close.
118+var errMuxClosed = errors.New("rsync: write after close")
119+
120+// Close flushes and stops the writer goroutine. Later writes fail.
119121 func (m *muxWriter) Close() error {
120122 err := m.Flush()
121123 m.mu.Lock()
122- m.closed = true
124+ if m.err == nil {
125+ m.err = errMuxClosed
126+ }
123127 m.cond.Broadcast()
124128 m.mu.Unlock()
125129 return err
......@@ -139,10 +143,10 @@ func (m *muxWriter) pump() {
139143 m.mu.Lock()
140144 defer m.mu.Unlock()
141145 for {
142- for !m.closed && m.err == nil && (!m.kick || len(m.queue) == 0) {
146+ for m.err == nil && (!m.kick || len(m.queue) == 0) {
143147 m.cond.Wait()
144148 }
145- if m.closed || m.err != nil {
149+ if m.err != nil {
146150 m.queue = nil
147151 m.cond.Broadcast()
148152 return
+3, -0
......@@ -205,6 +205,9 @@ func (g *generator) run() error {
205205 if f.inactive {
206206 continue
207207 }
208+ if err := g.ctx.Err(); err != nil {
209+ return err
210+ }
208211 if err := g.recvGenerator(int32(i), f, false); err != nil {
209212 return err
210213 }
+37, -7
......@@ -5,23 +5,25 @@ import (
55 "io"
66 "slices"
77 "testing"
8+ "time"
89 )
910
1011 type loopback struct {
1112 bytes.Buffer
1213 }
1314
14-func newTestConn(protocol int) (*conn, *loopback) {
15+func newTestConn(t *testing.T, protocol int) (*conn, *loopback) {
1516 var buf loopback
1617 c := newConn(&buf)
1718 c.protocol = protocol
19+ t.Cleanup(func() { _ = c.close() })
1820 return c, &buf
1921 }
2022
2123 func TestVarintRoundTrip(t *testing.T) {
2224 values := []int32{0, 1, 0x7f, 0x80, 0xff, 0x3fff, 0x4000, 0x1fffff, 0x200000, 0x0fffffff, 0x10000000, 0x7fffffff, -1, -2, -0x80000000}
2325 for _, v := range values {
24- c, _ := newTestConn(31)
26+ c, _ := newTestConn(t, 31)
2527 if err := c.writeVarint(v); err != nil {
2628 t.Fatal(err)
2729 }
......@@ -42,7 +44,7 @@ func TestVarlongRoundTrip(t *testing.T) {
4244 values := []int64{0, 1, 0xffffff, 0x1000000, 0x7fffffff, 0x80000000, 1 << 40, 1<<62 + 12345, -1}
4345 for _, min := range []int{3, 4} {
4446 for _, v := range values {
45- c, _ := newTestConn(31)
47+ c, _ := newTestConn(t, 31)
4648 if err := c.writeVarlong(v, min); err != nil {
4749 t.Fatal(err)
4850 }
......@@ -59,7 +61,7 @@ func TestVarlongRoundTrip(t *testing.T) {
5961
6062 func TestLongintRoundTrip(t *testing.T) {
6163 for _, v := range []int64{0, 0x7fffffff, 0x80000000, 1 << 40, -5} {
62- c, _ := newTestConn(29)
64+ c, _ := newTestConn(t, 29)
6365 if err := c.writeLongint(v); err != nil {
6466 t.Fatal(err)
6567 }
......@@ -76,7 +78,7 @@ func TestLongintRoundTrip(t *testing.T) {
7678 func TestNdxRoundTrip(t *testing.T) {
7779 seq := []int32{0, 1, 2, 5, 300, 299, 40000, 70000, 3, ndxDone, ndxDelStats, ndxFlistEOF, 7, ndxDone}
7880 for _, proto := range []int{29, 30} {
79- c, _ := newTestConn(proto)
81+ c, _ := newTestConn(t, proto)
8082 for _, v := range seq {
8183 if err := c.writeNdx(v); err != nil {
8284 t.Fatal(err)
......@@ -95,7 +97,7 @@ func TestNdxRoundTrip(t *testing.T) {
9597 }
9698
9799 func TestMultiplexedMessages(t *testing.T) {
98- c, buf := newTestConn(31)
100+ c, buf := newTestConn(t, 31)
99101 if err := c.w.setMultiplex(true); err != nil {
100102 t.Fatal(err)
101103 }
......@@ -106,7 +108,7 @@ func TestMultiplexedMessages(t *testing.T) {
106108 t.Fatal(err)
107109 }
108110
109- r, _ := newTestConn(31)
111+ r, _ := newTestConn(t, 31)
110112 r.r.r.Reset(bytes.NewReader(buf.Bytes()))
111113 r.r.multiplex = true
112114 var msgs []string
......@@ -130,6 +132,34 @@ func TestMultiplexedMessages(t *testing.T) {
130132 }
131133 }
132134
135+// A generator can still be writing when the session gives up and closes the
136+// writer; it has to get an error instead of waiting on a queue nobody drains.
137+func TestMuxWriterAfterClose(t *testing.T) {
138+ m := newMuxWriter(io.Discard)
139+ if err := m.Close(); err != nil {
140+ t.Fatal(err)
141+ }
142+ done := make(chan error, 1)
143+ go func() {
144+ chunk := make([]byte, muxBatchSize)
145+ for range 2 * muxMaxQueued / muxBatchSize {
146+ if _, err := m.Write(chunk); err != nil {
147+ done <- err
148+ return
149+ }
150+ }
151+ done <- m.Flush()
152+ }()
153+ select {
154+ case err := <-done:
155+ if err == nil {
156+ t.Fatal("writes after Close succeeded")
157+ }
158+ case <-time.After(5 * time.Second):
159+ t.Fatal("write after Close blocked")
160+ }
161+}
162+
133163 func TestChecksum1(t *testing.T) {
134164 // Bytes above 0x7f count as negative, as in rsync's C code.
135165 if got := checksum1([]byte{0xff, 1, 2, 3, 4, 5}); got != 0x001d000e {