Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

map: add map_extra, memlock, frozen to MapInfo #1570

Merged
merged 1 commit into from
Oct 1, 2024
Merged
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
131 changes: 100 additions & 31 deletions info.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"os"
"reflect"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
79 changes: 41 additions & 38 deletions info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (

func TestMapInfoFromProc(t *testing.T) {
hash, err := NewMap(&MapSpec{
Name: "testing",
Type: Hash,
KeySize: 4,
ValueSize: 5,
Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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)))
}
7 changes: 6 additions & 1 deletion map.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down