aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/declextract
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2024-12-02 10:57:36 +0100
committerDmitry Vyukov <dvyukov@google.com>2024-12-11 15:22:17 +0000
commitb2c5a234aeb69e981c6e7ad120b49d37a86c6cae (patch)
treed2e575e4c5dd3f044d43a71231c50c1b1459e35a /pkg/declextract
parentbfb4b3275371a3b53cd6562fa06e5a9dfb5627b7 (diff)
tools/syz-declextract: rewrite
syz-declextract accumulated a bunch of code health problems so that now it's hard to change/extend it, lots of new features can only be added in in hacky ways and cause lots of code duplication. It's also completly untested. Rewrite the tool to: - move as much code as possible to Go (working with the clang tool is painful for a number of reasons) - allow testing and add unit tests (first layer of tests test what information is produced by the clang tool, second layer of tests test how that information is transformed to descriptions) - allow extending the clang tool output to export arbitrary info in non-hacky way (now it produces arbitrary JSON instead of a mix of incomplete descriptions and interfaces) - remove code duplication in the clang tool and provide common infrastructure to add new analysis w/o causing more duplication - provide more convinient primitives in the clang tool - improve code style consistency and stick to the LLVM code style (in particular, variable names must start with a capital letter, single-statement blocks are not surrounded with {}) - remove intermixing of code that works on different levels (currently we have AST analysis + busness logic + printfs all intermixed with each other) - provide several helper Go packages for better code structuring (e.g. pkg/clangtool just runs the tool on source files in parallel and returns results, this already separates a bunch of low-level logic from the rest of the code under a simple abstraction) I've tried to make the output match the current output as much as possible so that the diff is managable (in some cases at the cost of code quality, this should be fixed in future commits). There are still some differences, but hopefully they are managable for review (more includes/defines, reordered some netlink attributes). Fixed minor bugs are fixed along the way, but mostly NFC: 1. Some unions were incorrectly emitted as [varlen] (C unions are never varlen). 2. Only a of [packed], [align[N]] attributes was emitted for struct (both couldn't be emitted).
Diffstat (limited to 'pkg/declextract')
-rw-r--r--pkg/declextract/declextract.go394
-rw-r--r--pkg/declextract/entity.go175
-rw-r--r--pkg/declextract/interface.go46
-rw-r--r--pkg/declextract/netlink.go236
-rw-r--r--pkg/declextract/serialization.go113
5 files changed, 964 insertions, 0 deletions
diff --git a/pkg/declextract/declextract.go b/pkg/declextract/declextract.go
new file mode 100644
index 000000000..dd54bf79c
--- /dev/null
+++ b/pkg/declextract/declextract.go
@@ -0,0 +1,394 @@
+// Copyright 2024 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.
+
+package declextract
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "slices"
+ "strings"
+)
+
+func Run(out *Output, syscallRename map[string][]string) ([]byte, []*Interface, error) {
+ ctx := &context{
+ Output: out,
+ syscallRename: syscallRename,
+ structs: make(map[string]*Struct),
+ }
+ ctx.processIncludes()
+ ctx.processEnums()
+ ctx.processStructs()
+ ctx.processSyscalls()
+ ctx.processIouring()
+ ctx.fabricateNetlinkPolicies()
+
+ ctx.serialize()
+ ctx.finishInterfaces()
+ return ctx.descriptions.Bytes(), ctx.interfaces, errors.Join(ctx.errs...)
+}
+
+type context struct {
+ *Output
+ syscallRename map[string][]string // syscall function -> syscall names
+ structs map[string]*Struct
+ interfaces []*Interface
+ descriptions *bytes.Buffer
+ errs []error
+}
+
+func (ctx *context) error(msg string, args ...any) {
+ ctx.errs = append(ctx.errs, fmt.Errorf(msg, args...))
+}
+
+func (ctx *context) processIncludes() {
+ // These additional includes must be at the top, because other kernel headers
+ // are broken and won't compile without these additional ones included first.
+ ctx.Includes = append([]string{
+ "vdso/bits.h",
+ "linux/types.h",
+ "net/netlink.h",
+ }, ctx.Includes...)
+ replaces := map[string]string{
+ // Arches may use some includes from asm-generic and some from arch/arm.
+ // If the arch used for extract used asm-generic for a header,
+ // other arches may need arch/asm version of the header. So switch to
+ // a more generic file name that should resolve correctly for all arches.
+ "include/uapi/asm-generic/ioctls.h": "asm/ioctls.h",
+ "include/uapi/asm-generic/sockios.h": "asm/sockios.h",
+ }
+ for i, inc := range ctx.Includes {
+ if replace := replaces[inc]; replace != "" {
+ ctx.Includes[i] = replace
+ }
+ }
+}
+
+func (ctx *context) processEnums() {
+ for _, enum := range ctx.Enums {
+ // TODO: change for consistency:
+ // enum.Name += autoSuffix
+ enum.Name = "auto_" + enum.Name
+ }
+}
+
+func (ctx *context) processSyscalls() {
+ var syscalls []*Syscall
+ for _, call := range ctx.Syscalls {
+ ctx.processFields(call.Args, "", false)
+ fn := strings.TrimPrefix(call.Func, "__do_sys_")
+ for _, name := range ctx.syscallRename[fn] {
+ ctx.noteInterface(&Interface{
+ Type: IfaceSyscall,
+ Name: name,
+ IdentifyingConst: "__NR_" + name,
+ Files: []string{call.SourceFile},
+ Func: call.Func,
+ AutoDescriptions: true,
+ })
+ newCall := *call
+ newCall.Func = name + autoSuffix
+ syscalls = append(syscalls, &newCall)
+ }
+ }
+ ctx.Syscalls = sortAndDedupSlice(syscalls)
+}
+
+func (ctx *context) processIouring() {
+ for _, op := range ctx.IouringOps {
+ ctx.noteInterface(&Interface{
+ Type: IfaceIouring,
+ Name: op.Name,
+ IdentifyingConst: op.Name,
+ Files: []string{op.SourceFile},
+ Func: op.Func,
+ Access: AccessUser,
+ })
+ }
+}
+
+func (ctx *context) processStructs() {
+ for _, str := range ctx.Structs {
+ // TODO: change for consistency:
+ // str.Name += autoSuffix
+ str.Name += "$auto_record"
+ ctx.structs[str.Name] = str
+ }
+ ctx.Structs = slices.DeleteFunc(ctx.Structs, func(str *Struct) bool {
+ return str.ByteSize == 0 // Empty structs are not supported.
+ })
+ for _, str := range ctx.Structs {
+ ctx.processFields(str.Fields, str.Name, true)
+ }
+}
+
+func (ctx *context) processFields(fields []*Field, parent string, needBase bool) {
+ counts := make([]*Field, len(fields))
+ for _, f := range fields {
+ f.Name = fixIdentifier(f.Name)
+ if f.CountedBy != -1 {
+ counts[f.CountedBy] = f
+ }
+ }
+ for i, f := range fields {
+ f.syzType = ctx.fieldType(f, counts[i], parent, needBase)
+ }
+}
+
+func (ctx *context) fieldType(f, counts *Field, parent string, needBase bool) string {
+ if f.BitWidth != 0 && !needBase {
+ ctx.error("syscall arg %v is a bitfield", f.Name)
+ }
+ if f.BitWidth != 0 && f.Type.Int == nil {
+ ctx.error("non-int field %v is a bitfield", f.Name)
+ }
+ if counts != nil && f.Type.Int == nil && f.Type.Ptr == nil {
+ ctx.error("non-int/ptr field %v counts field %v", f.Name, counts.Name)
+ }
+ f.Name = strings.ToLower(f.Name)
+ switch {
+ case f.Type.Int != nil:
+ return ctx.fieldTypeInt(f, counts, needBase)
+ case f.Type.Ptr != nil:
+ return ctx.fieldTypePtr(f, counts, parent)
+ case f.Type.Array != nil:
+ return ctx.fieldTypeArray(f, parent)
+ case f.Type.Buffer != nil:
+ return ctx.fieldTypeBuffer(f)
+ case f.Type.Struct != "":
+ return ctx.fieldTypeStruct(f)
+ }
+ ctx.error("field %v does not have type", f.Name)
+ return ""
+}
+
+func (ctx *context) fieldTypeInt(f, counts *Field, needBase bool) string {
+ t := f.Type.Int
+ switch t.ByteSize {
+ case 1, 2, 4, 8:
+ default:
+ ctx.error("field %v has unsupported size %v", f.Name, t.ByteSize)
+ }
+ if t.Enum != "" && counts != nil {
+ ctx.error("field %v is both enum %v and counts field %v", f.Name, t.Enum, counts.Name)
+ }
+ baseType := fmt.Sprintf("int%v", t.ByteSize*8)
+ // Note: we make all 8-byte syscall arguments intptr b/c for 64-bit arches it does not matter,
+ // but for 32-bit arches int64 as syscall argument won't work. IIUC the ABI is that these
+ // are split into 2 32-bit arguments.
+ intptr := t.ByteSize == 8 && (!needBase || strings.Contains(t.Base, "long") &&
+ !strings.Contains(t.Base, "long long"))
+ if intptr {
+ baseType = "intptr"
+ }
+ if t.isBigEndian && t.ByteSize != 1 {
+ baseType += "be"
+ }
+ if f.BitWidth == t.ByteSize*8 {
+ f.BitWidth = 0
+ }
+ if f.BitWidth != 0 {
+ baseType += fmt.Sprintf(":%v", f.BitWidth)
+ }
+ unusedType := fmt.Sprintf("const[0 %v]", maybeBaseType(baseType, needBase))
+ if f.IsAnonymous {
+ return unusedType
+ }
+ if t.Enum != "" {
+ // TODO: change for consistency:
+ // t.Enum += autoSuffix
+ t.Enum = "auto_" + t.Enum
+ return fmt.Sprintf("flags[%v %v]", t.Enum, maybeBaseType(baseType, needBase))
+ }
+ if counts != nil {
+ return fmt.Sprintf("len[%v %v]", counts.Name, maybeBaseType(baseType, needBase))
+ }
+ if t.Name == "TODO" {
+ return todoType
+ }
+ special := ""
+ switch t.ByteSize {
+ case 2:
+ special = ctx.specialInt2(f.Name, t.Name, needBase)
+ case 4:
+ special = ctx.specialInt4(f.Name, t.Name, needBase)
+ case 8:
+ if intptr {
+ special = ctx.specialIntptr(f.Name, t.Name, needBase)
+ }
+ }
+ if special != "" {
+ if f.BitWidth != 0 {
+ // We don't have syntax to express this.
+ ctx.error("field %v is both special %v and a bitfield", f.Name, special)
+ }
+ return special
+ }
+ if strings.HasSuffix(f.Name, "enabled") || strings.HasSuffix(f.Name, "enable") {
+ return "bool" + strings.TrimPrefix(baseType, "int")
+ }
+ if strings.Contains(f.Name, "pad") || strings.Contains(f.Name, "unused") ||
+ strings.Contains(f.Name, "_reserved") {
+ return unusedType
+ }
+ return baseType
+}
+
+func (ctx *context) specialInt2(field, typ string, needBase bool) string {
+ switch {
+ case strings.Contains(field, "port"):
+ return "sock_port"
+ }
+ return ""
+}
+
+func (ctx *context) specialInt4(field, typ string, needBase bool) string {
+ switch {
+ case strings.Contains(field, "ipv4"):
+ return "ipv4_addr"
+ case strings.HasSuffix(field, "_pid") || strings.HasSuffix(field, "_tid") ||
+ strings.HasSuffix(field, "_pgid") || strings.HasSuffix(field, "_tgid") ||
+ field == "pid" || field == "tid" || field == "pgid" || field == "tgid":
+ return "pid"
+ case strings.HasSuffix(field, "dfd") && !strings.HasSuffix(field, "oldfd") && !strings.HasSuffix(field, "pidfd"):
+ return "fd_dir"
+ case strings.HasSuffix(field, "ns_fd"):
+ return "fd_namespace"
+ case strings.HasSuffix(field, "_uid") || field == "uid" || field == "user" ||
+ field == "ruid" || field == "euid" || field == "suid":
+ return "uid"
+ case strings.HasSuffix(field, "_gid") || field == "gid" || field == "group" ||
+ field == "rgid" || field == "egid" || field == "sgid":
+ return "gid"
+ case strings.HasSuffix(field, "fd") || strings.HasPrefix(field, "fd_") ||
+ strings.Contains(field, "fildes") || field == "fdin" || field == "fdout":
+ return "fd"
+ case strings.Contains(field, "ifindex") || strings.Contains(field, "dev_index"):
+ return "ifindex"
+ }
+ return ""
+}
+
+func (ctx *context) specialIntptr(field, typ string, needBase bool) string {
+ switch {
+ case field == "sigsetsize":
+ return fmt.Sprintf("const[8 %v]", maybeBaseType("intptr", needBase))
+ }
+ return ""
+}
+
+func (ctx *context) fieldTypePtr(f, counts *Field, parent string) string {
+ t := f.Type.Ptr
+ dir := "inout"
+ if t.IsConst {
+ dir = "in"
+ }
+ opt := ""
+ // Use an opt pointer if the direct parent is the same as this node, or if the field name is next.
+ // Looking at the field name is a hack, but it's enough to avoid some recursion cases,
+ // e.g. for struct adf_user_cfg_section.
+ if f.Name == "next" || parent != "" && parent == t.Elem.Struct+"$auto_record" {
+ opt = ", opt"
+ }
+ elem := &Field{
+ Name: f.Name,
+ Type: t.Elem,
+ }
+ return fmt.Sprintf("ptr[%v, %v %v]", dir, ctx.fieldType(elem, counts, parent, true), opt)
+}
+
+func (ctx *context) fieldTypeArray(f *Field, parent string) string {
+ t := f.Type.Array
+ elem := &Field{
+ Name: f.Name,
+ Type: t.Elem,
+ }
+ elemType := ctx.fieldType(elem, nil, parent, true)
+ if t.MinSize == 1 && t.MaxSize == 1 {
+ return elemType
+ }
+ bounds := ctx.bounds(f.Name, t.MinSize, t.MaxSize)
+ return fmt.Sprintf("array[%v%v]", elemType, bounds)
+}
+
+func (ctx *context) fieldTypeBuffer(f *Field) string {
+ t := f.Type.Buffer
+ bounds := ctx.bounds(f.Name, t.MinSize, t.MaxSize)
+ baseType := "string"
+ if t.IsNonTerminated {
+ baseType = "stringnoz"
+ }
+ switch {
+ case !t.IsString:
+ return fmt.Sprintf("array[int8 %v]", bounds)
+ case strings.Contains(f.Name, "ifname") || strings.HasSuffix(f.Name, "dev_name"):
+ return "devname"
+ case strings.Contains(f.Name, "filename") || strings.Contains(f.Name, "pathname") ||
+ strings.Contains(f.Name, "dir_name") || f.Name == "oldname" ||
+ f.Name == "newname" || f.Name == "path":
+ if !t.IsNonTerminated && bounds == "" {
+ return "filename" // alias that is easier to read
+ }
+ return fmt.Sprintf("%v[filename %v]", baseType, bounds)
+ }
+ return baseType
+}
+
+func (ctx *context) fieldTypeStruct(f *Field) string {
+ // TODO: change for consistency:
+ // f.Type.Struct += autoSuffix
+ f.Type.Struct += "$auto_record"
+ if ctx.structs[f.Type.Struct].ByteSize == 0 {
+ return "void"
+ }
+ return f.Type.Struct
+}
+
+func (ctx *context) bounds(name string, min, max int) string {
+ if min < 0 || min > max {
+ ctx.error("field %v has bad bounds %v:%v", name, min, max)
+ }
+ if max > min {
+ return fmt.Sprintf(", %v:%v", min, max)
+ }
+ if max != 0 {
+ return fmt.Sprintf(", %v", max)
+ }
+ return ""
+}
+
+const (
+ autoSuffix = "$auto"
+ todoType = "auto_todo"
+)
+
+func fixIdentifier(name string) string {
+ switch name {
+ case "resource", "include", "define", "incdir", "syscall", "parent":
+ return "_" + name
+ }
+ return name
+}
+
+func stringIdentifier(name string) string {
+ // TODO: make the identifier lower case.
+ for _, bad := range []string{" ", ".", "-"} {
+ name = strings.ReplaceAll(name, bad, "_")
+ }
+ return name
+}
+
+func maybeBaseType(baseType string, needBase bool) string {
+ if needBase {
+ return ", " + baseType
+ }
+ return ""
+}
+
+func comma(i int) string {
+ if i == 0 {
+ return ""
+ }
+ return ", "
+}
diff --git a/pkg/declextract/entity.go b/pkg/declextract/entity.go
new file mode 100644
index 000000000..57e589e40
--- /dev/null
+++ b/pkg/declextract/entity.go
@@ -0,0 +1,175 @@
+// Copyright 2024 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.
+
+package declextract
+
+import (
+ "bytes"
+ "encoding/json"
+ "slices"
+)
+
+type Output struct {
+ Includes []string `json:"includes,omitempty"`
+ Defines []*Define `json:"defines,omitempty"`
+ Enums []*Enum `json:"enums,omitempty"`
+ Structs []*Struct `json:"structs,omitempty"`
+ Syscalls []*Syscall `json:"syscalls,omitempty"`
+ IouringOps []*IouringOp `json:"iouring_ops,omitempty"`
+ NetlinkFamilies []*NetlinkFamily `json:"netlink_families,omitempty"`
+ NetlinkPolicies []*NetlinkPolicy `json:"netlink_policies,omitempty"`
+}
+
+type Define struct {
+ Name string `json:"name,omitempty"`
+ Value string `json:"value,omitempty"`
+}
+
+type Field struct {
+ Name string `json:"name,omitempty"`
+ IsAnonymous bool `json:"is_anonymous,omitempty"`
+ BitWidth int `json:"bit_width,omitempty"`
+ CountedBy int `json:"counted_by,omitempty"`
+ Type *Type `json:"type,omitempty"`
+
+ syzType string
+}
+
+type Syscall struct {
+ Func string `json:"func,omitempty"`
+ Args []*Field `json:"args,omitempty"`
+ SourceFile string `json:"source_file,omitempty"`
+}
+
+type IouringOp struct {
+ Name string `json:"name,omitempty"`
+ Func string `json:"func,omitempty"`
+ SourceFile string `json:"source_file,omitempty"`
+}
+
+type NetlinkFamily struct {
+ Name string `json:"name,omitempty"`
+ Ops []*NetlinkOp `json:"ops,omitempty"`
+ SourceFile string `json:"source_file,omitempty"`
+}
+
+type NetlinkPolicy struct {
+ Name string `json:"name,omitempty"`
+ Attrs []*NetlinkAttr `json:"attrs,omitempty"`
+}
+
+type NetlinkOp struct {
+ Name string `json:"name,omitempty"`
+ Func string `json:"func,omitempty"`
+ Access string `json:"access,omitempty"`
+ Policy string `json:"policy,omitempty"`
+}
+
+type NetlinkAttr struct {
+ Name string `json:"name,omitempty"`
+ Kind string `json:"kind,omitempty"`
+ MaxSize int `json:"max_size,omitempty"`
+ NestedPolicy string `json:"nested_policy,omitempty"`
+ Elem *Type `json:"elem,omitempty"`
+}
+
+type Struct struct {
+ Name string `json:"name,omitempty"`
+ ByteSize int `json:"byte_size,omitempty"`
+ IsUnion bool `json:"is_union,omitempty"`
+ IsPacked bool `json:"is_packed,omitempty"`
+ Align int `json:"align,omitempty"`
+ Fields []*Field `json:"fields,omitempty"`
+
+ // TODO: remove me.
+ isVarlen bool
+}
+
+type Enum struct {
+ Name string `json:"name,omitempty"`
+ Values []string `json:"values,omitempty"`
+}
+
+type Type struct {
+ Int *IntType `json:"int,omitempty"`
+ Ptr *PtrType `json:"ptr,omitempty"`
+ Array *ArrayType `json:"array,omitempty"`
+ Buffer *BufferType `json:"buffer,omitempty"`
+ Struct string `json:"struct,omitempty"`
+}
+
+type IntType struct {
+ ByteSize int `json:"byte_size,omitempty"`
+ Name string `json:"name,omitempty"`
+ Base string `json:"base,omitempty"`
+ Enum string `json:"enum,omitempty"`
+
+ isBigEndian bool
+}
+
+type PtrType struct {
+ Elem *Type `json:"elem,omitempty"`
+ IsConst bool `json:"is_const,omitempty"`
+}
+
+type ArrayType struct {
+ Elem *Type `json:"elem,omitempty"`
+ MinSize int `json:"min_size,omitempty"`
+ MaxSize int `json:"max_size,omitempty"`
+}
+
+type BufferType struct {
+ MinSize int `json:"min_size,omitempty"`
+ MaxSize int `json:"max_size,omitempty"`
+ IsString bool `json:"is_string,omitempty"`
+ IsNonTerminated bool `json:"is_non_terminated,omitempty"`
+}
+
+func (out *Output) Merge(other *Output) {
+ out.Includes = append(out.Includes, other.Includes...)
+ out.Defines = append(out.Defines, other.Defines...)
+ out.Enums = append(out.Enums, other.Enums...)
+ out.Structs = append(out.Structs, other.Structs...)
+ out.Syscalls = append(out.Syscalls, other.Syscalls...)
+ out.IouringOps = append(out.IouringOps, other.IouringOps...)
+ out.NetlinkFamilies = append(out.NetlinkFamilies, other.NetlinkFamilies...)
+ out.NetlinkPolicies = append(out.NetlinkPolicies, other.NetlinkPolicies...)
+}
+
+func (out *Output) SortAndDedup() {
+ out.Includes = sortAndDedupSlice(out.Includes)
+ out.Defines = sortAndDedupSlice(out.Defines)
+ out.Enums = sortAndDedupSlice(out.Enums)
+ out.Structs = sortAndDedupSlice(out.Structs)
+ out.Syscalls = sortAndDedupSlice(out.Syscalls)
+ out.IouringOps = sortAndDedupSlice(out.IouringOps)
+ out.NetlinkFamilies = sortAndDedupSlice(out.NetlinkFamilies)
+ out.NetlinkPolicies = sortAndDedupSlice(out.NetlinkPolicies)
+}
+
+// SetSoureFile attaches the source file to the entities that need it.
+// The clang tool could do it, but it looks easier to do it here.
+func (out *Output) SetSourceFile(file string) {
+ for _, call := range out.Syscalls {
+ call.SourceFile = file
+ }
+ for _, fam := range out.NetlinkFamilies {
+ fam.SourceFile = file
+ }
+ for _, op := range out.IouringOps {
+ op.SourceFile = file
+ }
+}
+
+func sortAndDedupSlice[Slice ~[]E, E any](s Slice) Slice {
+ slices.SortFunc(s, func(a, b E) int {
+ aa, _ := json.Marshal(a)
+ bb, _ := json.Marshal(b)
+ return bytes.Compare(aa, bb)
+ })
+ return slices.CompactFunc(s, func(a, b E) bool {
+ aa, _ := json.Marshal(a)
+ bb, _ := json.Marshal(b)
+ return bytes.Equal(aa, bb)
+ })
+}
diff --git a/pkg/declextract/interface.go b/pkg/declextract/interface.go
new file mode 100644
index 000000000..dfb223d16
--- /dev/null
+++ b/pkg/declextract/interface.go
@@ -0,0 +1,46 @@
+// Copyright 2024 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.
+
+package declextract
+
+import (
+ "slices"
+)
+
+type Interface struct {
+ Type string
+ Name string
+ IdentifyingConst string
+ Files []string
+ Func string
+ Access string
+ Subsystems []string
+ ManualDescriptions bool
+ AutoDescriptions bool
+}
+
+const (
+ IfaceSyscall = "SYSCALL"
+ IfaceNetlinkOp = "NETLINK"
+ IfaceIouring = "IOURING"
+
+ AccessUnknown = "unknown"
+ AccessUser = "user"
+ AccessNsAdmin = "ns_admin"
+ AccessAdmin = "admin"
+)
+
+func (ctx *context) noteInterface(iface *Interface) {
+ ctx.interfaces = append(ctx.interfaces, iface)
+}
+
+func (ctx *context) finishInterfaces() {
+ for _, iface := range ctx.interfaces {
+ slices.Sort(iface.Files)
+ iface.Files = slices.Compact(iface.Files)
+ if iface.Access == "" {
+ iface.Access = AccessUnknown
+ }
+ }
+ ctx.interfaces = sortAndDedupSlice(ctx.interfaces)
+}
diff --git a/pkg/declextract/netlink.go b/pkg/declextract/netlink.go
new file mode 100644
index 000000000..de60ae30e
--- /dev/null
+++ b/pkg/declextract/netlink.go
@@ -0,0 +1,236 @@
+// Copyright 2024 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.
+
+package declextract
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+func (ctx *context) fabricateNetlinkPolicies() {
+ for _, pol := range ctx.NetlinkPolicies {
+ if len(pol.Attrs) == 0 {
+ continue
+ }
+ str := &Struct{
+ Name: pol.Name,
+ IsUnion: true,
+ isVarlen: true,
+ }
+ for _, attr := range pol.Attrs {
+ str.Fields = append(str.Fields, &Field{
+ Name: attr.Name,
+ syzType: ctx.nlattrType(attr),
+ })
+ }
+ ctx.Structs = append(ctx.Structs, str)
+ }
+ ctx.Structs = sortAndDedupSlice(ctx.Structs)
+}
+
+func (ctx *context) emitNetlinkTypes() {
+ for _, fam := range ctx.NetlinkFamilies {
+ if isEmptyFamily(fam) {
+ continue
+ }
+ id := stringIdentifier(fam.Name)
+ ctx.fmt("resource genl_%v_family_id_auto[int16]\n", id)
+ }
+ for _, fam := range ctx.NetlinkFamilies {
+ if isEmptyFamily(fam) {
+ continue
+ }
+ id := stringIdentifier(fam.Name)
+ ctx.fmt("type msghdr_%v_auto[CMD, POLICY] msghdr_netlink[netlink_msg_t"+
+ "[genl_%v_family_id_auto, genlmsghdr_t[CMD], POLICY]]\n", id, id)
+ }
+ for _, pol := range ctx.NetlinkPolicies {
+ if len(pol.Attrs) == 0 {
+ ctx.fmt("type %v auto_todo\n", pol.Name)
+ }
+ }
+}
+
+func (ctx *context) emitNetlinkGetFamily() {
+ for _, fam := range ctx.NetlinkFamilies {
+ if isEmptyFamily(fam) {
+ continue
+ }
+ id := stringIdentifier(fam.Name)
+ ctx.fmt("syz_genetlink_get_family_id$auto_%v(name ptr[in, string[\"%v\"]],"+
+ " fd sock_nl_generic) genl_%v_family_id_auto\n", id, fam.Name, id)
+ }
+}
+
+func (ctx *context) emitNetlinkSendmsgs() {
+ var syscalls []string
+ for _, fam := range ctx.NetlinkFamilies {
+ id := stringIdentifier(fam.Name)
+ dedup := make(map[string]bool)
+ for _, op := range fam.Ops {
+ // TODO: emit these as well, these are dump commands w/o input arguments.
+ if op.Policy == "" {
+ continue
+ }
+ // TODO: emit all of these with unique names, these should be doit/dump variants.
+ // They may have different policies.
+ if dedup[op.Name] {
+ continue
+ }
+ dedup[op.Name] = true
+ syscalls = append(syscalls, fmt.Sprintf("sendmsg$auto_%v(fd sock_nl_generic,"+
+ " msg ptr[in, msghdr_%v_auto[%v, %v]], f flags[send_flags])\n",
+ op.Name, id, op.Name, op.Policy))
+
+ ctx.noteInterface(&Interface{
+ Type: IfaceNetlinkOp,
+ Name: op.Name,
+ IdentifyingConst: op.Name,
+ Files: []string{fam.SourceFile},
+ Func: op.Func,
+ Access: op.Access,
+ AutoDescriptions: true,
+ })
+ }
+ }
+ sort.Strings(syscalls)
+ for _, call := range syscalls {
+ ctx.fmt("%s", call)
+ }
+}
+
+func isEmptyFamily(fam *NetlinkFamily) bool {
+ for _, op := range fam.Ops {
+ if op.Policy != "" {
+ return false
+ }
+ }
+ return true
+}
+
+func (ctx *context) nlattrType(attr *NetlinkAttr) string {
+ nlattr, typ := "nlattr", ""
+ switch attr.Kind {
+ case "NLA_BITFIELD32":
+ // TODO: Extract values from NLA_POLICY_BITFIELD32 macro.
+ typ = "int32"
+ case "NLA_MSECS":
+ typ = "int64"
+ case "NLA_FLAG":
+ typ = "void"
+ case "NLA_NESTED", "NLA_NESTED_ARRAY":
+ nlattr = "nlnest"
+ policy := "nl_generic_attr"
+ if attr.NestedPolicy != "" {
+ policy = attr.NestedPolicy
+ }
+ typ = fmt.Sprintf("array[%v]", policy)
+ if attr.Kind == "NLA_NESTED_ARRAY" {
+ typ = fmt.Sprintf("array[nlnest[0, %v]]", typ)
+ }
+ case "NLA_BINARY", "NLA_UNSPEC", "":
+ // TODO: also handle size 6 for MAC addresses.
+ if attr.Elem == nil && (attr.MaxSize == 16 || attr.MaxSize == 0) &&
+ strings.Contains(attr.Name, "IPV6") {
+ typ = "ipv6_addr"
+ break
+ }
+ fallthrough
+ default:
+ field := &Field{
+ Name: attr.Name,
+ Type: ctx.netlinkType(attr),
+ }
+ typ = ctx.fieldType(field, nil, "", true)
+ }
+ return fmt.Sprintf("%v[%v, %v]", nlattr, attr.Name, typ)
+}
+
+func (ctx *context) netlinkType(attr *NetlinkAttr) *Type {
+ switch attr.Kind {
+ case "NLA_STRING", "NLA_NUL_STRING":
+ return &Type{
+ Buffer: &BufferType{
+ MaxSize: attr.MaxSize,
+ IsString: true,
+ IsNonTerminated: attr.Kind == "NLA_STRING",
+ },
+ }
+ case "NLA_BINARY", "NLA_UNSPEC", "":
+ if attr.Elem == nil {
+ switch attr.MaxSize {
+ case 1, 2, 4, 8:
+ attr.Kind = fmt.Sprintf("NLA_U%v", attr.MaxSize*8)
+ return ctx.netlinkTypeInt(attr)
+ }
+ minSize := 0
+ if attr.Kind != "NLA_BINARY" {
+ minSize = attr.MaxSize
+ }
+ return &Type{
+ Buffer: &BufferType{
+ MaxSize: attr.MaxSize,
+ MinSize: minSize,
+ },
+ }
+ }
+ elemSize := 1
+ switch {
+ case attr.Elem.Int != nil:
+ elemSize = attr.Elem.Int.ByteSize
+ case attr.Elem.Struct != "":
+ if str := ctx.structs[attr.Elem.Struct+"$auto_record"]; str != nil {
+ elemSize = str.ByteSize
+ } else {
+ ctx.error("binary nlattr %v referenced non-existing struct %v",
+ attr.Name, attr.Elem.Struct)
+ }
+ default:
+ ctx.error("binary nlattr %v has unsupported elem type", attr.Name)
+ }
+ if attr.MaxSize%elemSize != 0 {
+ ctx.error("binary nlattr %v has odd size: %v, elem size %v",
+ attr.Name, attr.MaxSize, elemSize)
+ }
+ numElems := attr.MaxSize / elemSize
+ if numElems == 1 {
+ return attr.Elem
+ }
+ return &Type{
+ Array: &ArrayType{
+ Elem: attr.Elem,
+ MaxSize: numElems,
+ },
+ }
+ default:
+ return ctx.netlinkTypeInt(attr)
+ }
+}
+
+func (ctx *context) netlinkTypeInt(attr *NetlinkAttr) *Type {
+ size, be := 0, false
+ switch attr.Kind {
+ case "NLA_U8", "NLA_S8":
+ size = 1
+ case "NLA_U16", "NLA_S16":
+ size = 2
+ case "NLA_U32", "NLA_S32":
+ size = 4
+ case "NLA_U64", "NLA_S64", "NLA_SINT", "NLA_UINT":
+ size = 8
+ case "NLA_BE16":
+ size, be = 2, true
+ case "NLA_BE32":
+ size, be = 4, true
+ default:
+ panic(fmt.Sprintf("unhandled netlink attribute kind %v", attr.Kind))
+ }
+ return &Type{
+ Int: &IntType{
+ ByteSize: size,
+ isBigEndian: be,
+ },
+ }
+}
diff --git a/pkg/declextract/serialization.go b/pkg/declextract/serialization.go
new file mode 100644
index 000000000..c737a675c
--- /dev/null
+++ b/pkg/declextract/serialization.go
@@ -0,0 +1,113 @@
+// Copyright 2024 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.
+
+package declextract
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+func (ctx *context) serialize() {
+ ctx.descriptions = new(bytes.Buffer)
+ ctx.fmt(header)
+ ctx.serializeIncludes()
+ ctx.serializeEnums()
+ ctx.emitNetlinkTypes()
+ ctx.serializeSyscalls()
+ ctx.serializeStructs()
+ ctx.serializeDefines()
+}
+
+const header = `# Code generated by syz-declextract. DO NOT EDIT.
+
+meta automatic
+
+type auto_todo intptr
+
+`
+
+func (ctx *context) fmt(msg string, args ...any) {
+ fmt.Fprintf(ctx.descriptions, msg, args...)
+}
+
+func (ctx *context) serializeIncludes() {
+ for _, inc := range ctx.Includes {
+ ctx.fmt("include <%s>\n", inc)
+ }
+ ctx.fmt("\n")
+}
+
+func (ctx *context) serializeDefines() {
+ for _, def := range ctx.Defines {
+ ctx.fmt("define %v %v\n", def.Name, def.Value)
+ }
+ ctx.fmt("\n")
+}
+
+func (ctx *context) serializeSyscalls() {
+ printedGetFamily, printedSendmsg := false, false
+ for _, call := range ctx.Syscalls {
+ ctx.fmt("%v(", call.Func)
+ for i, arg := range call.Args {
+ ctx.fmt("%v%v %v", comma(i), arg.Name, arg.syzType)
+ }
+ ctx.fmt(")\n")
+
+ if call.Func == "syslog$auto" {
+ printedGetFamily = true
+ ctx.emitNetlinkGetFamily()
+ }
+ if call.Func == "sendmsg$auto" {
+ printedSendmsg = true
+ ctx.emitNetlinkSendmsgs()
+ }
+ }
+ if !printedGetFamily {
+ ctx.emitNetlinkGetFamily()
+ }
+ if !printedSendmsg {
+ ctx.emitNetlinkSendmsgs()
+ }
+ ctx.fmt("\n")
+}
+
+func (ctx *context) serializeEnums() {
+ for _, enum := range ctx.Enums {
+ ctx.fmt("%v = ", enum.Name)
+ for i, val := range enum.Values {
+ ctx.fmt("%v %v", comma(i), val)
+ }
+ ctx.fmt("\n")
+ }
+ ctx.fmt("\n")
+}
+
+func (ctx *context) serializeStructs() {
+ for _, str := range ctx.Structs {
+ delims := "{}"
+ if str.IsUnion {
+ delims = "[]"
+ }
+ ctx.fmt("%v %c\n", str.Name, delims[0])
+ for _, f := range str.Fields {
+ ctx.fmt("%v %v\n", f.Name, f.syzType)
+ }
+ ctx.fmt("%c", delims[1])
+ var attrs []string
+ if str.IsPacked {
+ attrs = append(attrs, "packed")
+ }
+ if str.Align != 0 {
+ attrs = append(attrs, fmt.Sprintf("align[%v]", str.Align))
+ }
+ if str.isVarlen {
+ attrs = append(attrs, "varlen")
+ }
+ if len(attrs) != 0 {
+ ctx.fmt(" [%v]", strings.Join(attrs, ", "))
+ }
+ ctx.fmt("\n\n")
+ }
+}