From 21be59580a51a37397d2041c46d33c7c529b35b8 Mon Sep 17 00:00:00 2001 From: Timo Beckers Date: Thu, 26 Sep 2024 16:27:43 +0200 Subject: [PATCH] map: add map_extra, memlock, frozen to MapInfo - Added MapInfo.MapExtra(), .Memlock() and .Frozen() methods. - Moved all unexported fields to a separate section within MapInfo. - Changed a few tests to use quicktest instead of manual if statements. This change required a small refactor to how fdinfo is used, since memlock and frozen are only available there. fdinfo is now always consulted regardless of the success of ObjInfo, and any empty fields are supplemented by data from fdinfo. Removed errMissingFields since its meaning sort of overlapped with ErrNotSupported. It was only used for getting ProgramInfo, which currently looks for just 2 fields. If we want to pull more fields from fdinfo, this no longer works for the majority of older kernels, since they'll always be missing some fields that were introduced later. Going forward, rely on ErrNotSupported to tell us if no fields were found in fdinfo, which would indicate there's no map info present because we're trying to query the wrong kind of fd (e.g. a program, link or regular file). Signed-off-by: Timo Beckers --- info.go | 131 +++++++++++++++++++++++++++++++++++++++------------ info_test.go | 79 ++++++++++++++++--------------- map.go | 7 ++- 3 files changed, 147 insertions(+), 70 deletions(-) diff --git a/info.go b/info.go index 93cc2dbae..4318540a3 100644 --- a/info.go +++ b/info.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "reflect" "strings" "syscall" "time" @@ -39,53 +40,83 @@ import ( // MapInfo describes a map. type MapInfo struct { - Type MapType - id MapID - KeySize uint32 - ValueSize uint32 + // Type of the map. + Type MapType + // KeySize is the size of the map key in bytes. + KeySize uint32 + // ValueSize is the size of the map value in bytes. + ValueSize uint32 + // MaxEntries is the maximum number of entries the map can hold. Its meaning + // is map-specific. MaxEntries uint32 - Flags uint32 + // Flags used during map creation. + Flags uint32 // Name as supplied by user space at load time. Available from 4.15. Name string - btf btf.ID + id MapID + btf btf.ID + mapExtra uint64 + memlock uint64 + frozen bool } +// newMapInfoFromFd queries map information about the given fd. [sys.ObjInfo] is +// attempted first, supplementing any missing values with information from +// /proc/self/fdinfo. Ignores EINVAL from ObjInfo as well as ErrNotSupported +// from reading fdinfo (indicating the file exists, but no fields of interest +// were found). If both fail, an error is always returned. func newMapInfoFromFd(fd *sys.FD) (*MapInfo, error) { var info sys.MapInfo - err := sys.ObjInfo(fd, &info) - if errors.Is(err, syscall.EINVAL) { - return newMapInfoFromProc(fd) - } - if err != nil { - return nil, err + err1 := sys.ObjInfo(fd, &info) + // EINVAL means the kernel doesn't support BPF_OBJ_GET_INFO_BY_FD. Continue + // with fdinfo if that's the case. + if err1 != nil && !errors.Is(err1, unix.EINVAL) { + return nil, fmt.Errorf("getting object info: %w", err1) } - return &MapInfo{ + mi := &MapInfo{ MapType(info.Type), - MapID(info.Id), info.KeySize, info.ValueSize, info.MaxEntries, uint32(info.MapFlags), unix.ByteSliceToString(info.Name[:]), + MapID(info.Id), btf.ID(info.BtfId), - }, nil + info.MapExtra, + 0, + false, + } + + // Supplement OBJ_INFO with data from /proc/self/fdinfo. It contains fields + // like memlock and frozen that are not present in OBJ_INFO. + err2 := readMapInfoFromProc(fd, mi) + if err2 != nil && !errors.Is(err2, ErrNotSupported) { + return nil, fmt.Errorf("getting map info from fdinfo: %w", err2) + } + + if err1 != nil && err2 != nil { + return nil, fmt.Errorf("ObjInfo and fdinfo both failed: objinfo: %w, fdinfo: %w", err1, err2) + } + + return mi, nil } -func newMapInfoFromProc(fd *sys.FD) (*MapInfo, error) { - var mi MapInfo - err := scanFdInfo(fd, map[string]interface{}{ +// readMapInfoFromProc queries map information about the given fd from +// /proc/self/fdinfo. It only writes data into fields that have a zero value. +func readMapInfoFromProc(fd *sys.FD, mi *MapInfo) error { + return scanFdInfo(fd, map[string]interface{}{ "map_type": &mi.Type, + "map_id": &mi.id, "key_size": &mi.KeySize, "value_size": &mi.ValueSize, "max_entries": &mi.MaxEntries, "map_flags": &mi.Flags, + "map_extra": &mi.mapExtra, + "memlock": &mi.memlock, + "frozen": &mi.frozen, }) - if err != nil { - return nil, err - } - return &mi, nil } // ID returns the map ID. @@ -109,6 +140,35 @@ func (mi *MapInfo) BTFID() (btf.ID, bool) { return mi.btf, mi.btf > 0 } +// MapExtra returns an opaque field whose meaning is map-specific. +// +// Available from 5.16. +// +// The bool return value indicates whether this optional field is available and +// populated, if it was specified during Map creation. +func (mi *MapInfo) MapExtra() (uint64, bool) { + return mi.mapExtra, mi.mapExtra > 0 +} + +// Memlock returns an approximate number of bytes allocated to this map. +// +// Available from 4.10. +// +// The bool return value indicates whether this optional field is available. +func (mi *MapInfo) Memlock() (uint64, bool) { + return mi.memlock, mi.memlock > 0 +} + +// Frozen indicates whether [Map.Freeze] was called on this map. If true, +// modifications from user space are not allowed. +// +// Available from 5.2. Requires access to procfs. +// +// If the kernel doesn't support map freezing, this field will always be false. +func (mi *MapInfo) Frozen() bool { + return mi.frozen +} + // programStats holds statistics of a program. type programStats struct { // Total accumulated runtime of the program ins ns. @@ -236,7 +296,7 @@ func newProgramInfoFromProc(fd *sys.FD) (*ProgramInfo, error) { "prog_type": &info.Type, "prog_tag": &info.Tag, }) - if errors.Is(err, errMissingFields) { + if errors.Is(err, ErrNotSupported) { return nil, &internal.UnsupportedFeatureError{ Name: "reading program info from /proc/self/fdinfo", MinimumVersion: internal.Version{4, 10, 0}, @@ -461,8 +521,6 @@ func scanFdInfo(fd *sys.FD, fields map[string]interface{}) error { return nil } -var errMissingFields = errors.New("missing fields") - func scanFdInfoReader(r io.Reader, fields map[string]interface{}) error { var ( scanner = bufio.NewScanner(r) @@ -481,26 +539,37 @@ func scanFdInfoReader(r io.Reader, fields map[string]interface{}) error { continue } - if n, err := fmt.Sscanln(parts[1], field); err != nil || n != 1 { - return fmt.Errorf("can't parse field %s: %v", name, err) + // If field already contains a non-zero value, don't overwrite it with fdinfo. + if zero(field) { + if n, err := fmt.Sscanln(parts[1], field); err != nil || n != 1 { + return fmt.Errorf("can't parse field %s: %v", name, err) + } } scanned++ } if err := scanner.Err(); err != nil { - return err + return fmt.Errorf("scanning fdinfo: %w", err) } if len(fields) > 0 && scanned == 0 { return ErrNotSupported } - if scanned != len(fields) { - return errMissingFields + return nil +} + +func zero(arg any) bool { + v := reflect.ValueOf(arg) + + // Unwrap pointers and interfaces. + for v.Kind() == reflect.Pointer || + v.Kind() == reflect.Interface { + v = v.Elem() } - return nil + return v.IsZero() } // EnableStats starts the measuring of the runtime diff --git a/info_test.go b/info_test.go index a0987c4fd..a3c31d3e2 100644 --- a/info_test.go +++ b/info_test.go @@ -19,7 +19,6 @@ import ( func TestMapInfoFromProc(t *testing.T) { hash, err := NewMap(&MapSpec{ - Name: "testing", Type: Hash, KeySize: 4, ValueSize: 5, @@ -32,41 +31,20 @@ func TestMapInfoFromProc(t *testing.T) { } defer hash.Close() - info, err := newMapInfoFromProc(hash.fd) + var info MapInfo + err = readMapInfoFromProc(hash.fd, &info) testutils.SkipIfNotSupported(t, err) - if err != nil { - t.Fatal("Can't get map info:", err) - } - - if info.Type != Hash { - t.Error("Expected Hash, got", info.Type) - } - - if info.KeySize != 4 { - t.Error("Expected KeySize of 4, got", info.KeySize) - } - - if info.ValueSize != 5 { - t.Error("Expected ValueSize of 5, got", info.ValueSize) - } - if info.MaxEntries != 2 { - t.Error("Expected MaxEntries of 2, got", info.MaxEntries) - } - - if info.Flags != sys.BPF_F_NO_PREALLOC { - t.Errorf("Expected Flags to be %d, got %d", sys.BPF_F_NO_PREALLOC, info.Flags) - } - - if info.Name != "" && info.Name != "testing" { - t.Error("Expected name to be testing, got", info.Name) - } - - if _, ok := info.ID(); ok { - t.Error("Expected ID to not be available") - } + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(info.Type, Hash)) + qt.Assert(t, qt.Equals(info.KeySize, 4)) + qt.Assert(t, qt.Equals(info.ValueSize, 5)) + qt.Assert(t, qt.Equals(info.MaxEntries, 2)) + qt.Assert(t, qt.Equals(info.Flags, sys.BPF_F_NO_PREALLOC)) +} - nested, err := NewMap(&MapSpec{ +func TestMapInfoFromProcOuterMap(t *testing.T) { + outer, err := NewMap(&MapSpec{ Type: ArrayOfMaps, KeySize: 4, MaxEntries: 2, @@ -81,12 +59,15 @@ func TestMapInfoFromProc(t *testing.T) { if err != nil { t.Fatal(err) } - defer nested.Close() + defer outer.Close() - _, err = newMapInfoFromProc(nested.fd) - if err != nil { - t.Fatal("Can't get nested map info from /proc:", err) - } + var info MapInfo + err = readMapInfoFromProc(outer.fd, &info) + testutils.SkipIfNotSupported(t, err) + + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(info.KeySize, 4)) + qt.Assert(t, qt.Equals(info.MaxEntries, 2)) } func TestProgramInfo(t *testing.T) { @@ -507,3 +488,25 @@ func TestInfoExportedFields(t *testing.T) { "Name", })) } + +func TestZero(t *testing.T) { + var ( + nul uint32 = 0 + one uint32 = 1 + + inul any = uint32(0) + ione any = uint32(1) + ) + + qt.Assert(t, qt.IsTrue(zero(nul))) + qt.Assert(t, qt.IsFalse(zero(one))) + + qt.Assert(t, qt.IsTrue(zero(&nul))) + qt.Assert(t, qt.IsFalse(zero(&one))) + + qt.Assert(t, qt.IsTrue(zero(inul))) + qt.Assert(t, qt.IsFalse(zero(ione))) + + qt.Assert(t, qt.IsTrue(zero(&inul))) + qt.Assert(t, qt.IsFalse(zero(&ione))) +} diff --git a/map.go b/map.go index 7782f4eb2..d5f087da1 100644 --- a/map.go +++ b/map.go @@ -582,7 +582,12 @@ func (m *Map) Flags() uint32 { return m.flags } -// Info returns metadata about the map. +// Info returns metadata about the map. This was first introduced in Linux 4.5, +// but newer kernels support more MapInfo fields with the introduction of more +// features. See [MapInfo] and its methods for more details. +// +// Returns an error wrapping ErrNotSupported if the kernel supports neither +// BPF_OBJ_GET_INFO_BY_FD nor reading map information from /proc/self/fdinfo. func (m *Map) Info() (*MapInfo, error) { return newMapInfoFromFd(m.fd) }