Skip to content

Commit 19b4163

Browse files
shun159lmb
authored andcommitted
elf_reader: add struct_ops support
This commit adds struct_ops support to the ELF reader: it classifies non-executable PROGBITS sections, parses their BTF Datasec to build MapSpecs, associates relocs with func-pointer members to set ps.AttachTo, and adds TestStructOps. Related: #1845 Signed-off-by: shun159 <[email protected]>
1 parent c64ffee commit 19b4163

File tree

8 files changed

+263
-8
lines changed

8 files changed

+263
-8
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ TARGETS := \
5050
testdata/errors \
5151
testdata/variables \
5252
testdata/arena \
53+
testdata/struct_ops \
5354
btf/testdata/relocs \
5455
btf/testdata/relocs_read \
5556
btf/testdata/relocs_read_tgt \

elf_reader.go

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,17 @@ func LoadCollectionSpecFromReader(rd io.ReaderAt) (*CollectionSpec, error) {
116116
case sec.Type == elf.SHT_REL:
117117
// Store relocations under the section index of the target
118118
relSections[elf.SectionIndex(sec.Info)] = sec
119-
case sec.Type == elf.SHT_PROGBITS && (sec.Flags&elf.SHF_EXECINSTR) != 0 && sec.Size > 0:
120-
sections[idx] = newElfSection(sec, programSection)
119+
case sec.Type == elf.SHT_PROGBITS && sec.Size > 0:
120+
if (sec.Flags&elf.SHF_EXECINSTR) != 0 && sec.Size > 0 {
121+
sections[idx] = newElfSection(sec, programSection)
122+
} else if sec.Name == structOpsLinkSec {
123+
// classification based on sec names so that struct_ops-specific
124+
// sections (.struct_ops.link) is correctly recognized
125+
// as non-executable PROGBITS, allowing value placement and link metadata to be loaded.
126+
sections[idx] = newElfSection(sec, structOpsSection)
127+
} else if sec.Name == structOpsSec {
128+
return nil, fmt.Errorf("section %q: got '.struct_ops' section: %w", sec.Name, ErrNotSupported)
129+
}
121130
}
122131
}
123132

@@ -186,6 +195,11 @@ func LoadCollectionSpecFromReader(rd io.ReaderAt) (*CollectionSpec, error) {
186195
return nil, fmt.Errorf("load programs: %w", err)
187196
}
188197

