Operations 8 min read

eBPF Talk: Who Modified My BPF Map?

This article demonstrates how to use eBPF together with BTF to trace BPF map update and delete functions, shows concrete command‑line output, explains the code that identifies target kernel functions, and details the data‑dumping logic for debugging map contents.

Linux Kernel Journey
Linux Kernel Journey
Linux Kernel Journey
eBPF Talk: Who Modified My BPF Map?

Purpose

The goal is to track updates and deletions of BPF maps using eBPF, thereby revealing which process or program modifies a given map.

TL;DR

Implemented tracing of BPF map update/delete functions; batch‑operation tracing is not covered. Source code: github.com/Asphaltt/mad.

Implementation Effect

Running the tool produces output such as:

# ./mad
2024/11/17 07:06:47 mad is running ..
0: map(5755:.rodata:Array) is updated by process(76052:pwru)
key: 00000000 [4 bytes]

0: map(5755:.rodata:Array) is updated by process(76052:pwru)
value: {".rodata":[{"CFG":{"netns":0,"mark":0,"ifindex":0,"output_meta":0x1,"output_tuple":0x1,"output_skb":0x0,"output_shinfo":0x1,"output_stack":0x0,"output_caller":0x0,"output_unused":0x0,"is_set":0x1,"track_skb":0x0,"track_skb_by_stackid":0x0,"track_xdp":0x1,"unused":0x0,"skb_btf_id":1875,"shinfo_btf_id":21388}}, {"TRUE":true}, {"ZERO":0}, {"BPF_PROG_ADDR":0}]}

Even string values are displayed correctly.

Identifying Target Functions via BTF

The program does not hard‑code kernel functions; it walks the BTF spec and selects functions whose names end with _map_update_elem or _map_delete_elem and whose parameters match the expected signatures.

func isTgtFunc(typ btf.Type) (string, bool) {
    fn, ok := typ.(*btf.Func)
    if !ok {
        return "", false
    }
    fnName := fn.Name
    fnProto := fn.Type.(*btf.FuncProto)
    if strings.HasSuffix(fnName, "_map_update_elem") && len(fnProto.Params) == 4 {
        if isBpfMap(fnProto.Params[0].Type) && isVoid(fnProto.Params[1].Type) && isVoid(fnProto.Params[2].Type) {
            return fnName, true
        }
    } else if strings.HasSuffix(fnName, "_map_delete_elem") && len(fnProto.Params) == 2 {
        if isBpfMap(fnProto.Params[0].Type) && isVoid(fnProto.Params[1].Type) {
            return fnName, true
        }
    }
    return "", false
}

func retrieveHooks(spec *btf.Spec) ([]string, error) {
    var hooks []string
    iter := spec.Iterate()
    for iter.Next() {
        if name, ok := isTgtFunc(iter.Type); ok {
            hooks = append(hooks, name)
        }
    }
    return hooks, nil
}

Algorithm Steps

Iterate over the entire BTF specification.

Check whether each BTF type is a function.

Determine if the function is an update‑map function by matching the _map_update_elem suffix and verifying its four parameters.

Determine if the function is a delete‑map function by matching the _map_delete_elem suffix and verifying its two parameters.

Parsing KV Data with bpftool

Using bpftool m d i 5672 reveals the key/value layout of a map, for example:

# bpftool m d i 5672
[{"value":{ ".rodata":[{"CFG":{"netns":0,"mark":0,"ifindex":0,"output_meta":0x1,"output_tuple":0x1,"output_skb":0x0,"output_shinfo":0x1,"output_stack":0x0,"output_caller":0x0,"output_unused":0x0,"is_set":0x1,"track_skb":0x0,"track_skb_by_stackid":0x0,"track_xdp":0x1,"unused":0x0,"skb_btf_id":1875,"shinfo_btf_id":21388}}, {"TRUE":true}, {"ZERO":0}, {"BPF_PROG_ADDR":0}]}}

The same effect can be reproduced inside mad by re‑implementing the relevant bpftool logic.

Dumping Data Based on BTF Types

The following function walks a BTF type and prints its contents according to the concrete kind (int, struct, union, array, etc.). This mirrors the behaviour of bpftool but is tailored for the tracing tool.

func (dd *dataDumper) dumpData(typ btf.Type, bits bitsInfo, data []byte) error {
    switch v := typ.(type) {
    case *btf.Int:
        dd.dumpInt(v, bits, data)
    case *btf.Struct:
        dd.dumpStruct(v, data)
    case *btf.Union:
        dd.dumpUnion(v, data)
    case *btf.Array:
        dd.dumpArray(v, data)
    case *btf.Enum:
        dd.dumpEnum(v, data)
    case *btf.Pointer:
        dd.dumpPointer(v, data)
    case *btf.Fwd:
        dd.print("(fwd-kind-invalid)")
        return fmt.Errorf("fwd kind invalid")
    case *btf.Typedef:
        return dd.dumpData(v.Type, bits, data)
    case *btf.Volatile:
        return dd.dumpData(v.Type, bits, data)
    case *btf.Const:
        return dd.dumpData(v.Type, bits, data)
    case *btf.Restrict:
        return dd.dumpData(v.Type, bits, data)
    case *btf.Var:
        dd.dumpVar(v, bits, data)
    case *btf.Datasec:
        return dd.dumpDataSec(v, data)
    default:
        dd.print("(unsupported-kind)")
        return fmt.Errorf("unsupported kind %T", v)
    }
    return nil
}

Conclusion

By leveraging BTF metadata, the tool can precisely display the key/value details of BPF map updates and deletions, which greatly aids debugging and root‑cause analysis of kernel‑level tracing scenarios.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

debuggingeBPFkernel tracingBTFBPF map
Linux Kernel Journey
Written by

Linux Kernel Journey

Linux Kernel Journey

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.