XHTTP client: Fix a race condition and a data race - #6665
Conversation
`uploadWriter.Write` read `buff.Len()` after handing the buffer to the pipe. Past that point the buffer belongs to the pipe's reader, which drains it into the body of the POST request -- `MultiBufferContainer.Read` -> `SplitBytes` -> `Buffer.Read`, and `Buffer.Read` calls `Clear()` once the buffer runs out, zeroing start and end while the writer is still reading them. The result is not only a race but a short count: with `Len()` reading zero, `Write` reports fewer bytes than it accepted, and `buf.WriteAllBytes` advances its payload by that count in a loop, so the same bytes go out a second time. Those bytes are already in the pipe and already on their way, so the proxied stream gets duplicated data. Should the buffer have been recycled and refilled instead, `Len()` can read larger than expected and the caller's `payload[n:]` panics on the slice bounds. Taking the length before the write keeps the deliberate per-buffer splitting that bounds how far a single ReadMultiBuffer may exceed the pipe's size limit. `DefaultDialerClient.closed` was a plain bool written from concurrent goroutines -- one per uplink packet in packet-up, plus the response goroutine in OpenStream -- and read by XMUX in `GetXmuxClient` under a mutex the writers never take, so there is no happens-before between them. A byte store does not tear on amd64, but nothing orders it either: the reader may keep observing a stale false and go on handing new proxied requests to a dead connection until `hMaxRequestTimes` or `hMaxReusableSecs` evicts it. Made it an atomic.Bool, matching `LeftRequests`, `Running` and `NotUsed` next to it. Both races reproduce under `go test -race` and are gone after this change
|
第二个,在 amd64 架构上会这样的深层原因是? |
|
主要是回的writed不对 那个closed理论上是没什么问题的 爱改atomic就顺带改了吧 |
|
Google AI 的说法是:
|
|
这么写的旧代码在全世界都不少 真出问题就大爆爆了 现代调度不会这么蠢的 也就百来个周期而已 实际上不是极其高频而且强要求一致性基本遇不到 更别说这几个认错了也问题不大可以恢复的 |
|
我也觉得,主要是它这块内存只有 0 或 1 两种可能,多核并发读写理应也没啥问题,不过若有多核缓存不一致问题的话就另说, |
1.
uploadWriter.Writereads a buffer it no longer ownsOnce
WriteMultiBuffersucceeds, the buffer belongs to the pipe's reader. Thatreader is the upload loop in
Dial, which drains it into the body of the POSTrequest —
MultiBufferContainer.Read→SplitBytes→Buffer.Read— andBuffer.ReadcallsClear()once the buffer runs out, zeroingstartandendwhile
Len()is being read.This is not only a race. With
Len()reading zero,Writereports fewer bytesthan it accepted, and
buf.WriteAllBytesadvances its payload by the returnedcount in a loop:
so the same bytes go out a second time. They are already in the pipe and already
on their way to the server, so the proxied stream carries duplicated data. Should
the buffer have been recycled and refilled instead,
Len()can read larger thanexpected and the caller's
payload[n:]panics on the slice bounds.Reading the length before the write keeps the deliberate per-buffer splitting
that bounds how far a single
ReadMultiBuffermay exceed the pipe's size limit,so nothing else about the behaviour changes.
2.
DefaultDialerClient.closedis a plain boolIt is written from concurrent goroutines — one per uplink packet in
packet-up,plus the response goroutine in
OpenStream— and read byGetXmuxClientunderglobalDialerAccess, a mutex the writers never take, so there is nohappens-before between them.
A byte store does not tear on amd64, but nothing orders it either: the reader can
keep observing a stale
falseand go on handing new proxied requests to a deadconnection until
hMaxRequestTimesorhMaxReusableSecsevicts it. Making it anatomic.BoolmatchesLeftRequests,RunningandNotUsedused a few linesaway in the very same
GetXmuxClientcheck.Reproduction
The first one, in
package splithttp:The second one needs only concurrent
PostPacketcalls against a transport thatalways errors, plus one goroutine calling
IsClosed().On the unpatched tree the detector reports the race on every run, while the short
count itself lands in roughly 3 runs out of 5, 1–4 times per 3000 writes.
Verification
go build ./...clean,go vetunchanged, package tests pass.go test -race ./transport/internet/splithttp/drops from ~20 race reports to~12. What remains is
WaitReadCloser.ReadCloserand the certificate cache intransport/internet/tls, both unrelated to this change — the same 7 tests failunder
-racebefore and after it.