198+
// assiociate members in structs with ProgramSpecs using relo
199+
if err := ec.associateStructOpsRelocs(progs); err != nil {
200+
return nil, fmt.Errorf("load struct_ops: %w", err)
201+
}
202+
189203
return &CollectionSpec{
190204
ec.maps,
191205
progs,
@@ -239,6 +253,7 @@ const (
239253
btfMapSection
240254
programSection
241255
dataSection
256+
structOpsSection
242257
)
243258

244259
type elfSection struct {
@@ -1379,6 +1394,91 @@ func (ec *elfCode) loadKsymsSection() error {
13791394
return nil
13801395
}
13811396

1397+
// associateStructOpsRelocs handles `.struct_ops.link`
1398+
// and associates the target function with the correct struct member in the map.
1399+
func (ec *elfCode) associateStructOpsRelocs(progs map[string]*ProgramSpec) error {
1400+
for _, sec := range ec.sections {
1401+
if sec.kind != structOpsSection {
1402+
continue
1403+
}
1404+
1405+
userData, err := sec.Data()
1406+
if err != nil {
1407+
return fmt.Errorf("failed to read section data: %w", err)
1408+
}
1409+
1410+
// Resolve the BTF datasec describing variables in this section.
1411+
var ds *btf.Datasec
1412+
if err := ec.btf.TypeByName(sec.Name, &ds); err != nil {
1413+
return fmt.Errorf("datasec %s: %w", sec.Name, err)
1414+
}
1415+
1416+
// Set flags for .struct_ops.link (BPF_F_LINK).
1417+
flags := uint32(0)
1418+
if sec.Name == structOpsLinkSec {
1419+
flags = sys.BPF_F_LINK
1420+
}
1421+
1422+
for _, vsi := range ds.Vars {
1423+
userSt, baseOff, err := ec.createStructOpsMap(vsi, userData, flags)
1424+
if err != nil {
1425+
return err
1426+
}
1427+
1428+
if err := structOpsSetAttachTo(sec, baseOff, userSt, progs); err != nil {
1429+
return err
1430+
}
1431+
}
1432+
}
1433+
1434+
return nil
1435+
}
1436+
1437+
// createStructOpsMap() creates and registers a MapSpec for a struct_ops
1438+
func (ec *elfCode) createStructOpsMap(vsi btf.VarSecinfo, userData []byte, flags uint32) (*btf.Struct, uint32, error) {
1439+
varType, ok := btf.As[*btf.Var](vsi.Type)
1440+
if !ok {
1441+
return nil, 0, fmt.Errorf("vsi: expect var, got %T", vsi.Type)
1442+
}
1443+
1444+
mapName := varType.Name
1445+
1446+
userSt, ok := btf.As[*btf.Struct](varType.Type)
1447+
if !ok {
1448+
return nil, 0, fmt.Errorf("var %s: expect struct, got %T", varType.Name, varType.Type)
1449+
}
1450+
1451+
userSize := userSt.Size
1452+
baseOff := vsi.Offset
1453+
if baseOff+userSize > uint32(len(userData)) {
1454+
return nil, 0, fmt.Errorf("%s exceeds section", mapName)
1455+
}
1456+
1457+
// Register the MapSpec for this struct_ops instance if doesn't exist
1458+
if _, exists := ec.maps[mapName]; exists {
1459+
return nil, 0, fmt.Errorf("struct_ops map %s: already exists", mapName)
1460+
}
1461+
1462+
ec.maps[mapName] = &MapSpec{
1463+
Name: mapName,
1464+
Type: StructOpsMap,
1465+
Key: &btf.Int{Size: 4},
1466+
KeySize: structOpsKeySize,
1467+
ValueSize: userSize, // length of the user-struct type
1468+
Value: userSt,
1469+
Flags: flags,
1470+
MaxEntries: 1,
1471+
Contents: []MapKV{
1472+
{
1473+
Key: uint32(0),
1474+
Value: append([]byte(nil), userData[baseOff:baseOff+userSize]...),
1475+
},
1476+
},
1477+
}
1478+
1479+
return userSt, baseOff, nil
1480+
}
1481+
13821482
type libbpfElfSectionDef struct {
13831483
pattern string
13841484
programType sys.ProgType
@@ -1419,6 +1519,9 @@ func init() {
14191519
// This has been in the library since the beginning of time. Not sure
14201520
// where it came from.
14211521
{"seccomp", sys.BPF_PROG_TYPE_SOCKET_FILTER, 0, _SEC_NONE},
1522+
// Override libbpf definition because we want ignoreExtra.
1523+
{"struct_ops+", sys.BPF_PROG_TYPE_STRUCT_OPS, 0, _SEC_NONE | ignoreExtra},
1524+
{"struct_ops.s+", sys.BPF_PROG_TYPE_STRUCT_OPS, 0, _SEC_SLEEPABLE | ignoreExtra},
14221525
}, elfSectionDefs...)
14231526
}
14241527

elf_reader_test.go

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"syscall"
1313
"testing"
1414

15+
"github.com/cilium/ebpf/asm"
1516
"github.com/cilium/ebpf/btf"
1617
"github.com/cilium/ebpf/internal"
1718
"github.com/cilium/ebpf/internal/kallsyms"
@@ -932,6 +933,102 @@ func TestArena(t *testing.T) {
932933
mustNewCollection(t, coll, nil)
933934
}
934935

936+
func TestStructOps(t *testing.T) {
937+
file := testutils.NativeFile(t, "testdata/struct_ops-%s.elf")
938+
coll, err := LoadCollectionSpec(file)
939+
qt.Assert(t, qt.IsNil(err))
940+
941+
userData := []byte{
942+
// test_1 func ptr (8B)
943+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
944+
// test_2 func ptr (8B)
945+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
946+
// data (4B) + padding (4B)
947+
0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, 0x00, 0x00,
948+
}
949+
950+
want := &CollectionSpec{
951+
Maps: map[string]*MapSpec{
952+
"testmod_ops": {
953+
Name: "testmod_ops",
954+
Type: StructOpsMap,
955+
MaxEntries: 1,
956+
Flags: sys.BPF_F_LINK,
957+
Key: &btf.Int{Size: 4},
958+
KeySize: 4,
959+
ValueSize: 24,
960+
Value: &btf.Struct{
961+
Name: "bpf_testmod_ops",
962+
Size: 24,
963+
Members: []btf.Member{
964+
{
965+
Name: "test_1",
966+
Type: &btf.Pointer{
967+
Target: &btf.FuncProto{
968+
Params: []btf.FuncParam{},
969+
Return: &btf.Int{Name: "int", Size: 4, Encoding: btf.Signed}}},
970+
Offset: 0,
971+
},
972+
{
973+
Name: "test_2",
974+
Type: &btf.Pointer{
975+
Target: &btf.FuncProto{
976+
Params: []btf.FuncParam{
977+
{Type: &btf.Int{Name: "int", Size: 4, Encoding: btf.Signed}},
978+
{Type: &btf.Int{Name: "int", Size: 4, Encoding: btf.Signed}},
979+
},
980+
Return: (*btf.Void)(nil),
981+
},
982+
},
983+
Offset: 64,
984+
},
985+
{
986+
Name: "data",
987+
Type: &btf.Int{Name: "int", Size: 4, Encoding: btf.Signed},
988+
Offset: 128, // bits
989+
},
990+
},
991+
},
992+
Contents: []MapKV{
993+
{
994+
Key: uint32(0),
995+
Value: userData,
996+
},
997+
},
998+
},
999+
},
1000+
Programs: map[string]*ProgramSpec{
1001+
"test_1": {
1002+
Name: "test_1",
1003+
Type: StructOps,
1004+
AttachTo: "bpf_testmod_ops:test_1",
1005+
License: "GPL",
1006+
SectionName: "struct_ops/test_1",
1007+
Instructions: asm.Instructions{
1008+
asm.Mov.Imm(asm.R0, 0),
1009+
asm.Return(),
1010+
},
1011+
},
1012+
},
1013+
Variables: map[string]*VariableSpec{},
1014+
}
1015+
1016+
testModOps, ok := coll.Maps["testmod_ops"]
1017+
if !ok {
1018+
t.Fatalf("testmod_ops doesn't exist")
1019+
}
1020+
1021+
data, ok := testModOps.Contents[0].Value.([]byte)
1022+
if !ok {
1023+
t.Fatalf("Contents[0].Value should be an array of byte")
1024+
}
1025+
1026+
qt.Assert(t, qt.CmpEquals(coll.Programs, want.Programs, csCmpOpts))
1027+
qt.Assert(t, qt.CmpEquals(coll.Maps, want.Maps, csCmpOpts))
1028+
qt.Assert(t, qt.CmpEquals(testModOps.Value, want.Maps["testmod_ops"].Value, csCmpOpts))
1029+
qt.Assert(t, qt.CmpEquals(data, userData, csCmpOpts))
1030+
}
1031+
9351032
var (
9361033
elfPath = flag.String("elfs", os.Getenv("CI_KERNEL_SELFTESTS"), "`Path` containing libbpf-compatible ELFs (defaults to $CI_KERNEL_SELFTESTS)")
9371034
elfPattern = flag.String("elf-pattern", "*.o", "Glob `pattern` for object files that should be tested")
@@ -968,6 +1065,9 @@ func TestLibBPFCompat(t *testing.T) {
9681065
t.Fatal("Expected an error during load")
9691066
}
9701067
} else if err != nil {
1068+
if errors.Is(err, errUnknownStructOps) {
1069+
t.Skip("Skipping since the struct_ops target doesn't exist in kernel")
1070+
}
9711071
t.Fatal("Error during loading:", err)
9721072
}
9731073
}
@@ -1061,12 +1161,6 @@ func TestLibBPFCompat(t *testing.T) {
10611161
}
10621162
}
10631163

