Skip to content

Commit 8895462

Browse files
authored
fix(G118): eliminate false positive when cancel is called via struct field in a closure (#1596)
* fix(G118): eliminate false positive when cancel stored in struct field post-construction When a cancel function is assigned to a struct field after construction (e.g. s.cancel = cancel), the SSA FieldAddr for the store is a distinct value from any FieldAddr created later for defer s.cancel() or inside a closure. The existing isCancelCalledViaStructField only matched receiver methods and missed these patterns. Add isFieldCalledInAnyFunc which scans all SSA functions (including closures) for a FieldAddr with matching struct pointer type and field index, then checks whether the loaded value is called. As a side effect, this also resolves the known false positive for nested struct field access. fixes: 1595 * update rules documentation
1 parent 619ce21 commit 8895462

File tree

3 files changed

+164
-3
lines changed

3 files changed

+164
-3
lines changed

RULES.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [G104](#g104)
1717
- [G111](#g111)
1818
- [G117](#g117)
19+
- [G118](#g118)
1920
- [G301, G302, G306, G307](#g301-g302-g306-g307)
2021

2122
## Rules List
@@ -38,7 +39,7 @@
3839
- G115 — Type conversion which leads to integer overflow (**SSA**)
3940
- G116 — Detect Trojan Source attacks using bidirectional Unicode characters (**AST**)
4041
- [G117](#g117) — Potential exposure of secrets via JSON/YAML/XML/TOML marshaling (**AST**)
41-
- G118 — Context propagation failure leading to goroutine/resource leaks (**SSA**)
42+
- [G118](#g118) — Context propagation failure leading to goroutine/resource leaks (**SSA**)
4243
- G119 — Unsafe redirect policy may propagate sensitive headers (**SSA**)
4344
- G120 — Unbounded form parsing in HTTP handlers can cause memory exhaustion (**SSA**)
4445
- G121 — Unsafe CrossOriginProtection bypass patterns (**SSA**)
@@ -170,6 +171,89 @@ This replaces the default pattern.
170171
}
171172
```
172173

174+
### G118
175+
176+
`G118` detects three classes of context-propagation failure using SSA-level analysis:
177+
178+
**1. Lost cancel function (CWE-400)**
179+
180+
Reports when a `context.WithCancel`, `context.WithTimeout`, or `context.WithDeadline` call
181+
returns a cancel function that is never called, potentially leaking resources.
182+
183+
```go
184+
// Flagged: cancel never called
185+
func work(ctx context.Context) {
186+
child, _ := context.WithTimeout(ctx, time.Second)
187+
_ = child
188+
}
189+
190+
// Safe: cancel deferred
191+
func work(ctx context.Context) {
192+
child, cancel := context.WithTimeout(ctx, time.Second)
193+
defer cancel()
194+
_ = child
195+
}
196+
```
197+
198+
The following patterns are all recognised as *safe* (cancel is considered called):
199+
200+
| Pattern | Description |
201+
|---|---|
202+
| `defer cancel()` | Direct deferred call |
203+
| `defer func() { cancel() }()` | Cancel in a deferred closure |
204+
| `cancelCopy := cancel; defer cancelCopy()` | Alias via variable |
205+
| `return ctx, cancel` | Cancel returned to caller (responsibility transferred) |
206+
| `s.cancelFn = cancel` + method `s.cancelFn()` | Stored in struct field, called via receiver method |
207+
| `s.cancel = cancel; defer s.cancel()` | Stored in struct field, deferred in same function |
208+
| `s.cancel = cancel; defer func() { s.cancel() }()` | Stored in struct field, called in closure |
209+
| Struct containing field is returned | Caller inherits cancel responsibility |
210+
211+
**2. Goroutine uses `context.Background`/`TODO` when request context is available (CWE-400)**
212+
213+
Reports when a goroutine spawned inside an HTTP handler or a function accepting a
214+
`context.Context` / `*http.Request` uses `context.Background()` or `context.TODO()`
215+
instead of the request-scoped context.
216+
217+
```go
218+
// Flagged
219+
func handler(w http.ResponseWriter, r *http.Request) {
220+
go func() {
221+
ctx := context.Background() // ignores request context
222+
doWork(ctx)
223+
}()
224+
}
225+
```
226+
227+
**3. Long-running loop without `ctx.Done()` guard (CWE-400)**
228+
229+
Reports an infinite loop that performs blocking I/O (e.g. `http.Get`, `db.Query`,
230+
`time.Sleep`, interface methods such as `Read`/`Write`) but never checks `ctx.Done()`,
231+
making the loop impossible to cancel.
232+
233+
```go
234+
// Flagged
235+
func poll(ctx context.Context) {
236+
for {
237+
http.Get("https://example.com") // blocks, no cancellation path
238+
time.Sleep(time.Second)
239+
}
240+
}
241+
242+
// Safe
243+
func poll(ctx context.Context) {
244+
for {
245+
select {
246+
case <-ctx.Done():
247+
return
248+
case <-time.After(time.Second):
249+
http.Get("https://example.com")
250+
}
251+
}
252+
}
253+
```
254+
255+
Loops with an external exit path (e.g. a `break` or bounded `for i < n`) are not flagged.
256+
173257
### G301, G302, G306, G307
174258

175259
File and directory permission rules can be configured with stricter maximum permissions:

analyzers/context_propagation.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,14 @@ func isCancelCalled(cancelValue ssa.Value, allFuncs []*ssa.Function) bool {
710710
if isStructFieldReturnedFromFunc(fa) {
711711
return true
712712
}
713+
// Check if any function (including closures capturing the
714+
// struct) loads and calls the same field. This handles
715+
// post-construction storage such as:
716+
// s.cancel = cancel; defer s.cancel()
717+
// s.cancel = cancel; defer func() { s.cancel() }()
718+
if isFieldCalledInAnyFunc(fa, allFuncs) {
719+
return true
720+
}
713721
}
714722
queue = append(queue, r.Addr)
715723
case *ssa.UnOp:
@@ -783,6 +791,39 @@ func isStructFieldReturnedFromFunc(fa *ssa.FieldAddr) bool {
783791
return false
784792
}
785793

794+
// isFieldCalledInAnyFunc checks whether a cancel function stored into a struct
795+
// field is subsequently called in any function (including closures) that
796+
// accesses the same field by struct pointer type and field index. This covers
797+
// post-construction storage patterns not handled by isCancelCalledViaStructField:
798+
//
799+
// s.cancel = cancel; defer s.cancel()
800+
// s.cancel = cancel; defer func() { s.cancel() }()
801+
func isFieldCalledInAnyFunc(fa *ssa.FieldAddr, allFuncs []*ssa.Function) bool {
802+
structPtrType := fa.X.Type()
803+
fieldIdx := fa.Field
804+
805+
for _, fn := range allFuncs {
806+
if fn == nil {
807+
continue
808+
}
809+
for _, block := range fn.Blocks {
810+
for _, instr := range block.Instrs {
811+
otherFA, ok := instr.(*ssa.FieldAddr)
812+
if !ok || otherFA.Field != fieldIdx {
813+
continue
814+
}
815+
if !types.Identical(otherFA.X.Type(), structPtrType) {
816+
continue
817+
}
818+
if isFieldValueCalled(otherFA) {
819+
return true
820+
}
821+
}
822+
}
823+
}
824+
return false
825+
}
826+
786827
// isCancelCalledViaStructField checks whether a cancel function stored into a
787828
// struct field (e.g., job.cancelFn = cancel) is subsequently called in any other
788829
// method of the same receiver type (e.g., job.Close() calls job.cancelFn()).

testutils/g118_samples.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ func multiPhiEdges(ctx context.Context, a, b, c bool) {
852852
}
853853
`}, 0, gosec.NewConfig()},
854854

855-
// Note: nested field access not tracked by current implementation
855+
// Safe: nested field cancel called via outer method on Inner type (fixed by isFieldCalledInAnyFunc)
856856
{[]string{`
857857
package main
858858
@@ -876,7 +876,7 @@ func (o *Outer) Teardown() {
876876
o.inner.cancel()
877877
}
878878
}
879-
`}, 1, gosec.NewConfig()},
879+
`}, 0, gosec.NewConfig()},
880880

881881
// Vulnerable: loop with interface method Do (tests analyzeBlockFeatures invoke)
882882
{[]string{`
@@ -1591,5 +1591,41 @@ func main() {
15911591
foo := NewFoo()
15921592
foo.Cancel()
15931593
}
1594+
`}, 0, gosec.NewConfig()},
1595+
1596+
// Safe: cancel stored in struct field post-construction, called via defer in same function (issue #1595)
1597+
{[]string{`
1598+
package main
1599+
1600+
import "context"
1601+
1602+
type State struct {
1603+
done context.CancelFunc
1604+
}
1605+
1606+
func manage(ctx context.Context) {
1607+
s := &State{}
1608+
_, s.done = context.WithCancel(ctx)
1609+
defer s.done()
1610+
}
1611+
`}, 0, gosec.NewConfig()},
1612+
1613+
// Safe: cancel stored in struct field post-construction, called via deferred closure (issue #1595)
1614+
{[]string{`
1615+
package main
1616+
1617+
import "context"
1618+
1619+
type Runner struct {
1620+
stop context.CancelFunc
1621+
}
1622+
1623+
func launch(ctx context.Context) {
1624+
r := &Runner{}
1625+
_, r.stop = context.WithCancel(ctx)
1626+
defer func() {
1627+
r.stop()
1628+
}()
1629+
}
15941630
`}, 0, gosec.NewConfig()},
15951631
}

0 commit comments

Comments
 (0)