// Copyright 2019 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. // syz-check does best-effort static correctness checking of the syscall descriptions in sys/os/*.txt. // Use: // // $ go install ./tools/syz-check // $ syz-check -obj-amd64 /linux_amd64/vmlinux -obj-arm64 /linux_arm64/vmlinux \ // -obj-386 /linux_386/vmlinux -obj-arm /linux_arm/vmlinux // // The vmlinux files should include debug info, enable all relevant configs (since we parse dwarf), // and be compiled with -gdwarf-3 -fno-eliminate-unused-debug-types -fno-eliminate-unused-debug-symbols flags. // -gdwarf-3 is required because version 4 changes the way bitfields are encoded and Go before 1.18 // does not support then new encoding and at least earlier versions mis-handle it, see: // https://go-review.googlesource.com/c/go/+/328709/comments/edf0619d_daec236f // // Use the following configs for kernels (x86_64 config for i386 as well): // upstream-apparmor-kasan.config, upstream-arm-full.config, upstream-arm64-full.config // // You may check only one arch as well (but then don't commit changes to warn files): // // $ syz-check -obj-amd64 /linux_amd64/vmlinux // // You may also disable dwarf or netlink checks with the corresponding flags. // E.g. -dwarf=0 greatly speeds up checking if you are only interested in netlink warnings // (but then again don't commit changes). // // The results are produced in sys/os/*.warn files. // On implementation level syz-check parses vmlinux dwarf, extracts struct descriptions // and compares them with what we have (size, fields, alignment, etc). Netlink checking extracts policy symbols // from the object files and parses them. package main import ( "bytes" "debug/dwarf" "debug/elf" "flag" "fmt" "os" "path/filepath" "runtime" "sort" "strings" "unsafe" "github.com/google/syzkaller/pkg/ast" "github.com/google/syzkaller/pkg/compiler" "github.com/google/syzkaller/pkg/osutil" "github.com/google/syzkaller/pkg/symbolizer" "github.com/google/syzkaller/pkg/tool" "github.com/google/syzkaller/prog" "github.com/google/syzkaller/sys/targets" ) func main() { var ( flagOS = flag.String("os", runtime.GOOS, "OS") flagDWARF = flag.Bool("dwarf", true, "do checking based on DWARF") flagNetlink = flag.Bool("netlink", true, "do checking of netlink policies") ) arches := make(map[string]*string) for arch := range targets.List[targets.Linux] { arches[arch] = flag.String("obj-"+arch, "", arch+" kernel object file") } defer tool.Init()() var warnings []Warn for arch, obj := range arches { if *obj == "" { delete(arches, arch) continue } warnings1, err := check(*flagOS, arch, *obj, *flagDWARF, *flagNetlink) if err != nil { tool.Fail(err) } warnings = append(warnings, warnings1...) runtime.GC() } if len(arches) == 0 { fmt.Fprintf(os.Stderr, "specify at least one -obj-arch flag\n") flag.PrintDefaults() os.Exit(1) } if err := writeWarnings(*flagOS, len(arches), warnings); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func check(OS, arch, obj string, dwarf, netlink bool) ([]Warn, error) { var warnings []Warn if obj == "" { return nil, fmt.Errorf("no object file in -obj-%v flag", arch) } structTypes, locs, warnings1, err := parseDescriptions(OS, arch) if err != nil { return nil, err } warnings = append(warnings, warnings1...) if dwarf { structs, err := parseKernelObject(obj) if err != nil { return nil, err } warnings2, err := checkImpl(structs, structTypes, locs) if err != nil { return nil, err } warnings = append(warnings, warnings2...) } if netlink { warnings3, err := checkNetlink(arch, obj, structTypes, locs) if err != nil { return nil, err } warnings = append(warnings, warnings3...) } for i := range warnings { warnings[i].arch = arch } return warnings, nil } const ( WarnCompiler = "compiler" WarnNoSuchStruct = "no-such-struct" WarnBadStructSize = "bad-struct-size" WarnBadFieldNumber = "bad-field-number" WarnBadFieldSize = "bad-field-size" WarnBadFieldOffset = "bad-field-offset" WarnBadBitfield = "bad-bitfield" WarnNoNetlinkPolicy = "no-such-netlink-policy" WarnNetlinkBadSize = "bad-kernel-netlink-policy-size" WarnNetlinkBadAttrType = "bad-netlink-attr-type" WarnNetlinkBadAttr = "bad-netlink-attr" ) type Warn struct { pos ast.Pos arch string typ string msg string } func writeWarnings(OS string, narches int, warnings []Warn) error { allFiles, err := filepath.Glob(filepath.Join("sys", OS, "*.warn")) if err != nil { return err } toRemove := make(map[string]bool) for _, file := range allFiles { toRemove[file] = true } byFile := make(map[string][]Warn) for _, warn := range warnings { byFile[warn.pos.File] = append(byFile[warn.pos.File], warn) } for file, warns := range byFile { sort.Slice(warns, func(i, j int) bool { w1, w2 := warns[i], warns[j] if w1.pos.Line != w2.pos.Line { return w1.pos.Line < w2.pos.Line } if w1.typ != w2.typ { return w1.typ < w2.typ } if w1.msg != w2.msg { return w1.msg < w2.msg } return w1.arch < w2.arch }) buf := new(bytes.Buffer) for i := 0; i < len(warns); i++ { warn := warns[i] arch := warn.arch arches := []string{warn.arch} for i < len(warns)-1 && warn.msg == warns[i+1].msg { if arch != warns[i+1].arch { arch = warns[i+1].arch arches = append(arches, arch) } i++ } archStr := "" // We do netlink checking only on amd64, so don't add arch. if len(arches) < narches && !strings.Contains(warn.typ, "netlink") { archStr = fmt.Sprintf(" [%v]", strings.Join(arches, ",")) } fmt.Fprintf(buf, "%v: %v%v\n", warn.typ, warn.msg, archStr) } warnFile := file + ".warn" if err := osutil.WriteFile(warnFile, buf.Bytes()); err != nil { return err } delete(toRemove, warnFile) } for file := range toRemove { os.Remove(file) } return nil } func checkImpl(structs map[string]*dwarf.StructType, structTypes []prog.Type, locs map[string]*ast.Struct) ([]Warn, error) { var warnings []Warn for _, typ := range structTypes { name := typ.TemplateName() astStruct := locs[name] if astStruct == nil { continue } if _, ok := isNetlinkPolicy(typ); ok { continue // netlink policies are not structs even if we describe them as structs } // In some cases we split a single struct into multiple ones // (more precise description), so try to match our foo$bar with kernel foo. kernelStruct := structs[name] if delim := strings.LastIndexByte(name, '$'); kernelStruct == nil && delim != -1 { kernelStruct = structs[name[:delim]] } warns, err := checkStruct(typ, astStruct, kernelStruct) if err != nil { return nil, err } warnings = append(warnings, warns...) } return warnings, nil } func checkStruct(typ prog.Type, astStruct *ast.Struct, str *dwarf.StructType) ([]Warn, error) { var warnings []Warn warn := func(pos ast.Pos, typ, msg string, args ...any) { warnings = append(warnings, Warn{pos: pos, typ: typ, msg: fmt.Sprintf(msg, args...)}) } name := typ.TemplateName() if str == nil { // Varlen structs are frequently not described in kernel (not possible in C). if !typ.Varlen() { warn(astStruct.Pos, WarnNoSuchStruct, "%v", name) } return warnings, nil } if !typ.Varlen() && typ.Size() != uint64(str.ByteSize) { warn(astStruct.Pos, WarnBadStructSize, "%v: syz=%v kernel=%v", name, typ.Size(), str.ByteSize) } // TODO: handle unions, currently we should report some false errors. if _, ok := typ.(*prog.UnionType); ok || str.Kind == "union" { return warnings, nil } // Ignore structs with out_overlay attribute. // They are never described in the kernel as a simple struct. // We could only match and check fields based on some common conventions, // but since we have very few of them it's unclear what are these conventions // and implementing something complex will have low RoI. if typ.(*prog.StructType).OverlayField != 0 { return warnings, nil } // TODO: we could also check enums (elements match corresponding flags in syzkaller). // TODO: we could also check values of literal constants (dwarf should have that, right?). // TODO: handle nested structs/unions, e.g.: // struct foo { // union { // ... // } bar; // }; // should be matched with: // foo_bar [ // ... // ] // TODO: consider making guesses about semantic types of fields, // e.g. if a name contains filedes/uid/pid/gid that may be the corresponding resource. ai := 0 offset := uint64(0) for _, field := range typ.(*prog.StructType).Fields { if field.Type.Varlen() { ai = len(str.Field) break } if prog.IsPad(field.Type) { offset += field.Type.Size() continue } if ai < len(str.Field) { fld := str.Field[ai] pos := astStruct.Fields[ai].Pos desc := fmt.Sprintf("%v.%v", name, field.Name) if field.Name != fld.Name { desc += "/" + fld.Name } if field.Type.UnitSize() != uint64(fld.Type.Size()) { warn(pos, WarnBadFieldSize, "%v: syz=%v kernel=%v", desc, field.Type.UnitSize(), fld.Type.Size()) } byteOffset := offset - field.Type.UnitOffset() if byteOffset != uint64(fld.ByteOffset) { warn(pos, WarnBadFieldOffset, "%v: syz=%v kernel=%v", desc, byteOffset, fld.ByteOffset) } // How would you define bitfield offset? // Offset of the beginning of the field from the beginning of the memory location, right? // No, DWARF defines it as offset of the end of the field from the end of the memory location. bitOffset := fld.Type.Size()*8 - fld.BitOffset - fld.BitSize if fld.BitSize == 0 { // And to make things even more interesting this calculation // does not work for normal variables. bitOffset = 0 } if field.Type.BitfieldLength() != uint64(fld.BitSize) || field.Type.BitfieldOffset() != uint64(bitOffset) { warn(pos, WarnBadBitfield, "%v: size/offset: syz=%v/%v kernel=%v/%v", desc, field.Type.BitfieldLength(), field.Type.BitfieldOffset(), fld.BitSize, bitOffset) } } ai++ offset += field.Size() } if ai != len(str.Field) { warn(astStruct.Pos, WarnBadFieldNumber, "%v: syz=%v kernel=%v", name, ai, len(str.Field)) } return warnings, nil } func parseDescriptions(OS, arch string) ([]prog.Type, map[string]*ast.Struct, []Warn, error) { errorBuf := new(bytes.Buffer) var warnings []Warn eh := func(pos ast.Pos, msg string) { warnings = append(warnings, Warn{pos: pos, typ: WarnCompiler, msg: msg}) fmt.Fprintf(errorBuf, "%v: %v\n", pos, msg) } top := ast.ParseGlob(filepath.Join("sys", OS, "*.txt"), eh) if top == nil { return nil, nil, nil, fmt.Errorf("failed to parse txt files:\n%s", errorBuf.Bytes()) } consts := compiler.DeserializeConstFile(filepath.Join("sys", OS, "*.const"), eh).Arch(arch) if consts == nil { return nil, nil, nil, fmt.Errorf("failed to parse const files:\n%s", errorBuf.Bytes()) } prg := compiler.Compile(top, consts, targets.Get(OS, arch), eh) if prg == nil { return nil, nil, nil, fmt.Errorf("failed to compile descriptions:\n%s", errorBuf.Bytes()) } prog.RestoreLinks(prg.Syscalls, prg.Resources, prg.Types) locs := make(map[string]*ast.Struct) for _, decl := range top.Nodes { switch n := decl.(type) { case *ast.Struct: locs[n.Name.Name] = n case *ast.TypeDef: if n.Struct != nil { locs[n.Name.Name] = n.Struct } } } var structs []prog.Type for _, typ := range prg.Types { switch typ.(type) { case *prog.StructType, *prog.UnionType: structs = append(structs, typ) } } return structs, locs, warnings, nil } // Overall idea of netlink checking. // Currnetly we check netlink policies for common detectable mistakes. // First, we detect what looks like a netlink policy in our descriptions // (these are structs/unions only with nlattr/nlnext/nlnetw fields). // Then we find corresponding symbols (offset/size) in vmlinux using nm. // Then we read elf headers and locate where these symbols are in the rodata section. // Then read in the symbol data, which is an array of nla_policy structs. // These structs allow to easily figure out type/size of attributes. // Finally we compare our descriptions with the kernel policy description. func checkNetlink(arch, obj string, structTypes []prog.Type, locs map[string]*ast.Struct) ([]Warn, error) { if arch != targets.AMD64 { // Netlink policies are arch-independent (?), // so no need to check all arches. // Also our definition of nlaPolicy below is 64-bit specific. return nil, nil } ef, err := elf.Open(obj) if err != nil { return nil, err } rodata := ef.Section(".rodata") if rodata == nil { return nil, fmt.Errorf("object file %v does not contain .rodata section", obj) } symbols, err := symbolizer.ReadRodataSymbols(obj) if err != nil { return nil, err } var warnings []Warn structMap := make(map[string]prog.Type) for _, typ := range structTypes { structMap[typ.Name()] = typ } checkedAttrs := make(map[string]*checkAttr) for _, typ := range structTypes { warnings1, err := checkNetlinkStruct(locs, symbols, rodata, structMap, checkedAttrs, typ) if err != nil { return nil, err } warnings = append(warnings, warnings1...) } warnings = append(warnings, checkMissingAttrs(checkedAttrs)...) return warnings, nil } func checkNetlinkStruct(locs map[string]*ast.Struct, symbols map[string][]symbolizer.Symbol, rodata *elf.Section, structMap map[string]prog.Type, checkedAttrs map[string]*checkAttr, typ prog.Type) ([]Warn, error) { name := typ.TemplateName() astStruct := locs[name] if astStruct == nil { return nil, nil } fields, ok := isNetlinkPolicy(typ) if !ok { return nil, nil } // In some cases we split a single policy into multiple ones (more precise description), // so try to match our foo$bar with kernel foo as well. kernelName, ss := name, symbols[name] if delim := strings.LastIndexByte(name, '$'); len(ss) == 0 && delim != -1 { kernelName = name[:delim] ss = symbols[kernelName] } if len(ss) == 0 { return []Warn{{pos: astStruct.Pos, typ: WarnNoNetlinkPolicy, msg: name}}, nil } var warnings []Warn var warnings1 *[]Warn var policy1 []nlaPolicy var attrs1 map[int]bool // We may have several symbols with the same name (they frequently have internal linking), // in such case we choose the one that produces fewer warnings. for _, symb := range ss { if symb.Size == 0 || symb.Size%int(unsafe.Sizeof(nlaPolicy{})) != 0 { warnings = append(warnings, Warn{pos: astStruct.Pos, typ: WarnNetlinkBadSize, msg: fmt.Sprintf("%v (%v), size %v", kernelName, name, ss[0].Size)}) continue } binary := make([]byte, symb.Size) addr := symb.Addr - rodata.Addr if _, err := rodata.ReadAt(binary, int64(addr)); err != nil { return nil, fmt.Errorf("failed to read policy %v (%v) at %v: %w", kernelName, name, symb.Addr, err) } policy := (*[1e6]nlaPolicy)(unsafe.Pointer(&binary[0]))[:symb.Size/int(unsafe.Sizeof(nlaPolicy{}))] warnings2, attrs2, err := checkNetlinkPolicy(structMap, typ, fields, astStruct, policy) if err != nil { return nil, err } if warnings1 == nil || len(*warnings1) > len(warnings2) { warnings1 = &warnings2 policy1 = policy attrs1 = attrs2 } } if warnings1 != nil { warnings = append(warnings, *warnings1...) ca := checkedAttrs[kernelName] if ca == nil { ca = &checkAttr{ pos: astStruct.Pos, name: name, policy: policy1, attrs: make(map[int]bool), } checkedAttrs[kernelName] = ca } for attr := range attrs1 { ca.attrs[attr] = true } } return warnings, nil } type checkAttr struct { pos ast.Pos name string policy []nlaPolicy attrs map[int]bool } func checkMissingAttrs(checkedAttrs map[string]*checkAttr) []Warn { // Missing attribute checking is a bit tricky because we may split a single // kernel policy into several policies for better precision. // They have different names, but map to the same kernel policy. // We want to report a missing attribute iff it's missing in all copies of the policy. var warnings []Warn for _, ca := range checkedAttrs { var missing []int for i, pol := range ca.policy { // Ignore attributes that are not described in the policy // (some of them are unused at all, however there are cases where // they are not described but used as inputs, and these are actually // the worst ones). if !ca.attrs[i] && (pol.typ != NLA_UNSPEC && pol.typ != NLA_REJECT || pol.len != 0) { missing = append(missing, i) } } // If we miss too many, there is probably something else going on. if len(missing) != 0 && len(missing) <= 5 { warnings = append(warnings, Warn{ pos: ca.pos, typ: WarnNetlinkBadAttr, msg: fmt.Sprintf("%v: missing attributes: %v", ca.name, missing), }) } } return warnings } func isNetlinkPolicy(typ prog.Type) ([]prog.Field, bool) { var fields []prog.Field switch t := typ.(type) { case *prog.StructType: fields = t.Fields case *prog.UnionType: fields = t.Fields default: return nil, false } haveAttr := false for _, fld := range fields { field := fld.Type if prog.IsPad(field) { continue } if isNlattr(field) { haveAttr = true continue } if arr, ok := field.(*prog.ArrayType); ok { field = arr.Elem } if _, ok := isNetlinkPolicy(field); ok { continue } return nil, false } return fields, haveAttr } const ( nlattrT = "nlattr_t" nlattrTT = "nlattr_tt" ) func isNlattr(typ prog.Type) bool { name := typ.TemplateName() return name == nlattrT || name == nlattrTT } func checkNetlinkPolicy(structMap map[string]prog.Type, typ prog.Type, fields []prog.Field, astStruct *ast.Struct, policy []nlaPolicy) ([]Warn, map[int]bool, error) { var warnings []Warn warn := func(pos ast.Pos, typ, msg string, args ...any) { warnings = append(warnings, Warn{pos: pos, typ: typ, msg: fmt.Sprintf(msg, args...)}) } checked := make(map[int]bool) ai := 0 for _, field := range fields { if prog.IsPad(field.Type) { continue } fld := astStruct.Fields[ai] ai++ if !isNlattr(field.Type) { continue } ft := field.Type.(*prog.StructType) attr := int(ft.Fields[1].Type.(*prog.ConstType).Val) if attr >= len(policy) { warn(fld.Pos, WarnNetlinkBadAttrType, "%v.%v: type %v, kernel policy size %v", typ.TemplateName(), field.Name, attr, len(policy)) continue } if checked[attr] { warn(fld.Pos, WarnNetlinkBadAttr, "%v.%v: duplicate attribute", typ.TemplateName(), field.Name) } checked[attr] = true w := checkNetlinkAttr(ft, policy[attr]) if w != "" { warn(fld.Pos, WarnNetlinkBadAttr, "%v.%v: %v", typ.TemplateName(), field.Name, w) } } return warnings, checked, nil } func checkNetlinkAttr(typ *prog.StructType, policy nlaPolicy) string { payload := typ.Fields[2].Type if typ.TemplateName() == nlattrTT { payload = typ.Fields[4].Type } if warn := checkAttrType(typ, payload, policy); warn != "" { return warn } size, minSize, maxSize := attrSize(policy) payloadSize := minTypeSize(payload) if size != -1 && size != payloadSize { return fmt.Sprintf("bad size %v, expect %v", payloadSize, size) } if minSize != -1 && minSize > payloadSize { return fmt.Sprintf("bad size %v, expect min %v", payloadSize, minSize) } if maxSize != -1 && maxSize < payloadSize { return fmt.Sprintf("bad size %v, expect max %v", payloadSize, maxSize) } valMin, valMax, haveVal := typeMinMaxValue(payload) if haveVal { if policy.validation == NLA_VALIDATE_RANGE || policy.validation == NLA_VALIDATE_MIN { if int64(valMin) < int64(policy.minVal) { // This is a common case that occurs several times: limit on min value of 1. // Not worth fixing (at least not in initial batch), it just crosses out a // single value of 0, which we shuold test anyway. if policy.validation != NLA_VALIDATE_MIN || policy.minVal != 1 { return fmt.Sprintf("bad min value %v, expect %v", int64(valMin), policy.minVal) } } } if policy.validation == NLA_VALIDATE_RANGE || policy.validation == NLA_VALIDATE_MAX { if int64(valMax) > int64(policy.maxVal) { return fmt.Sprintf("bad max value %v, expect %v", int64(valMax), policy.maxVal) } } } return "" } func minTypeSize(t prog.Type) int { if !t.Varlen() { return int(t.Size()) } switch typ := t.(type) { case *prog.StructType: if typ.OverlayField != 0 { // Overlayed structs are not supported here // (and should not be used in netlink). // Make this always produce a warning. return -1 } // Some struct args has trailing arrays, but are only checked for min size. // Try to get some estimation for min size of this struct. size := 0 for _, field := range typ.Fields { if !field.Varlen() { size += int(field.Size()) } } return size case *prog.ArrayType: if typ.Kind == prog.ArrayRangeLen && !typ.Elem.Varlen() { return int(typ.RangeBegin * typ.Elem.Size()) } case *prog.UnionType: size := 0 for _, field := range typ.Fields { if size1 := minTypeSize(field.Type); size1 != -1 && size > size1 || size == 0 { size = size1 } } return size } return -1 } func checkAttrType(typ *prog.StructType, payload prog.Type, policy nlaPolicy) string { switch policy.typ { case NLA_STRING, NLA_NUL_STRING: if _, ok := payload.(*prog.BufferType); !ok { return "expect string" } case NLA_NESTED: if typ.TemplateName() != nlattrTT || typ.Fields[3].Type.(*prog.ConstType).Val != 1 { return "should be nlnest" } case NLA_BITFIELD32: if typ.TemplateName() != nlattrT || payload.TemplateName() != "nla_bitfield32" { return "should be nlattr[nla_bitfield32]" } case NLA_NESTED_ARRAY: if _, ok := payload.(*prog.ArrayType); !ok { return "expect array" } case NLA_REJECT: return "NLA_REJECT attribute will always be rejected" } return "" } func attrSize(policy nlaPolicy) (int, int, int) { switch policy.typ { case NLA_UNSPEC: if policy.len != 0 { return -1, int(policy.len), -1 } case NLA_MIN_LEN: return -1, int(policy.len), -1 case NLA_EXACT_LEN, NLA_EXACT_LEN_WARN: return int(policy.len), -1, -1 case NLA_U8, NLA_S8: return 1, -1, -1 case NLA_U16, NLA_S16: return 2, -1, -1 case NLA_U32, NLA_S32: return 4, -1, -1 case NLA_U64, NLA_S64, NLA_MSECS: return 8, -1, -1 case NLA_FLAG: return 0, -1, -1 case NLA_BINARY: if policy.len != 0 { return -1, -1, int(policy.len) } } return -1, -1, -1 } func typeMinMaxValue(payload prog.Type) (minVal, maxVal uint64, ok bool) { switch typ := payload.(type) { case *prog.ConstType: return typ.Val, typ.Val, true case *prog.IntType: if typ.Kind == prog.IntRange { return typ.RangeBegin, typ.RangeEnd, true } return 0, ^uint64(0), true case *prog.FlagsType: minVal, maxVal := ^uint64(0), uint64(0) for _, v := range typ.Vals { minVal = min(minVal, v) maxVal = max(maxVal, v) } return minVal, maxVal, true } return 0, 0, false } type nlaPolicy struct { typ uint8 validation uint8 len uint16 _ uint32 minVal int16 maxVal int16 _ int32 } // nolint:staticcheck const ( NLA_UNSPEC = iota NLA_U8 NLA_U16 NLA_U32 NLA_U64 NLA_STRING NLA_FLAG NLA_MSECS NLA_NESTED NLA_NESTED_ARRAY NLA_NUL_STRING NLA_BINARY NLA_S8 NLA_S16 NLA_S32 NLA_S64 NLA_BITFIELD32 NLA_REJECT NLA_EXACT_LEN NLA_EXACT_LEN_WARN NLA_MIN_LEN ) // nolint:staticcheck const ( _ = iota NLA_VALIDATE_RANGE NLA_VALIDATE_MIN NLA_VALIDATE_MAX )