From: Yuan Chen <[email protected]>

bpf_ma_set_dtor() duplicates the map's btf_record for the bpf_mem_alloc
destructor. For kptr fields backed by the program BTF (MEM_ALLOC kptrs,
e.g. objects allocated with bpf_obj_new()/bpf_percpu_obj_new()),
btf_record_dup() only borrows the reference, matching what
btf_parse_fields() did for the map's own record.

The duplicated record, however, is released later from the deferred
bpf_mem_alloc destructor workqueue (free_mem_alloc_deferred), by which
time the program BTF may already have been freed: bpf_map_free() drops
the map's own reference, and the RCU callback can run before the
workqueue. Reading field->kptr.btf in btf_record_free() (via
btf_is_kernel()) is then a use-after-free, detected by KASAN as
"slab-use-after-free in btf_is_kernel" when a map with a MEM_ALLOC kptr
field is destroyed.

Hold a reference on program BTF for the lifetime of the duplicated
record and drop it right before the record is freed. The last btf_put()
only schedules the object for RCU destruction, so btf_record_free() can
still safely read the field descriptors.

The rhtab kptr selftests exercise this path on every map teardown and
triggered the bug under KASAN; with this fix they pass cleanly.

Fixes: 1df97a7453ee ("bpf: Register dtor for freeing special fields")
Signed-off-by: Yuan Chen <[email protected]>
---
 kernel/bpf/hashtab.c | 45 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/kernel/bpf/hashtab.c b/kernel/bpf/hashtab.c
index 0df8db27cd8c..b8df2bc9a9a0 100644
--- a/kernel/bpf/hashtab.c
+++ b/kernel/bpf/hashtab.c
@@ -493,10 +493,54 @@ static void htab_pcpu_mem_dtor(void *obj, void *ctx)
                bpf_obj_free_fields(hrec->record, per_cpu_ptr(pptr, cpu));
 }
 
+/*
+ * bpf_ma_set_dtor() duplicates the map's btf_record. For kptr fields whose
+ * btf is the program BTF (MEM_ALLOC kptrs, e.g. objects allocated with
+ * bpf_obj_new()/bpf_percpu_obj_new()) btf_record_dup() only borrows the
+ * reference, like btf_parse_fields() did for the map's own record. The
+ * duplicated record is released later from the deferred bpf_mem_alloc
+ * destructor workqueue, by which time the program BTF may already have been
+ * freed (the map dropped its own reference in bpf_map_free()), so reading
+ * field->kptr.btf there would be a use-after-free.
+ *
+ * Hold a reference on non-kernel (program) BTF for the lifetime of the
+ * duplicated record and release it before the record is freed. After the
+ * last btf_put() the object is only destroyed after an RCU grace period, so
+ * btf_record_free() can still safely read the field descriptors.
+ */
+static void htab_record_prog_btf_ref(struct btf_record *rec, bool get)
+{
+       int i;
+
+       if (IS_ERR_OR_NULL(rec))
+               return;
+
+       for (i = 0; i < rec->cnt; i++) {
+               const struct btf_field *field = &rec->fields[i];
+
+               switch (field->type) {
+               case BPF_KPTR_UNREF:
+               case BPF_KPTR_REF:
+               case BPF_KPTR_PERCPU:
+               case BPF_UPTR:
+                       if (field->kptr.btf && !btf_is_kernel(field->kptr.btf)) 
{
+                               if (get)
+                                       btf_get(field->kptr.btf);
+                               else
+                                       btf_put(field->kptr.btf);
+                       }
+                       break;
+               default:
+                       break;
+               }
+       }
+}
+
 static void htab_dtor_ctx_free(void *ctx)
 {
        struct htab_btf_record *hrec = ctx;
 
+       htab_record_prog_btf_ref(hrec->record, false);
        btf_record_free(hrec->record);
        kfree(ctx);
 }
@@ -521,6 +565,7 @@ static int bpf_ma_set_dtor(struct bpf_map *map, struct 
bpf_mem_alloc *ma,
                kfree(hrec);
                return err;
        }
+       htab_record_prog_btf_ref(hrec->record, true);
        bpf_mem_alloc_set_dtor(ma, dtor, htab_dtor_ctx_free, hrec);
        return 0;
 }
-- 
2.54.0


Reply via email to