Skip to content

Commit

Permalink
cilium-migrate-map: port from from C to Go
Browse files Browse the repository at this point in the history
Currently initializing bpf programs and migrating maps is done by a C program
implementing an ELF parser. This patch introduces a Go version of the program
where cilium/ebpf does the heavy lifting instead. The feature is now also
exposed as a Go API, making it callable from other parts of the agent.
This version also parses and verifies the ELF's BTF section.

A CLI wrapper is temporarily included to make it callable from init.sh while
it is translated to Go.

Signed-off-by: Nate Sweet <nathanjsweet@pm.me>
Co-authored-by: Timo Beckers <timo@isovalent.com>
  • Loading branch information
2 people authored and borkmann committed Oct 14, 2021
1 parent 22dd886 commit 4305f92
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 4 deletions.
8 changes: 4 additions & 4 deletions bpf/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,12 @@ function bpf_load()
bpf_compile $IN $OUT obj "$OPTS"
tc qdisc replace dev $DEV clsact || true
[ -z "$(tc filter show dev $DEV $WHERE | grep -v 'pref 1 bpf chain 0 $\|pref 1 bpf chain 0 handle 0x1')" ] || tc filter del dev $DEV $WHERE
cilium-map-migrate -s $OUT
cilium bpf migrate-maps -s $OUT
set +e
tc filter replace dev $DEV $WHERE prio 1 handle 1 bpf da obj $OUT sec $SEC
RETCODE=$?
set -e
cilium-map-migrate -e $OUT -r $RETCODE
cilium bpf migrate-maps -e $OUT -r $RETCODE
return $RETCODE
}

Expand All @@ -286,12 +286,12 @@ function bpf_load_cgroups()
TMP_FILE="$BPFMNT/tc/globals/cilium_cgroups_$WHERE"
rm -f $TMP_FILE

cilium-map-migrate -s $OUT
cilium bpf migrate-maps -s $OUT
set +e
tc exec bpf pin $TMP_FILE obj $OUT type $PROG_TYPE attach_type $WHERE sec "cgroup/$WHERE"
RETCODE=$?
set -e
cilium-map-migrate -e $OUT -r $RETCODE
cilium bpf migrate-maps -e $OUT -r $RETCODE

if [ "$RETCODE" -eq "0" ]; then
set +e
Expand Down
76 changes: 76 additions & 0 deletions cilium/cmd/bpf_migrate_maps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2021 Authors of Cilium

package cmd

import (
"errors"
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"

"github.com/cilium/cilium/pkg/bpf"
"github.com/cilium/cilium/pkg/defaults"
)

func init() {
bpfCmd.AddCommand(bpfMigrateMapsCmd)
bpfMigrateMapsCmd.Flags().StringVarP(&start, "start", "s", "", "ELF file to start migrating maps for")
bpfMigrateMapsCmd.Flags().StringVarP(&end, "end", "e", "", "ELF file to finalize migrating maps for")
bpfMigrateMapsCmd.Flags().IntVarP(&rc, "return", "r", 0, "return code of the iproute2 command(s) executed between start and end")

// Allow configuring bpffs path using env variable, default to /sys/fs/bpf.
bpffsRoot := os.Getenv("TC_BPF_MNT")
if bpffsRoot == "" {
bpffsRoot = defaults.DefaultMapRoot
}

bpffsPath = filepath.Join(bpffsRoot, defaults.DefaultMapPrefix)
}

