Re: [PATCH bpf-next v1 4/8] bpf/libbpf: BTF support for typed ksyms

From: Hao Luo
Date: Thu Aug 27 2020 - 18:29:48 EST


On Fri, Aug 21, 2020 at 3:37 PM Andrii Nakryiko
<andrii.nakryiko@xxxxxxxxx> wrote:
>
> On Wed, Aug 19, 2020 at 3:42 PM Hao Luo <haoluo@xxxxxxxxxx> wrote:
> >
> > If a ksym is defined with a type, libbpf will try to find the ksym's btf
> > information from kernel btf. If a valid btf entry for the ksym is found,
> > libbpf can pass in the found btf id to the verifier, which validates the
> > ksym's type and value.
> >
> > Typeless ksyms (i.e. those defined as 'void') will not have such btf_id,
> > but it has the symbol's address (read from kallsyms) and its value is
> > treated as a raw pointer.
> >
> > Signed-off-by: Hao Luo <haoluo@xxxxxxxxxx>
> > ---
> > tools/lib/bpf/libbpf.c | 130 ++++++++++++++++++++++++++++++++++++-----
> > 1 file changed, 114 insertions(+), 16 deletions(-)
> >
> > diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> > index 4a81c6b2d21b..94eff612c7c2 100644
> > --- a/tools/lib/bpf/libbpf.c
> > +++ b/tools/lib/bpf/libbpf.c
> > @@ -357,7 +357,16 @@ struct extern_desc {
> > bool is_signed;
> > } kcfg;
> > struct {
> > - unsigned long long addr;
> > + /*
> > + * 1. If ksym is typeless, the field 'addr' is valid.
> > + * 2. If ksym is typed, the field 'vmlinux_btf_id' is
> > + * valid.
> > + */
> > + bool is_typeless;
> > + union {
> > + unsigned long long addr;
> > + int vmlinux_btf_id;
> > + };
>
> ksym is 16 bytes anyways, union doesn't help to save space. I propose
> to encode all this with just two fields: vmlinux_btf_id and addr. If
> btf_id == 0, then extern is typeless.

Ack on expanding the union. But I slightly preferred keeping
is_typeless. IIUC, btf_id points a VAR_KIND, we need the following
pointer chasing every time

t = btf__type_by_id(obj->btf, ext->btf_id);
t->type;

which I felt is worse than keeping a is_typeless flag.

>
> > } ksym;
> > };
> > };
> > @@ -382,6 +391,7 @@ struct bpf_object {
> >
> > bool loaded;
> > bool has_pseudo_calls;
> > + bool has_typed_ksyms;
> >
> > /*
> > * Information when doing elf related work. Only valid if fd
> > @@ -2521,6 +2531,10 @@ static int bpf_object__load_vmlinux_btf(struct bpf_object *obj)
> > if (obj->btf_ext && obj->btf_ext->core_relo_info.len)
> > need_vmlinux_btf = true;
> >
> > + /* Support for typed ksyms needs kernel BTF */
> > + if (obj->has_typed_ksyms)
> > + need_vmlinux_btf = true;
>
> On the second read, I don't think you really need `has_typed_ksyms` at
> all. Just iterate over ksym externs and see if you have a typed one.
> It's the only place that cares.
>

Ack. Will iterate over ksym externs here.

> > +
> > bpf_object__for_each_program(prog, obj) {
> > if (!prog->load)
> > continue;
> > @@ -2975,10 +2989,10 @@ static int bpf_object__collect_externs(struct bpf_object *obj)
> > ext->type = EXT_KSYM;
> >
> > vt = skip_mods_and_typedefs(obj->btf, t->type, NULL);
> > - if (!btf_is_void(vt)) {
> > - pr_warn("extern (ksym) '%s' is not typeless (void)\n", ext_name);
> > - return -ENOTSUP;
> > - }
> > + ext->ksym.is_typeless = btf_is_void(vt);
> > +
> > + if (!obj->has_typed_ksyms && !ext->ksym.is_typeless)
> > + obj->has_typed_ksyms = true;
>
> nit: keep it simple:
>
> if (ext->ksym.is_typeless)
> obj->has_typed_ksyms = true;
>

Ack.

> > } else {
> > pr_warn("unrecognized extern section '%s'\n", sec_name);
> > return -ENOTSUP;
> > @@ -2992,9 +3006,9 @@ static int bpf_object__collect_externs(struct bpf_object *obj)
> > /* sort externs by type, for kcfg ones also by (align, size, name) */
> > qsort(obj->externs, obj->nr_extern, sizeof(*ext), cmp_externs);
> >
> > - /* for .ksyms section, we need to turn all externs into allocated
> > - * variables in BTF to pass kernel verification; we do this by
> > - * pretending that each extern is a 8-byte variable
> > + /* for .ksyms section, we need to turn all typeless externs into
> > + * allocated variables in BTF to pass kernel verification; we do
> > + * this by pretending that each typeless extern is a 8-byte variable
> > */
> > if (ksym_sec) {
> > /* find existing 4-byte integer type in BTF to use for fake
> > @@ -3012,7 +3026,7 @@ static int bpf_object__collect_externs(struct bpf_object *obj)
> >
> > sec = ksym_sec;
> > n = btf_vlen(sec);
> > - for (i = 0, off = 0; i < n; i++, off += sizeof(int)) {
> > + for (i = 0, off = 0; i < n; i++) {
> > struct btf_var_secinfo *vs = btf_var_secinfos(sec) + i;
> > struct btf_type *vt;
> >
> > @@ -3025,9 +3039,14 @@ static int bpf_object__collect_externs(struct bpf_object *obj)
> > return -ESRCH;
> > }
> > btf_var(vt)->linkage = BTF_VAR_GLOBAL_ALLOCATED;
> > - vt->type = int_btf_id;
> > + if (ext->ksym.is_typeless) {
> > + vt->type = int_btf_id;
> > + vs->size = sizeof(int);
> > + }
> > vs->offset = off;
> > - vs->size = sizeof(int);
> > + off += vs->size;
> > + pr_debug("ksym var_secinfo: var '%s', type #%d, size %d, offset %d\n",
> > + ext->name, vt->type, vs->size, vs->offset);
>
> It's a bit of a waste that we still allocate memory for those typed
> ksym externs, as they don't really need space. But modifying BTF is a
> pain right now, so I think we'll have to do it, until we have a better
> BTF API. But let's make them integers for now to take a fixed and
> small amount of space.
>

Do you mean making typed ksym externs of type integer? If so, we can't
do that, I think. After collect_externs, we later need to compare the
declared extern's type against the type defined in kernel. Better not
rewrite their types in BTf.

I am generally against modifying BTF. I initially didn't notice that
all the ksym externs' types are modified to 'int' and the type
comparison I mentioned above always failed. I dumped the btf in
vmlinux and the btf in object file, checked the kernel variable's
source code, printed out everything I could. The experience was very
bad.

> > }
> > sec->size = off;
> > }
> > @@ -5300,8 +5319,13 @@ bpf_program__relocate(struct bpf_program *prog, struct bpf_object *obj)
> > insn[0].imm = obj->maps[obj->kconfig_map_idx].fd;
> > insn[1].imm = ext->kcfg.data_off;
> > } else /* EXT_KSYM */ {
> > - insn[0].imm = (__u32)ext->ksym.addr;
> > - insn[1].imm = ext->ksym.addr >> 32;
> > + if (ext->ksym.is_typeless) { /* typelss ksyms */
>
> typo: typeless
>

Ack.

> > + insn[0].imm = (__u32)ext->ksym.addr;
> > + insn[1].imm = ext->ksym.addr >> 32;
> > + } else { /* typed ksyms */
> > + insn[0].src_reg = BPF_PSEUDO_BTF_ID;
> > + insn[0].imm = ext->ksym.vmlinux_btf_id;
> > + }
> > }
> > break;
> > case RELO_CALL:
> > @@ -6019,6 +6043,10 @@ static int bpf_object__read_kallsyms_file(struct bpf_object *obj)
> > if (!ext || ext->type != EXT_KSYM)
> > continue;
> >
> > + /* Typed ksyms have the verifier to fill their addresses. */
> > + if (!ext->ksym.is_typeless)
> > + continue;
>
> It might still be a good idea to try to find the symbol in kallsyms
> and emit nicer message if it's not there. Think about the user's
> experience when some global variable is removed from the kernel (or
> compiled out due to missing Kconfig). If libbpf can easily detect
> this, we should do it and provide a good error message.
>

Ack.

> > +
> > if (ext->is_set && ext->ksym.addr != sym_addr) {
> > pr_warn("extern (ksym) '%s' resolution is ambiguous: 0x%llx or 0x%llx\n",
> > sym_name, ext->ksym.addr, sym_addr);
> > @@ -6037,10 +6065,72 @@ static int bpf_object__read_kallsyms_file(struct bpf_object *obj)
> > return err;
> > }
> >
> > +static int bpf_object__resolve_ksyms_btf_id(struct bpf_object *obj)
> > +{
> > + struct extern_desc *ext;
> > + int i, id;
> > +
> > + if (!obj->btf_vmlinux) {
> > + pr_warn("support of typed ksyms needs kernel btf.\n");
> > + return -ENOENT;
> > + }
> > +
> > + for (i = 0; i < obj->nr_extern; i++) {
> > + const struct btf_type *v, *vx; /* VARs in object and vmlinux btf */
> > + const struct btf_type *t, *tx; /* TYPEs in btf */
> > + __u32 vt, vtx; /* btf_ids of TYPEs */
>
> I use targ_ and local_ prefixes with CO-RE to distinguish something
> that's coming from BPF object's BTF vs kernel's BTF, can you please
> adopt the same, as the process is essentially the same. "vx" vs "vtx"
> vs "v" are hard to distinguish and understand.
>

No problem. Will follow the convention.

> > +
> > + ext = &obj->externs[i];
> > + if (ext->type != EXT_KSYM)
> > + continue;
> > +
> > + if (ext->ksym.is_typeless)
> > + continue;
>
> nit: combine into a single filter condition (we need typed ksym,
> that's one check)
>

Ack.

> > +
> > + if (ext->is_set) {
> > + pr_warn("typed ksym '%s' resolved as typeless ksyms.\n",
> > + ext->name);
> > + return -EFAULT;
>
> this is a bug if that happens, that should be caught by test, please
> remove unnecessary check
>

Ack.

> > + }
> > +
> > + id = btf__find_by_name_kind(obj->btf_vmlinux, ext->name,
> > + BTF_KIND_VAR);
> > + if (id <= 0) {
> > + pr_warn("no btf entry for ksym '%s' in vmlinux.\n",
> > + ext->name);
> > + return -ESRCH;
> > + }
> > +
> > + vx = btf__type_by_id(obj->btf_vmlinux, id);
> > + tx = skip_mods_and_typedefs(obj->btf_vmlinux, vx->type, &vtx);
> > +
> > + v = btf__type_by_id(obj->btf, ext->btf_id);
> > + t = skip_mods_and_typedefs(obj->btf, v->type, &vt);
> > +
> > + if (!btf_ksym_type_match(obj->btf_vmlinux, vtx, obj->btf, vt)) {
> > + const char *tname, *txname; /* names of TYPEs */
> > +
> > + txname = btf__name_by_offset(obj->btf_vmlinux, tx->name_off);
> > + tname = btf__name_by_offset(obj->btf, t->name_off);
> > +
> > + pr_warn("ksym '%s' expects type '%s' (vmlinux_btf_id: #%d), "
> > + "but got '%s' (btf_id: #%d)\n", ext->name,
> > + txname, vtx, tname, vt);
> > + return -EINVAL;
> > + }
>
> yeah, definitely just use bpf_core_types_are_compat() here. You'll
> want to skip_mods_and_typedefs first, but everything else should work
> for your use case.
>

Ack. bpf_core_types_are_compat() is indeed a perfect fit here.

[...]