Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tools/customlint/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func (f *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
cleanupAnalyzer,
caseBodyAnalyzer,
forbidParentAccessAnalyzer,
protectedFieldAnalyzer,
shadowAnalyzer,
typedPathsAnalyzer,
unexportedAPIAnalyzer,
Expand Down
221 changes: 221 additions & 0 deletions tools/customlint/protectedfield.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package customlint

import (
"go/ast"
"go/token"
"go/types"
"reflect"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)

// protectedFieldAnalyzer guards fields tagged `customlint:"protected"`, such as
// the kind headers that unsafe casts trust to identify the enclosing allocation.
// Writing such a field, or copying a struct that contains one, can make a value
// claim to be a larger record than its storage holds. Intentional writes are
// suppressed with //nolint:customlint.
var protectedFieldAnalyzer = &analysis.Analyzer{
Name: "protectedfield",
Doc: "forbids writes to protected fields and by-value copies of structs containing them",
Requires: []*analysis.Analyzer{
inspect.Analyzer,
},
Run: func(pass *analysis.Pass) (any, error) {
return (&protectedFieldPass{pass: pass, contains: make(map[types.Type]bool)}).run()
},
}

type protectedFieldPass struct {
pass *analysis.Pass
contains map[types.Type]bool
}

func (p *protectedFieldPass) run() (any, error) {
in := p.pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

for c := range in.Root().Preorder(
(*ast.AssignStmt)(nil),
(*ast.IncDecStmt)(nil),
(*ast.RangeStmt)(nil),
(*ast.UnaryExpr)(nil),
(*ast.CompositeLit)(nil),
(*ast.FuncType)(nil),
(*ast.FuncDecl)(nil),
) {
switch node := c.Node().(type) {
case *ast.AssignStmt:
for _, lhs := range node.Lhs {
p.checkWrite(lhs, "write to")
}
Comment thread
jakebailey marked this conversation as resolved.
case *ast.IncDecStmt:
p.checkWrite(node.X, "write to")
case *ast.RangeStmt:
if node.Key != nil {
p.checkWrite(node.Key, "write to")
}
if node.Value != nil {
p.checkWrite(node.Value, "write to")
}
case *ast.UnaryExpr:
if node.Op == token.AND {
p.checkWrite(node.X, "address of")
}
case *ast.CompositeLit:
p.checkLiteral(node)
case *ast.FuncType:
p.checkFields(node.Params, "parameter")
p.checkFields(node.Results, "result")
case *ast.FuncDecl:
p.checkFields(node.Recv, "receiver")
}
}

for id, obj := range p.pass.TypesInfo.Defs {
if v, ok := obj.(*types.Var); ok && (v.Kind() == types.PackageVar || v.Kind() == types.LocalVar) && p.containsProtected(v.Type()) {
p.pass.Reportf(id.Pos(), "variable %s holds %s by value, which contains a protected field", id.Name, v.Type())
}
}

for c := range in.Root().Preorder((*ast.StarExpr)(nil), (*ast.IndexExpr)(nil), (*ast.CallExpr)(nil), (*ast.CompositeLit)(nil), (*ast.SelectorExpr)(nil)) {
expr := c.Node().(ast.Expr)
tv, ok := p.pass.TypesInfo.Types[expr]
Comment on lines +82 to +83

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this out of scope. The lint guards against kinds that disagree with their storage. An element of a []Header is already a standalone header-sized value, and the only ways to give it a wrong kind are a write or a by-value copy out of a larger record, both of which are already flagged. Copying between such slices cannot create a new bad header.

if !ok || !tv.IsValue() || !p.containsProtected(tv.Type) || p.allowedValue(c) {
continue
}
p.pass.Reportf(expr.Pos(), "by-value copy of %s, which contains a protected field", tv.Type)
}

return nil, nil
}

func (p *protectedFieldPass) checkWrite(expr ast.Expr, what string) {
sel, ok := ast.Unparen(expr).(*ast.SelectorExpr)
if !ok {
return
}
selection := p.pass.TypesInfo.Selections[sel]
if selection == nil || selection.Kind() != types.FieldVal {
return
}
typ := selection.Recv()
var tag string
for _, index := range selection.Index() {
if ptr, ok := typ.Underlying().(*types.Pointer); ok {
typ = ptr.Elem()
}
st := typ.Underlying().(*types.Struct)
tag = st.Tag(index)
typ = st.Field(index).Type()
}
if isProtectedTag(tag) {
p.pass.Reportf(sel.Sel.Pos(), "%s protected field %s", what, sel.Sel.Name)
}
}

func (p *protectedFieldPass) checkLiteral(lit *ast.CompositeLit) {
typ := p.pass.TypesInfo.TypeOf(lit)
// Elided literals in []*T or map[K]*T composites have type *T.
if ptr, ok := typ.Underlying().(*types.Pointer); ok {
typ = ptr.Elem()
}
st, ok := typ.Underlying().(*types.Struct)
if !ok {
return
}
for i, elt := range lit.Elts {
index := i
if kv, ok := elt.(*ast.KeyValueExpr); ok {
key, ok := kv.Key.(*ast.Ident)
if !ok {
continue
}
index = -1
for j := range st.NumFields() {
if st.Field(j).Name() == key.Name {
index = j
break
}
}
if index < 0 {
continue
}
}
if index < st.NumFields() && isProtectedTag(st.Tag(index)) {
p.pass.Reportf(elt.Pos(), "write to protected field %s", st.Field(index).Name())
}
}
}

func (p *protectedFieldPass) checkFields(fields *ast.FieldList, what string) {
if fields == nil {
return
}
for _, field := range fields.List {
if typ := p.pass.TypesInfo.TypeOf(field.Type); typ != nil && p.containsProtected(typ) {
p.pass.Reportf(field.Type.Pos(), "%s holds %s by value, which contains a protected field", what, typ)
}
}
}

// allowedValue reports whether a record-valued expression is used without
// copying it: selecting a field or method, taking its address, indexing it, or
// as an operand of unsafe.Sizeof, Alignof, or Offsetof.
func (p *protectedFieldPass) allowedValue(c inspector.Cursor) bool {
child := c.Node()
for parent := c.Parent(); ; parent = parent.Parent() {
switch node := parent.Node().(type) {
case *ast.ParenExpr:
child = node
continue
case *ast.SelectorExpr:
return node.X == child
case *ast.UnaryExpr:
return node.Op == token.AND
case *ast.IndexExpr:
return node.X == child
case *ast.SliceExpr:
return node.X == child
}
for call := range parent.Enclosing((*ast.CallExpr)(nil)) {
if p.isUnsafeLayoutCall(call.Node().(*ast.CallExpr)) {
return true
}
}
return false
}
}

func (p *protectedFieldPass) isUnsafeLayoutCall(call *ast.CallExpr) bool {
sel, ok := ast.Unparen(call.Fun).(*ast.SelectorExpr)
if !ok {
return false
}
builtin, ok := p.pass.TypesInfo.Uses[sel.Sel].(*types.Builtin)
return ok && (builtin.Name() == "Sizeof" || builtin.Name() == "Alignof" || builtin.Name() == "Offsetof")
}

func (p *protectedFieldPass) containsProtected(typ types.Type) bool {
if result, ok := p.contains[typ]; ok {
return result
}
result := false
switch t := typ.Underlying().(type) {
case *types.Struct:
for i := range t.NumFields() {
if isProtectedTag(t.Tag(i)) || p.containsProtected(t.Field(i).Type()) {
result = true
break
}
}
case *types.Array:
result = p.containsProtected(t.Elem())
}
p.contains[typ] = result
return result
}

func isProtectedTag(tag string) bool {
return reflect.StructTag(tag).Get("customlint") == "protected"
}
65 changes: 65 additions & 0 deletions tools/customlint/testdata/protectedfield/consumer/consumer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package consumer

import "testdata/protectedfield/record"

type Container struct {
header *record.Header
small record.Small
}

func writes(h *record.Header, s *record.Small, d *record.Defined) {
h.Kind = record.KindLarge
s.Kind = record.KindLarge
s.Header.Kind++
d.Kind = record.KindLarge
h.Kind, h.Flags = record.KindLarge, 1
_ = &h.Kind
h.Flags = 1
_ = &h.Flags
_ = h.Kind
for _, h.Kind = range []record.Kind{record.KindLarge} {
}
for h.Kind = range record.Kind(2) {
}
var kinds []record.Kind
for _, kind := range kinds {
_ = kind
}
}

func literals() {
_ = &record.Header{Kind: record.KindLarge}
_ = &record.Header{record.KindLarge, 1}
_ = &record.Header{Flags: 1}
_ = &record.Small{}
_ = &record.Alias{Kind: record.KindSmall}
_ = []*record.Header{{Kind: record.KindLarge}}
_ = []*record.Header{{record.KindLarge, 1}}
_ = map[int]*record.Header{0: {Kind: record.KindLarge}}
_ = []*record.Header{{Flags: 1}}
}

func copies(h *record.Header, s *record.Small, hs []record.Header) {
*h = *record.NewSmall(1)
small := *s
var local record.Header
_ = record.Small{}
_ = hs[0]
_ = &hs[0]
_ = hs[0].Flags
_ = (*h).Flags
_ = &(*h)
_ = s.Header
_ = &s.Header
for _, v := range hs {
_ = v
}
_, _ = small, local
_ = h.AsLarge()
}

func param(h record.Header) {}

func result() (h *record.Header) { return nil }

func valueResult() record.Small { return record.Small{} }
Loading
Loading