var (
bpffsPath string

start string
end string
rc int

bpfMigrateMapsCmd = &cobra.Command{
Use: "migrate-maps",
Hidden: true,
Short: "(hidden) Migrate an ELF file's map pins on bpffs",
Long: `
Migrate an ELF file's map pins on bpffs to :pending if the new map spec's
properties differ from the map that's currently pinned.
Use '-s <elf_file>' to re-pin any maps. Then, run any iproute2 commands
to load the new ELFs. Finish up by calling '-e <elf_file> -r <iproute_return_code>'.
If the return code is non-zero, the :pending maps will be moved back to their
original locations. If the return code is 0, the :pending maps will be unpinned.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Only allow one of start or end parameters.
if start == "" && end == "" {
return errors.New("either s or e must be a valid filepath")
}
if start != "" && end != "" {
return fmt.Errorf("s (%q) and e (%q) cannot be both set", start, end)
}

if start != "" {
if err := bpf.StartBPFFSMigration(bpffsPath, start); err != nil {
return fmt.Errorf("error starting map migration for %q: %v", start, err)
}
}

if end != "" {
if err := bpf.FinalizeBPFFSMigration(bpffsPath, end, rc != 0); err != nil {
return fmt.Errorf("error finalizing map migration for %q: %v", end, err)
}
}

return nil
},
}
)
188 changes: 188 additions & 0 deletions pkg/bpf/bpffs_migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package bpf

import (
"errors"
"fmt"
"io"
"path/filepath"

"encoding/binary"

"github.com/cilium/ebpf"
"golang.org/x/sys/unix"
)

const bpffsPending = ":pending"

// StartBPFFSMigration the map migration process for a given ELF's maps.
// When a new ELF contains a map definition that differs from its existing (pinned)
// counterpart, re-pin it to its current path suffixed by ':pending'.
// A map's type, key size, value size, flags and max entries are compared to the given spec.
//
// Takes a bpffsPath explicitly since it does not necessarily execute within
// the same runtime as the agent. It is imported from a Cilium cmd that takes
// its bpffs path from an env.
func StartBPFFSMigration(bpffsPath, elfPath string) error {
coll, err := ebpf.LoadCollectionSpec(elfPath)
if err != nil {
return err
}

for name, spec := range coll.Maps {
// Parse iproute2 bpf_elf_map's extra fields, if any.
if err := parseExtra(spec, coll); err != nil {
return fmt.Errorf("parsing extra bytes of ELF map definition %q:", name)
}

// Skip map specs without the pinning flag. Also takes care of skipping .data,
// .rodata and .bss.
if spec.Pinning == 0 {
continue
}

// Re-pin the map with ':pending' suffix if incoming spec differs from
// the currently-pinned map.
if err := repinMap(bpffsPath, name, spec); err != nil {
return err
}
}

return nil
}

// FinalizeBPFFSMigration finalizes the migration of an ELF's maps.
// If revert is true, any pending maps are re-pinned back to their original
// locations. If revert is false, any pending maps are unpinned (deleted).
//
// Takes a bpffsPath explicitly since it does not necessarily execute within
// the same runtime as the agent. It is imported from a Cilium cmd that takes
// its bpffs path from an env.
func FinalizeBPFFSMigration(bpffsPath, elfPath string, revert bool) error {
coll, err := ebpf.LoadCollectionSpec(elfPath)
if err != nil {
return err
}

for name, spec := range coll.Maps {
// Parse iproute2 bpf_elf_map's extra fields, if any.
if err := parseExtra(spec, coll); err != nil {
return fmt.Errorf("parsing extra bytes of ELF map definition %q:", name)
}

// Skip map specs without the pinning flag. Also takes care of skipping .data,
// .rodata and .bss.
// Don't unpin existing maps if their new versions are missing the pinning flag.
if spec.Pinning == 0 {
continue
}

if err := finalizeMap(bpffsPath, name, revert); err != nil {
return err
}
}

return nil
}

// parseExtra parses extra bytes that appear at the end of a struct bpf_elf_map.
// If the Extra field is empty, the function is a no-op.
//
// The library supports parsing `struct bpf_map_def` out of the box, but Cilium
// uses `struct bpf_elf_map` instead, which is bigger.
// The 'extra' bytes are exposed in the Map's Extra field, and appear in the
// following order (all u32): id, pinning, inner_id, inner_idx.
func parseExtra(spec *ebpf.MapSpec, coll *ebpf.CollectionSpec) error {
// Nothing to parse. This will be the case for BTF-style maps that have
// built-in support for pinning and map-in-map.
if spec.Extra.Len() == 0 {
return nil
}

// Discard the id as it's not needed.
if _, err := io.CopyN(io.Discard, &spec.Extra, 4); err != nil {
return fmt.Errorf("reading id field: %v", err)
}

// Read the pinning field.
var pinning uint32
if err := binary.Read(&spec.Extra, coll.ByteOrder, &pinning); err != nil {
return fmt.Errorf("reading pinning field: %v", err)
}
spec.Pinning = ebpf.PinType(pinning)

return nil
}

// repinMap opens a map from bpffs by its pin in '<bpffs>/tc/globals/',
// compares its properties against the incoming spec and re-pins it to
// ':pending' if any of its properties differ.
func repinMap(bpffsPath string, name string, spec *ebpf.MapSpec) error {
file := filepath.Join(bpffsPath, name)
pinned, err := ebpf.LoadPinnedMap(file, nil)

// Given map was not pinned, nothing to do.
if errors.Is(err, unix.ENOENT) {
return nil
}

if err != nil {
return fmt.Errorf("map not found at path %s: %v", name, err)
}

if pinned.Type() == spec.Type &&
pinned.KeySize() == spec.KeySize &&
pinned.ValueSize() == spec.ValueSize &&
pinned.Flags() == spec.Flags &&
pinned.MaxEntries() == spec.MaxEntries {
return nil
}

dest := file + bpffsPending

log.Infof("New version of map '%s' has different properties, re-pinning from '%s' to '%s'", name, file, dest)

// Atomically re-pin the map to the its new path.
if err := pinned.Pin(dest); err != nil {
return err
}

return nil
}

// finalizeMap opens the ':pending' Map pin of the given named Map from bpffs.
// If the given map is not found in bppffs, returns nil.
// If revert is true, the map will be re-pinned back to its initial locations.
// If revert is false, the map will be unpinned.
func finalizeMap(bpffsPath, name string, revert bool) error {
// Attempt to open a 'pending' Map pin.
file := filepath.Join(bpffsPath, name+bpffsPending)
pending, err := ebpf.LoadPinnedMap(file, nil)

// Given map was not pending recreation, nothing to do.
if errors.Is(err, unix.ENOENT) {
return nil
}

if err != nil {
return fmt.Errorf("unable to open pinned map at path %s: %v", name, err)
}

// Pending Map was found on bpffs and needs to be reverted.
if revert {
dest := filepath.Join(bpffsPath, name)
log.Infof("Reverting map pin from '%s' to '%s' after failed migration", file, dest)

// Atomically re-pin the map to its original path.
if err := pending.Pin(dest); err != nil {
return err
}

return nil
}

log.Infof("Unpinning map '%s' after successful recreation", file)

// Pending Map found on bpffs and its replacement was successfully loaded.
// Unpin the old map since it no longer needs to be interacted with from userspace.
return pending.Unpin()
}

0 comments on commit 4305f92

Please sign in to comment.