|
| 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