Skip to content

Commit d40bb9d

Browse files
authored
Merge pull request #4346 from docker/fix/websocket-stream-lease
fix(openai): lease WebSocket connections exclusively
2 parents 47ef6ba + c713e64 commit d40bb9d

14 files changed

Lines changed: 1709 additions & 87 deletions

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ require (
9292
golang.org/x/sync v0.23.0
9393
golang.org/x/sys v0.48.0
9494
golang.org/x/term v0.46.0
95+
golang.org/x/tools v0.49.0
9596
google.golang.org/adk/v2 v2.4.0
9697
google.golang.org/genai v1.71.0
9798
google.golang.org/protobuf v1.36.12
@@ -222,7 +223,6 @@ require (
222223
golang.org/x/mod v0.41.0 // indirect
223224
golang.org/x/text v0.42.0 // indirect
224225
golang.org/x/time v0.15.0 // indirect
225-
golang.org/x/tools v0.49.0 // indirect
226226
google.golang.org/api v0.295.0 // indirect
227227
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect
228228
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect

‎lint/exclusive_stream_lease.go‎

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package main
2+
3+
import (
4+
"go/ast"
5+
"go/types"
6+
"maps"
7+
8+
"github.com/dgageot/rubocop-go/cop"
9+
"github.com/dgageot/rubocop-go/prog"
10+
"golang.org/x/tools/go/cfg"
11+
)
12+
13+
// ExclusiveStreamLease catches returned WebSocket readers that still alias a
14+
// receiver field. A factory lock does not protect the returned reader's lifetime.
15+
// Take-and-clear transfers ownership; a completed stream may return it later.
16+
// Analysis is intra-procedural: calls returning WebSocket wrappers may alias
17+
// their arguments. Helper side effects, receiver aliases, container fields and
18+
// arbitrary lease protocols aren't proved.
19+
var ExclusiveStreamLease = &prog.Func{
20+
Meta: exclusiveStreamLeaseFile.Meta,
21+
Run: func(p *prog.Pass) {
22+
// The file runner has partial types without imports; connection identity needs resolved dependencies.
23+
for _, pkg := range p.Program.Packages {
24+
for _, file := range pkg.Syntax {
25+
pass := &cop.Pass{Cop: exclusiveStreamLeaseFile, FileSet: p.Program.Fset, File: file, Info: pkg.TypesInfo, Package: pkg.Types}
26+
exclusiveStreamLeaseFile.Check(pass)
27+
for _, offense := range pass.Offenses() {
28+
tf := p.Program.Fset.File(file.Pos())
29+
p.ReportAtf(tf.Pos(offense.Pos.Offset), tf.Pos(offense.End.Offset), "%s", offense.Message)
30+
}
31+
}
32+
}
33+
},
34+
}
35+
36+
var exclusiveStreamLeaseFile = &cop.Func{
37+
Meta: cop.Meta{
38+
Name: "Lint/ExclusiveStreamLease",
39+
Description: "transfer WebSocket ownership out of a pool before returning a stream",
40+
Severity: cop.Error,
41+
},
42+
Types: true,
43+
Run: func(p *cop.Pass) {
44+
if p.IsTestFile() || p.Info == nil {
45+
return
46+
}
47+
p.ForEachFunc(func(fn *ast.FuncDecl) {
48+
if fn.Recv == nil || fn.Body == nil || len(fn.Recv.List[0].Names) == 0 {
49+
return
50+
}
51+
checkExclusiveStreamLease(p, fn)
52+
})
53+
},
54+
}
55+
56+
type leaseOrigins map[any]struct{}
57+
58+
type leaseState map[types.Object]leaseOrigins
59+
60+
type leaseAnalysis struct {
61+
pass *cop.Pass
62+
receiver types.Object
63+
fields []*types.Var
64+
}
65+
66+
func checkExclusiveStreamLease(p *cop.Pass, fn *ast.FuncDecl) {
67+
a := leaseAnalysis{pass: p, receiver: p.Info.ObjectOf(fn.Recv.List[0].Names[0])}
68+
if a.receiver == nil {
69+
return
70+
}
71+
t := a.receiver.Type()
72+
if ptr, ok := t.(*types.Pointer); ok {
73+
t = ptr.Elem()
74+
}
75+
st, ok := t.Underlying().(*types.Struct)
76+
if !ok {
77+
return
78+
}
79+
initial := make(leaseState)
80+
for field := range st.Fields() {
81+
if containsLeaseConn(field.Type(), make(map[types.Type]bool)) {
82+
a.fields = append(a.fields, field)
83+
initial[field] = leaseOrigins{field: {}}
84+
}
85+
}
86+
if len(a.fields) == 0 {
87+
return
88+
}
89+
graph := cfg.New(fn.Body, func(*ast.CallExpr) bool { return true })
90+
states := map[*cfg.Block]leaseState{graph.Blocks[0]: initial}
91+
queue := []*cfg.Block{graph.Blocks[0]}
92+
for len(queue) > 0 {
93+
block := queue[0]
94+
queue = queue[1:]
95+
state := cloneLeaseState(states[block])
96+
for _, node := range block.Nodes {
97+
a.assign(node, state)
98+
}
99+
for _, next := range block.Succs {
100+
if states[next] == nil {
101+
states[next] = cloneLeaseState(state)
102+
queue = append(queue, next)
103+
} else if mergeLeaseState(states[next], state) {
104+
queue = append(queue, next)
105+
}
106+
}
107+
}
108+
for _, block := range graph.Blocks {
109+
if states[block] == nil {
110+
continue
111+
}
112+
state := cloneLeaseState(states[block])
113+
for _, node := range block.Nodes {
114+
a.assign(node, state)
115+
ret, ok := node.(*ast.ReturnStmt)
116+
if !ok {
117+
continue
118+
}
119+
results := ret.Results
120+
if len(results) == 0 && fn.Type.Results != nil {
121+
for _, field := range fn.Type.Results.List {
122+
for _, name := range field.Names {
123+
results = append(results, name)
124+
}
125+
}
126+
}
127+
for _, result := range results {
128+
origins := a.origins(result, state)
129+
for _, field := range a.fields {
130+
for origin := range state[field] {
131+
if _, shared := origins[origin]; shared {
132+
p.Reportf(ret, "returned WebSocket stream aliases receiver field %s; remove the leased connection from the pool before returning it", field.Name())
133+
return
134+
}
135+
}
136+
}
137+
}
138+
}
139+
}
140+
}
141+
142+
func containsLeaseConn(t types.Type, seen map[types.Type]bool) bool {
143+
if t == nil || seen[t] {
144+
return false
145+
}
146+
seen[t] = true
147+
switch t := types.Unalias(t).(type) {
148+
case *types.Pointer:
149+
return containsLeaseConn(t.Elem(), seen)
150+
case *types.Named:
151+
obj := t.Obj()
152+
if obj.Pkg() != nil && obj.Pkg().Path() == "github.com/gorilla/websocket" && obj.Name() == "Conn" {
153+
return true
154+
}
155+
return containsLeaseConn(t.Underlying(), seen)
156+
case *types.Struct:
157+
for field := range t.Fields() {
158+
if containsLeaseConn(field.Type(), seen) {
159+
return true
160+
}
161+
}
162+
case *types.Tuple:
163+
for variable := range t.Variables() {
164+
if containsLeaseConn(variable.Type(), seen) {
165+
return true
166+
}
167+
}
168+
}
169+
return false
170+
}
171+
172+
func (a *leaseAnalysis) object(expr ast.Expr) types.Object {
173+
switch expr := ast.Unparen(expr).(type) {
174+
case *ast.Ident:
175+
return a.pass.Info.ObjectOf(expr)
176+
case *ast.SelectorExpr:
177+
if id, ok := ast.Unparen(expr.X).(*ast.Ident); ok && a.pass.Info.ObjectOf(id) == a.receiver {
178+
for _, field := range a.fields {
179+
if field.Name() == expr.Sel.Name {
180+
return field
181+
}
182+
}
183+
}
184+
}
185+
return nil
186+
}
187+
188+
func (a *leaseAnalysis) origins(expr ast.Expr, state leaseState) leaseOrigins {
189+
if expr == nil {
190+
return nil
191+
}
192+
if obj := a.object(expr); obj != nil {
193+
return state[obj]
194+
}
195+
switch expr := ast.Unparen(expr).(type) {
196+
case *ast.SelectorExpr:
197+
if containsLeaseConn(a.pass.Info.TypeOf(expr), make(map[types.Type]bool)) {
198+
return a.origins(expr.X, state)
199+
}
200+
case *ast.UnaryExpr:
201+
return a.origins(expr.X, state)
202+
case *ast.StarExpr:
203+
return a.origins(expr.X, state)
204+
case *ast.TypeAssertExpr:
205+
return a.origins(expr.X, state)
206+
case *ast.CompositeLit:
207+
origins := make(leaseOrigins)
208+
for _, elt := range expr.Elts {
209+
if kv, ok := elt.(*ast.KeyValueExpr); ok {
210+
elt = kv.Value
211+
}
212+
maps.Copy(origins, a.origins(elt, state))
213+
}
214+
return origins
215+
case *ast.CallExpr:
216+
if containsLeaseConn(a.pass.Info.TypeOf(expr), make(map[types.Type]bool)) {
217+
origins := make(leaseOrigins)
218+
for _, arg := range expr.Args {
219+
maps.Copy(origins, a.origins(arg, state))
220+
}
221+
if len(origins) == 0 {
222+
origins[expr] = struct{}{}
223+
}
224+
return origins
225+
}
226+
}
227+
return nil
228+
}
229+
230+
func (a *leaseAnalysis) assign(node ast.Node, state leaseState) {
231+
var lhs, rhs []ast.Expr
232+
switch node := node.(type) {
233+
case *ast.AssignStmt:
234+
lhs, rhs = node.Lhs, node.Rhs
235+
case *ast.ValueSpec:
236+
for _, name := range node.Names {
237+
lhs = append(lhs, name)
238+
}
239+
rhs = node.Values
240+
default:
241+
return
242+
}
243+
values := make([]leaseOrigins, len(lhs))
244+
for i := range lhs {
245+
if i < len(rhs) {
246+
values[i] = a.origins(rhs[i], state)
247+
} else if len(rhs) == 1 && containsLeaseConn(a.pass.Info.TypeOf(lhs[i]), make(map[types.Type]bool)) {
248+
values[i] = a.origins(rhs[0], state)
249+
}
250+
}
251+
for i, expr := range lhs {
252+
if obj := a.object(expr); obj != nil {
253+
state[obj] = values[i]
254+
}
255+
}
256+
}
257+
258+
func cloneLeaseState(state leaseState) leaseState {
259+
clone := make(leaseState, len(state))
260+
for obj, origins := range state {
261+
clone[obj] = maps.Clone(origins)
262+
}
263+
return clone
264+
}
265+
266+
func mergeLeaseState(dst, src leaseState) bool {
267+
changed := false
268+
for obj, origins := range src {
269+
if dst[obj] == nil {
270+
dst[obj] = make(leaseOrigins)
271+
}
272+
for origin := range origins {
273+
if _, ok := dst[obj][origin]; !ok {
274+
dst[obj][origin] = struct{}{}
275+
changed = true
276+
}
277+
}
278+
}
279+
return changed
280+
}

0 commit comments

Comments
 (0)