package btf import ( "encoding/binary" "errors" "fmt" "math" "reflect" "strconv" "strings" "github.com/cilium/ebpf/asm" ) // Code in this file is derived from libbpf, which is available under a BSD // 2-Clause license. // COREFixup is the result of computing a CO-RE relocation for a target. type COREFixup struct { kind coreKind local uint32 target uint32 // True if there is no valid fixup. The instruction is replaced with an // invalid dummy. poison bool // True if the validation of the local value should be skipped. Used by // some kinds of bitfield relocations. skipLocalValidation bool } func (f *COREFixup) equal(other COREFixup) bool { return f.local == other.local && f.target == other.target } func (f *COREFixup) String() string { if f.poison { return fmt.Sprintf("%s=poison", f.kind) } return fmt.Sprintf("%s=%d->%d", f.kind, f.local, f.target) } func (f *COREFixup) Apply(ins *asm.Instruction) error { if f.poison { const badRelo = 0xbad2310 *ins = asm.BuiltinFunc(badRelo).Call() return nil } switch class := ins.OpCode.Class(); class { case asm.LdXClass, asm.StClass, asm.StXClass: if want := int16(f.local); !f.skipLocalValidation && want != ins.Offset { return fmt.Errorf("invalid offset %d, expected %d", ins.Offset, f.local) } if f.target > math.MaxInt16 { return fmt.Errorf("offset %d exceeds MaxInt16", f.target) } ins.Offset = int16(f.target) case asm.LdClass: if !ins.IsConstantLoad(asm.DWord) { return fmt.Errorf("not a dword-sized immediate load") } if want := int64(f.local); !f.skipLocalValidation && want != ins.Constant { return fmt.Errorf("invalid immediate %d, expected %d (fixup: %v)", ins.Constant, want, f) } ins.Constant = int64(f.target) case asm.ALUClass: if ins.OpCode.ALUOp() == asm.Swap { return fmt.Errorf("relocation against swap") } fallthrough case asm.ALU64Class: if src := ins.OpCode.Source(); src != asm.ImmSource { return fmt.Errorf("invalid source %s", src) } if want := int64(f.local); !f.skipLocalValidation && want != ins.Constant { return fmt.Errorf("invalid immediate %d, expected %d (fixup: %v, kind: %v, ins: %v)", ins.Constant, want, f, f.kind, ins) } if f.target > math.MaxInt32 { return fmt.Errorf("immediate %d exceeds MaxInt32", f.target) } ins.Constant = int64(f.target) default: return fmt.Errorf("invalid class %s", class) } return nil } func (f COREFixup) isNonExistant() bool { return f.kind.checksForExistence() && f.target == 0 } // coreKind is the type of CO-RE relocation as specified in BPF source code. type coreKind uint32 const ( reloFieldByteOffset coreKind = iota /* field byte offset */ reloFieldByteSize /* field size in bytes */ reloFieldExists /* field existence in target kernel */ reloFieldSigned /* field signedness (0 - unsigned, 1 - signed) */ reloFieldLShiftU64 /* bitfield-specific left bitshift */ reloFieldRShiftU64 /* bitfield-specific right bitshift */ reloTypeIDLocal /* type ID in local BPF object */ reloTypeIDTarget /* type ID in target kernel */ reloTypeExists /* type existence in target kernel */ reloTypeSize /* type size in bytes */ reloEnumvalExists /* enum value existence in target kernel */ reloEnumvalValue /* enum value integer value */ ) func (k coreKind) checksForExistence() bool { return k == reloEnumvalExists || k == reloTypeExists || k == reloFieldExists } func (k coreKind) String() string { switch k { case reloFieldByteOffset: return "byte_off" case reloFieldByteSize: return "byte_sz" case reloFieldExists: return "field_exists" case reloFieldSigned: return "signed" case reloFieldLShiftU64: return "lshift_u64" case reloFieldRShiftU64: return "rshift_u64" case reloTypeIDLocal: return "local_type_id" case reloTypeIDTarget: return "target_type_id" case reloTypeExists: return "type_exists" case reloTypeSize: return "type_size" case reloEnumvalExists: return "enumval_exists" case reloEnumvalValue: return "enumval_value" default: return "unknown" } } // CORERelocate calculates the difference in types between local and target. // // Returns a list of fixups which can be applied to instructions to make them // match the target type(s). // // Fixups are returned in the order of relos, e.g. fixup[i] is the solution // for relos[i]. func CORERelocate(local, target *Spec, relos []*CORERelocation) ([]COREFixup, error) { if local.byteOrder != target.byteOrder { return nil, fmt.Errorf("can't relocate %s against %s", local.byteOrder, target.byteOrder) } type reloGroup struct { relos []*CORERelocation // Position of each relocation in relos. indices []int } // Split relocations into per Type lists. relosByType := make(map[Type]*reloGroup) result := make([]COREFixup, len(relos)) for i, relo := range relos { if relo.kind == reloTypeIDLocal { // Filtering out reloTypeIDLocal here makes our lives a lot easier // down the line, since it doesn't have a target at all. if len(relo.accessor) > 1 || relo.accessor[0] != 0 { return nil, fmt.Errorf("%s: unexpected accessor %v", relo.kind, relo.accessor) } id, err := local.TypeID(relo.typ) if err != nil { return nil, fmt.Errorf("%s: %w", relo.kind, err) } result[i] = COREFixup{ kind: relo.kind, local: uint32(id), target: uint32(id), } continue } group, ok := relosByType[relo.typ] if !ok { group = &reloGroup{} relosByType[relo.typ] = group } group.relos = append(group.relos, relo) group.indices = append(group.indices, i) } for localType, group := range relosByType { localTypeName := localType.TypeName() if localTypeName == "" { return nil, fmt.Errorf("relocate unnamed or anonymous type %s: %w", localType, ErrNotSupported) } targets := target.namedTypes[newEssentialName(localTypeName)] fixups, err := coreCalculateFixups(local, target, localType, targets, group.relos) if err != nil { return nil, fmt.Errorf("relocate %s: %w", localType, err) } for j, index := range group.indices { result[index] = fixups[j] } } return result, nil } var errAmbiguousRelocation = errors.New("ambiguous relocation") var errImpossibleRelocation = errors.New("impossible relocation") // coreCalculateFixups calculates the fixups for the given relocations using // the "best" target. // // The best target is determined by scoring: the less poisoning we have to do // the better the target is. func coreCalculateFixups(localSpec, targetSpec *Spec, local Type, targets []Type, relos []*CORERelocation) ([]COREFixup, error) { localID, err := localSpec.TypeID(local) if err != nil { return nil, fmt.Errorf("local type ID: %w", err) } local = Copy(local, UnderlyingType) bestScore := len(relos) var bestFixups []COREFixup for i := range targets { targetID, err := targetSpec.TypeID(targets[i]) if err != nil { return nil, fmt.Errorf("target type ID: %w", err) } target := Copy(targets[i], UnderlyingType) score := 0 // lower is better fixups := make([]COREFixup, 0, len(relos)) for _, relo := range relos { fixup, err := coreCalculateFixup(localSpec.byteOrder, local, localID, target, targetID, relo) if err != nil { return nil, fmt.Errorf("target %s: %w", target, err) } if fixup.poison || fixup.isNonExistant() { score++ } fixups = append(fixups, fixup) } if score > bestScore { // We have a better target already, ignore this one. continue } if score < bestScore { // This is the best target yet, use it. bestScore = score bestFixups = fixups continue } // Some other target has the same score as the current one. Make sure // the fixups agree with each other. for i, fixup := range bestFixups { if !fixup.equal(fixups[i]) { return nil, fmt.Errorf("%s: multiple types match: %w", fixup.kind, errAmbiguousRelocation) } } } if bestFixups == nil { // Nothing at all matched, probably because there are no suitable // targets at all. // // Poison everything except checksForExistence. bestFixups = make([]COREFixup, len(relos)) for i, relo := range relos { if relo.kind.checksForExistence() { bestFixups[i] = COREFixup{kind: relo.kind, local: 1, target: 0} } else { bestFixups[i] = COREFixup{kind: relo.kind, poison: true} } } } return bestFixups, nil } // coreCalculateFixup calculates the fixup for a single local type, target type // and relocation. func coreCalculateFixup(byteOrder binary.ByteOrder, local Type, localID TypeID, target Type, targetID TypeID, relo *CORERelocation) (COREFixup, error) { fixup := func(local, target uint32) (COREFixup, error) { return COREFixup{kind: relo.kind, local: local, target: target}, nil } fixupWithoutValidation := func(local, target uint32) (COREFixup, error) { return COREFixup{kind: relo.kind, local: local, target: target, skipLocalValidation: true}, nil } poison := func() (COREFixup, error) { if relo.kind.checksForExistence() { return fixup(1, 0) } return COREFixup{kind: relo.kind, poison: true}, nil } zero := COREFixup{} switch relo.kind { case reloTypeIDTarget, reloTypeSize, reloTypeExists: if len(relo.accessor) > 1 || relo.accessor[0] != 0 { return zero, fmt.Errorf("%s: unexpected accessor %v", relo.kind, relo.accessor) } err := coreAreTypesCompatible(local, target) if errors.Is(err, errImpossibleRelocation) { return poison() } if err != nil { return zero, fmt.Errorf("relocation %s: %w", relo.kind, err) } switch relo.kind { case reloTypeExists: return fixup(1, 1) case reloTypeIDTarget: return fixup(uint32(localID), uint32(targetID)) case reloTypeSize: localSize, err := Sizeof(local) if err != nil { return zero, err } targetSize, err := Sizeof(target) if err != nil { return zero, err } return fixup(uint32(localSize), uint32(targetSize)) } case reloEnumvalValue, reloEnumvalExists: localValue, targetValue, err := coreFindEnumValue(local, relo.accessor, target) if errors.Is(err, errImpossibleRelocation) { return poison() } if err != nil { return zero, fmt.Errorf("relocation %s: %w", relo.kind, err) } switch relo.kind { case reloEnumvalExists: return fixup(1, 1) case reloEnumvalValue: return fixup(uint32(localValue.Value), uint32(targetValue.Value)) } case reloFieldSigned: switch local.(type) { case *Enum: return fixup(1, 1) case *Int: return fixup( uint32(local.(*Int).Encoding&Signed), uint32(target.(*Int).Encoding&Signed), ) default: return fixupWithoutValidation(0, 0) } case reloFieldByteOffset, reloFieldByteSize, reloFieldExists, reloFieldLShiftU64, reloFieldRShiftU64: if _, ok := target.(*Fwd); ok { // We can't relocate fields using a forward declaration, so // skip it. If a non-forward declaration is present in the BTF // we'll find it in one of the other iterations. return poison() } localField, targetField, err := coreFindField(local, relo.accessor, target) if errors.Is(err, errImpossibleRelocation) { return poison() } if err != nil { return zero, fmt.Errorf("target %s: %w", target, err) } maybeSkipValidation := func(f COREFixup, err error) (COREFixup, error) { f.skipLocalValidation = localField.bitfieldSize > 0 return f, err } switch relo.kind { case reloFieldExists: return fixup(1, 1) case reloFieldByteOffset: return maybeSkipValidation(fixup(localField.offset, targetField.offset)) case reloFieldByteSize: localSize, err := Sizeof(localField.Type) if err != nil { return zero, err } targetSize, err := Sizeof(targetField.Type) if err != nil { return zero, err } return maybeSkipValidation(fixup(uint32(localSize), uint32(targetSize))) case reloFieldLShiftU64: var target uint32 if byteOrder == binary.LittleEndian { targetSize, err := targetField.sizeBits() if err != nil { return zero, err } target = uint32(64 - targetField.bitfieldOffset - targetSize) } else { loadWidth, err := Sizeof(targetField.Type) if err != nil { return zero, err } target = uint32(64 - Bits(loadWidth*8) + targetField.bitfieldOffset) } return fixupWithoutValidation(0, target) case reloFieldRShiftU64: targetSize, err := targetField.sizeBits() if err != nil { return zero, err } return fixupWithoutValidation(0, uint32(64-targetSize)) } } return zero, fmt.Errorf("relocation %s: %w", relo.kind, ErrNotSupported) } /* coreAccessor contains a path through a struct. It contains at least one index. * * The interpretation depends on the kind of the relocation. The following is * taken from struct bpf_core_relo in libbpf_internal.h: * * - for field-based relocations, string encodes an accessed field using * a sequence of field and array indices, separated by colon (:). It's * conceptually very close to LLVM's getelementptr ([0]) instruction's * arguments for identifying offset to a field. * - for type-based relocations, strings is expected to be just "0"; * - for enum value-based relocations, string contains an index of enum * value within its enum type; * * Example to provide a better feel. * * struct sample { * int a; * struct { * int b[10]; * }; * }; * * struct sample s = ...; * int x = &s->a; // encoded as "0:0" (a is field #0) * int y = &s->b[5]; // encoded as "0:1:0:5" (anon struct is field #1, * // b is field #0 inside anon struct, accessing elem #5) * int z = &s[10]->b; // encoded as "10:1" (ptr is used as an array) */ type coreAccessor []int func parseCOREAccessor(accessor string) (coreAccessor, error) { if accessor == "" { return nil, fmt.Errorf("empty accessor") } parts := strings.Split(accessor, ":") result := make(coreAccessor, 0, len(parts)) for _, part := range parts { // 31 bits to avoid overflowing int on 32 bit platforms. index, err := strconv.ParseUint(part, 10, 31) if err != nil { return nil, fmt.Errorf("accessor index %q: %s", part, err) } result = append(result, int(index)) } return result, nil } func (ca coreAccessor) String() string { strs := make([]string, 0, len(ca)) for _, i := range ca { strs = append(strs, strconv.Itoa(i)) } return strings.Join(strs, ":") } func (ca coreAccessor) enumValue(t Type) (*EnumValue, error) { e, ok := t.(*Enum) if !ok { return nil, fmt.Errorf("not an enum: %s", t) } if len(ca) > 1 { return nil, fmt.Errorf("invalid accessor %s for enum", ca) } i := ca[0] if i >= len(e.Values) { return nil, fmt.Errorf("invalid index %d for %s", i, e) } return &e.Values[i], nil } // coreField represents the position of a "child" of a composite type from the // start of that type. // // /- start of composite // | offset * 8 | bitfieldOffset | bitfieldSize | ... | // \- start of field end of field -/ type coreField struct { Type Type // The position of the field from the start of the composite type in bytes. offset uint32 // The offset of the bitfield in bits from the start of the field. bitfieldOffset Bits // The size of the bitfield in bits. // // Zero if the field is not a bitfield. bitfieldSize Bits } func (cf *coreField) adjustOffsetToNthElement(n int) error { size, err := Sizeof(cf.Type) if err != nil { return err } cf.offset += uint32(n) * uint32(size) return nil } func (cf *coreField) adjustOffsetBits(offset Bits) error { align, err := alignof(cf.Type) if err != nil { return err } // We can compute the load offset by: // 1) converting the bit offset to bytes with a flooring division. // 2) dividing and multiplying that offset by the alignment, yielding the // load size aligned offset. offsetBytes := uint32(offset/8) / uint32(align) * uint32(align) // The number of bits remaining is the bit offset less the number of bits // we can "skip" with the aligned offset. cf.bitfieldOffset = offset - Bits(offsetBytes*8) // We know that cf.offset is aligned at to at least align since we get it // from the compiler via BTF. Adding an aligned offsetBytes preserves the // alignment. cf.offset += offsetBytes return nil } func (cf *coreField) sizeBits() (Bits, error) { if cf.bitfieldSize > 0 { return cf.bitfieldSize, nil } // Someone is trying to access a non-bitfield via a bit shift relocation. // This happens when a field changes from a bitfield to a regular field // between kernel versions. Synthesise the size to make the shifts work. size, err := Sizeof(cf.Type) if err != nil { return 0, nil } return Bits(size * 8), nil } // coreFindField descends into the local type using the accessor and tries to // find an equivalent field in target at each step. // // Returns the field and the offset of the field from the start of // target in bits. func coreFindField(localT Type, localAcc coreAccessor, targetT Type) (coreField, coreField, error) { local := coreField{Type: localT} target := coreField{Type: targetT} // The first index is used to offset a pointer of the base type like // when accessing an array. if err := local.adjustOffsetToNthElement(localAcc[0]); err != nil { return coreField{}, coreField{}, err } if err := target.adjustOffsetToNthElement(localAcc[0]); err != nil { return coreField{}, coreField{}, err } if err := coreAreMembersCompatible(local.Type, target.Type); err != nil { return coreField{}, coreField{}, fmt.Errorf("fields: %w", err) } var localMaybeFlex, targetMaybeFlex bool for i, acc := range localAcc[1:] { switch localType := local.Type.(type) { case composite: // For composite types acc is used to find the field in the local type, // and then we try to find a field in target with the same name. localMembers := localType.members() if acc >= len(localMembers) { return coreField{}, coreField{}, fmt.Errorf("invalid accessor %d for %s", acc, localType) } localMember := localMembers[acc] if localMember.Name == "" { _, ok := localMember.Type.(composite) if !ok { return coreField{}, coreField{}, fmt.Errorf("unnamed field with type %s: %s", localMember.Type, ErrNotSupported) } // This is an anonymous struct or union, ignore it. local = coreField{ Type: localMember.Type, offset: local.offset + localMember.Offset.Bytes(), } localMaybeFlex = false continue } targetType, ok := target.Type.(composite) if !ok { return coreField{}, coreField{}, fmt.Errorf("target not composite: %w", errImpossibleRelocation) } targetMember, last, err := coreFindMember(targetType, localMember.Name) if err != nil { return coreField{}, coreField{}, err } local = coreField{ Type: localMember.Type, offset: local.offset, bitfieldSize: localMember.BitfieldSize, } localMaybeFlex = acc == len(localMembers)-1 target = coreField{ Type: targetMember.Type, offset: target.offset, bitfieldSize: targetMember.BitfieldSize, } targetMaybeFlex = last if local.bitfieldSize == 0 && target.bitfieldSize == 0 { local.offset += localMember.Offset.Bytes() target.offset += targetMember.Offset.Bytes() break } // Either of the members is a bitfield. Make sure we're at the // end of the accessor. if next := i + 1; next < len(localAcc[1:]) { return coreField{}, coreField{}, fmt.Errorf("can't descend into bitfield") } if err := local.adjustOffsetBits(localMember.Offset); err != nil { return coreField{}, coreField{}, err } if err := target.adjustOffsetBits(targetMember.Offset); err != nil { return coreField{}, coreField{}, err } case *Array: // For arrays, acc is the index in the target. targetType, ok := target.Type.(*Array) if !ok { return coreField{}, coreField{}, fmt.Errorf("target not array: %w", errImpossibleRelocation) } if localType.Nelems == 0 && !localMaybeFlex { return coreField{}, coreField{}, fmt.Errorf("local type has invalid flexible array") } if targetType.Nelems == 0 && !targetMaybeFlex { return coreField{}, coreField{}, fmt.Errorf("target type has invalid flexible array") } if localType.Nelems > 0 && acc >= int(localType.Nelems) { return coreField{}, coreField{}, fmt.Errorf("invalid access of %s at index %d", localType, acc) } if targetType.Nelems > 0 && acc >= int(targetType.Nelems) { return coreField{}, coreField{}, fmt.Errorf("out of bounds access of target: %w", errImpossibleRelocation) } local = coreField{ Type: localType.Type, offset: local.offset, } localMaybeFlex = false if err := local.adjustOffsetToNthElement(acc); err != nil { return coreField{}, coreField{}, err } target = coreField{ Type: targetType.Type, offset: target.offset, } targetMaybeFlex = false if err := target.adjustOffsetToNthElement(acc); err != nil { return coreField{}, coreField{}, err } default: return coreField{}, coreField{}, fmt.Errorf("relocate field of %T: %w", localType, ErrNotSupported) } if err := coreAreMembersCompatible(local.Type, target.Type); err != nil { return coreField{}, coreField{}, err } } return local, target, nil } // coreFindMember finds a member in a composite type while handling anonymous // structs and unions. func coreFindMember(typ composite, name string) (Member, bool, error) { if name == "" { return Member{}, false, errors.New("can't search for anonymous member") } type offsetTarget struct { composite offset Bits } targets := []offsetTarget{{typ, 0}} visited := make(map[composite]bool) for i := 0; i < len(targets); i++ { target := targets[i] // Only visit targets once to prevent infinite recursion. if visited[target] { continue } if len(visited) >= maxTypeDepth { // This check is different than libbpf, which restricts the entire // path to BPF_CORE_SPEC_MAX_LEN items. return Member{}, false, fmt.Errorf("type is nested too deep") } visited[target] = true members := target.members() for j, member := range members { if member.Name == name { // NB: This is safe because member is a copy. member.Offset += target.offset return member, j == len(members)-1, nil } // The names don't match, but this member could be an anonymous struct // or union. if member.Name != "" { continue } comp, ok := member.Type.(composite) if !ok { return Member{}, false, fmt.Errorf("anonymous non-composite type %T not allowed", member.Type) } targets = append(targets, offsetTarget{comp, target.offset + member.Offset}) } } return Member{}, false, fmt.Errorf("no matching member: %w", errImpossibleRelocation) } // coreFindEnumValue follows localAcc to find the equivalent enum value in target. func coreFindEnumValue(local Type, localAcc coreAccessor, target Type) (localValue, targetValue *EnumValue, _ error) { localValue, err := localAcc.enumValue(local) if err != nil { return nil, nil, err } targetEnum, ok := target.(*Enum) if !ok { return nil, nil, errImpossibleRelocation } localName := newEssentialName(localValue.Name) for i, targetValue := range targetEnum.Values { if newEssentialName(targetValue.Name) != localName { continue } return localValue, &targetEnum.Values[i], nil } return nil, nil, errImpossibleRelocation } /* The comment below is from bpf_core_types_are_compat in libbpf.c: * * Check local and target types for compatibility. This check is used for * type-based CO-RE relocations and follow slightly different rules than * field-based relocations. This function assumes that root types were already * checked for name match. Beyond that initial root-level name check, names * are completely ignored. Compatibility rules are as follows: * - any two STRUCTs/UNIONs/FWDs/ENUMs/INTs are considered compatible, but * kind should match for local and target types (i.e., STRUCT is not * compatible with UNION); * - for ENUMs, the size is ignored; * - for INT, size and signedness are ignored; * - for ARRAY, dimensionality is ignored, element types are checked for * compatibility recursively; * - CONST/VOLATILE/RESTRICT modifiers are ignored; * - TYPEDEFs/PTRs are compatible if types they pointing to are compatible; * - FUNC_PROTOs are compatible if they have compatible signature: same * number of input args and compatible return and argument types. * These rules are not set in stone and probably will be adjusted as we get * more experience with using BPF CO-RE relocations. * * Returns errImpossibleRelocation if types are not compatible. */ func coreAreTypesCompatible(localType Type, targetType Type) error { var ( localTs, targetTs typeDeque l, t = &localType, &targetType depth = 0 ) for ; l != nil && t != nil; l, t = localTs.shift(), targetTs.shift() { if depth >= maxTypeDepth { return errors.New("types are nested too deep") } localType = *l targetType = *t if reflect.TypeOf(localType) != reflect.TypeOf(targetType) { return fmt.Errorf("type mismatch: %w", errImpossibleRelocation) } switch lv := (localType).(type) { case *Void, *Struct, *Union, *Enum, *Fwd, *Int: // Nothing to do here case *Pointer, *Array: depth++ localType.walk(&localTs) targetType.walk(&targetTs) case *FuncProto: tv := targetType.(*FuncProto) if len(lv.Params) != len(tv.Params) { return fmt.Errorf("function param mismatch: %w", errImpossibleRelocation) } depth++ localType.walk(&localTs) targetType.walk(&targetTs) default: return fmt.Errorf("unsupported type %T", localType) } } if l != nil { return fmt.Errorf("dangling local type %T", *l) } if t != nil { return fmt.Errorf("dangling target type %T", *t) } return nil } /* coreAreMembersCompatible checks two types for field-based relocation compatibility. * * The comment below is from bpf_core_fields_are_compat in libbpf.c: * * Check two types for compatibility for the purpose of field access * relocation. const/volatile/restrict and typedefs are skipped to ensure we * are relocating semantically compatible entities: * - any two STRUCTs/UNIONs are compatible and can be mixed; * - any two FWDs are compatible, if their names match (modulo flavor suffix); * - any two PTRs are always compatible; * - for ENUMs, names should be the same (ignoring flavor suffix) or at * least one of enums should be anonymous; * - for ENUMs, check sizes, names are ignored; * - for INT, size and signedness are ignored; * - any two FLOATs are always compatible; * - for ARRAY, dimensionality is ignored, element types are checked for * compatibility recursively; * [ NB: coreAreMembersCompatible doesn't recurse, this check is done * by coreFindField. ] * - everything else shouldn't be ever a target of relocation. * These rules are not set in stone and probably will be adjusted as we get * more experience with using BPF CO-RE relocations. * * Returns errImpossibleRelocation if the members are not compatible. */ func coreAreMembersCompatible(localType Type, targetType Type) error { doNamesMatch := func(a, b string) error { if a == "" || b == "" { // allow anonymous and named type to match return nil } if newEssentialName(a) == newEssentialName(b) { return nil } return fmt.Errorf("names don't match: %w", errImpossibleRelocation) } _, lok := localType.(composite) _, tok := targetType.(composite) if lok && tok { return nil } if reflect.TypeOf(localType) != reflect.TypeOf(targetType) { return fmt.Errorf("type mismatch: %w", errImpossibleRelocation) } switch lv := localType.(type) { case *Array, *Pointer, *Float, *Int: return nil case *Enum: tv := targetType.(*Enum) return doNamesMatch(lv.Name, tv.Name) case *Fwd: tv := targetType.(*Fwd) return doNamesMatch(lv.Name, tv.Name) default: return fmt.Errorf("type %s: %w", localType, ErrNotSupported) } }