1064-
for _, ps := range spec.Programs {
1065-
if ps.Type == StructOps {
1066-
ps.AttachTo = ""
1067-
}
1068-
}
1069-
10701164
coreFiles := sourceOfBTF(t, path)
10711165
if len(coreFiles) == 0 {
10721166
// NB: test_core_reloc_kernel.o doesn't have dedicated BTF and

prog.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ var errBadRelocation = errors.New("bad CO-RE relocation")
3737
// This error is detected based on heuristics and therefore may not be reliable.
3838
var errUnknownKfunc = errors.New("unknown kfunc")
3939

40+
// errUnknownStructOps is returned when the struct_ops target doesn't exist in kernel
41+
var errUnknownStructOps = errors.New("unknown struct_ops target")
42+
4043
// ProgramID represents the unique ID of an eBPF program.
4144
type ProgramID = sys.ProgramID
4245

struct_ops.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ebpf
22

33
import (
4+
"errors"
45
"fmt"
56
"reflect"
67
"strings"
@@ -10,6 +11,9 @@ import (
1011
)
1112

1213
const structOpsValuePrefix = "bpf_struct_ops_"
14+
const structOpsLinkSec = ".struct_ops.link"
15+
const structOpsSec = ".struct_ops"
16+
const structOpsKeySize = 4
1317

1418
// structOpsFindInnerType returns the "inner" struct inside a value struct_ops type.
1519
//
@@ -44,6 +48,9 @@ func structOpsFindTarget(userType *btf.Struct, cache *btf.Cache) (vType *btf.Str
4448

4549
target := btf.Type((*btf.Struct)(nil))
4650
spec, module, err := findTargetInKernel(vTypeName, &target, cache)
51+
if errors.Is(err, btf.ErrNotFound) {
52+
return nil, 0, nil, fmt.Errorf("%q doesn't exist in kernel: %w", vTypeName, errUnknownStructOps)
53+
}
4754
if err != nil {
4855
return nil, 0, nil, fmt.Errorf("lookup value type %q: %w", vTypeName, err)
4956
}
@@ -137,3 +144,32 @@ func structOpsIsMemZeroed(data []byte) bool {
137144
}
138145
return true
139146
}
147+
148+
// structOpsSetAttachTo sets p.AttachTo in the expected "struct_name:memberName" format
149+
// based on the struct definition.
150+
//
151+
// this relies on the assumption that each member in the
152+
// `.struct_ops` section has a relocation at its starting byte offset.
153+
func structOpsSetAttachTo(
154+
sec *elfSection,
155+
baseOff uint32,
156+
userSt *btf.Struct,
157+
progs map[string]*ProgramSpec) error {
158+
for _, m := range userSt.Members {
159+
memberOff := m.Offset
160+
sym, ok := sec.relocations[uint64(baseOff+memberOff.Bytes())]
161+
if !ok {
162+
continue
163+
}
164+
p, ok := progs[sym.Name]
165+
if !ok || p == nil {
166+
return fmt.Errorf("program %s not found", sym.Name)
167+
}
168+
169+
if p.Type != StructOps {
170+
return fmt.Errorf("program %s is not StructOps", sym.Name)
171+
}
172+
p.AttachTo = userSt.Name + ":" + m.Name
173+
}
174+
return nil
175+
}

testdata/struct_ops-eb.elf

1.79 KB
Binary file not shown.

testdata/struct_ops-el.elf

1.79 KB
Binary file not shown.

testdata/struct_ops.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#include "common.h"
2+
3+
char _license[] __section("license") = "GPL";
4+
5+
struct bpf_testmod_ops {
6+
int (*test_1)(void);
7+
void (*test_2)(int, int);
8+
int data;
9+
};
10+
11+
__section("struct_ops/test_1") int test_1(void) {
12+
return 0;
13+
}
14+
15+
__section(".struct_ops.link") struct bpf_testmod_ops testmod_ops = {
16+
.test_1 = (void *)test_1,
17+
.data = 0xdeadbeef,
18+
};

0 commit comments

Comments
 (0)