Skip to content

Prevent serialization of bf_program object#419

Draft
qdeslandes wants to merge 7 commits intofacebook:mainfrom
qdeslandes:no_not_serialize_bytecode
Draft

Prevent serialization of bf_program object#419
qdeslandes wants to merge 7 commits intofacebook:mainfrom
qdeslandes:no_not_serialize_bytecode

Conversation

@qdeslandes
Copy link
Contributor

bf_program is used to generate the BPF bytecode for every chain: when a chain needs to be created, a new bf_program object is created and is used to contain the generation context. Once the chain is loaded, this object becomes irrelevant.

This change moves the references to the BPF objects into a new bf_handle object: when the program is loaded, all the maps and the program FD are stored in bf_program.handle, bf_cgen can then steal the pointer to bf_handle and discard the bf_program object.

bf_program creates the bf_link object in bf_program_new(), for every new
program. If the program is not attached, then program->link->hookopts is
NULL, and that's how we know.

This approach is not ideal as we might create a bf_link object we don't
need. Additionally, it leaves bf_link in a temporary state as long as we
don't attach the program to the kernel: the bf_link is in the bf_program
object and serialized, but not materialized.

This commit only create bf_link when the BPF link object is needed.
Calling bf_link_new() will create the BPF link, and fail on error. Doing
so, we know that any bf_link object refers to a program attached to the
kernel.
bf_program creates the necessary maps objects by default
(in bf_program_new()), then update them with the proper size and number
of elements.

This commit improves this behaviour by following bf_link's example:
bf_map are not created until they are required by the program. Calling
bf_map_new() will create the BPF map object, which makes the behaviour
consistent with bf_link.
Create bf_handle to store references to all the BPF objects defined for
a chain, instead of centralizing them in bf_program. The benefit is that
bf_cgen can take the ownership of bf_handle and discard the bf_program
object that becomes irrelevant once the bytecode is loaded.
Replace bf_program in bf_cgen with bf_handle. The bytecode generation
context (bf_program) don't need to be stored anymore, as we can keep a
reference to the BPF objects.
Copilot AI review requested due to automatic review settings February 14, 2026 09:23
@meta-cla meta-cla bot added the cla signed label Feb 14, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors BPF program management by introducing a bf_handle object that separates BPF object references (program FD, maps, link) from the bytecode generation context (bf_program). The goal is to prevent serialization of the transient bf_program object by allowing it to be discarded after the program is loaded, while keeping only the handle with the BPF object references.

Changes:

  • Introduced bf_handle to manage BPF object references separately from bytecode generation context
  • Moved BPF map creation from a two-phase approach (allocate then create) to single-phase (create immediately)
  • Integrated link attachment into bf_link_new, removing separate attach/detach functions
  • Updated bf_cgen to use bf_handle instead of bf_program for managing loaded programs

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/bpfilter/cgen/handle.h New header defining bf_handle structure and API
src/bpfilter/cgen/handle.c New implementation of bf_handle management
src/bpfilter/cgen/program.h Removed BPF object fields, added handle pointer, simplified API
src/bpfilter/cgen/program.c Refactored to create maps/handle during load, transfer handle ownership
src/bpfilter/cgen/prog/map.h Removed map mutation APIs, simplified to create-once approach
src/bpfilter/cgen/prog/map.c Maps now created immediately with BTF data
src/bpfilter/cgen/prog/link.h Integrated attachment into link creation
src/bpfilter/cgen/prog/link.c Moved attachment logic into bf_link_new
src/bpfilter/cgen/cgen.h Changed from bf_program pointer to bf_handle pointer
src/bpfilter/cgen/cgen.c Updated to use bf_handle for program management
src/bpfilter/xlate/cli.c Updated to access BPF objects through handle
src/libbpfilter/include/bpfilter/bpf.h Added btf_data_len parameter to bf_bpf_btf_load
src/libbpfilter/bpf.c Updated bf_bpf_btf_load to use btf_data_len
src/bpfilter/CMakeLists.txt Added handle.c/handle.h to build
tests/unit/libbpfilter/bpf.c Updated test calls to bf_bpf_btf_load
tests/e2e/daemon/restore_with_sets.sh New E2E test for restoring chains with sets
tests/e2e/CMakeLists.txt Added restore_with_sets.sh test
Multiple files Code formatting improvements

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +229 to +237
if (handle->lmap) {
DUMP(prefix, "lmap: struct bf_map *");
bf_dump_prefix_push(prefix);
bf_map_dump(handle->lmap, bf_dump_prefix_last(prefix));
bf_dump_prefix_pop(prefix);
} else {
DUMP(prefix, "lmap: struct bf_map * (NULL)");
}

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lmap field is being dumped twice in bf_handle_dump. The second dump block (lines 229-236) is a duplicate of lines 220-227 and should be removed.

Suggested change
if (handle->lmap) {
DUMP(prefix, "lmap: struct bf_map *");
bf_dump_prefix_push(prefix);
bf_map_dump(handle->lmap, bf_dump_prefix_last(prefix));
bf_dump_prefix_pop(prefix);
} else {
DUMP(prefix, "lmap: struct bf_map * (NULL)");
}

Copilot uses AI. Check for mistakes.
if (type != BF_MAP_TYPE_LOG && key_size == 0)
return bf_err_r(-EINVAL, "bf_map %s: key size can't be 0", name);
if (type == BF_MAP_TYPE_LOG && key_size != 0)
return bf_err_r(-EINVAL, "bf_map %s: key size must be", name);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message for log maps is incomplete. It says "key size must be" but doesn't say what the key size must be. The message should be "key size must be 0" to match the validation logic.

Suggested change
return bf_err_r(-EINVAL, "bf_map %s: key size must be", name);
return bf_err_r(-EINVAL, "bf_map %s: key size must be 0", name);

Copilot uses AI. Check for mistakes.
Comment on lines 827 to 866
@@ -1028,28 +857,19 @@ static int _bf_program_load_sets_maps(struct bf_program *new_prog)
}

r = bf_bpf_map_update_batch(map->fd, keys, values, nelems, BPF_ANY);
if (r < 0) {
bf_err_r(r, "failed to add set elements to the map");
goto err_destroy_maps;
}
if (r)
return bf_err_r(r, "failed to add set elements to the map");

set_node = bf_list_node_next(set_node);
map_node = bf_list_node_next(map_node);
}
r = bf_list_push(&new_prog->handle->sets, (void **)&map);
if (r)
return r;
};
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code breaks the index mapping for sets. When an empty set is encountered, the code skips adding it to handle->sets but continues without maintaining the index correspondence. The BPF fixups reference sets by their index in chain->sets, which includes empty sets, but handle->sets will only contain non-empty sets after this change.

To fix this, either:

  1. Add NULL entries for empty sets (like the old code did), or
  2. Track a mapping between chain set indices and handle set indices

The old code maintained a 1:1 mapping by adding NULL entries, which was the correct approach.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@yaakov-stein yaakov-stein Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to this copilot comment

Comment on lines +107 to +112
_free_bf_map_ struct bf_map *map = NULL;

r = bf_list_emplace(&_handle->sets, bf_map_new_from_pack, map, dir_fd,
array_node);
if (r)
return bf_err_r(r, "failed to unpack bf_map into bf_handle.sets");
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deserialization code doesn't handle NULL entries in the sets array. The old code in bf_program_new_from_pack would check if the array node is nil and add a NULL entry to maintain the index correspondence. This new code always tries to deserialize a map, which will fail if a nil was serialized for an empty set. Add a check: if (!bf_rpack_is_nil(array_node)) { ... } else { bf_list_add_tail(&_handle->sets, NULL); }

Suggested change
_free_bf_map_ struct bf_map *map = NULL;
r = bf_list_emplace(&_handle->sets, bf_map_new_from_pack, map, dir_fd,
array_node);
if (r)
return bf_err_r(r, "failed to unpack bf_map into bf_handle.sets");
if (!bf_rpack_is_nil(array_node)) {
_free_bf_map_ struct bf_map *map = NULL;
r = bf_list_emplace(&_handle->sets, bf_map_new_from_pack, map,
dir_fd, array_node);
if (r)
return bf_err_r(r,
"failed to unpack bf_map into bf_handle.sets");
} else {
bf_list_add_tail(&_handle->sets, NULL);
}

Copilot uses AI. Check for mistakes.
Comment on lines +289 to +295
bf_list_foreach (&handle->sets, set_node) {
r = bf_map_pin(bf_list_node_get_data(set_node), dir_fd);
if (r) {
bf_err_r(r, "failed to pin BPF set map");
goto err_unpin_all;
}
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will crash if there are NULL entries in the sets list. After fixing the set index mapping bug (where NULL entries should be added for empty sets), this code needs to check if the map is NULL before attempting to pin it. Add: struct bf_map *map = bf_list_node_get_data(set_node); if (!map) continue;

Copilot uses AI. Check for mistakes.
Comment on lines +323 to +325
bf_list_foreach (&handle->sets, set_node)
bf_map_unpin(bf_list_node_get_data(set_node), dir_fd);

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will crash if there are NULL entries in the sets list. After fixing the set index mapping bug (where NULL entries should be added for empty sets), this code needs to check if the map is NULL before attempting to unpin it. Add: struct bf_map *map = bf_list_node_get_data(set_node); if (map) bf_map_unpin(map, dir_fd);

Suggested change
bf_list_foreach (&handle->sets, set_node)
bf_map_unpin(bf_list_node_get_data(set_node), dir_fd);
bf_list_foreach (&handle->sets, set_node) {
struct bf_map *map = bf_list_node_get_data(set_node);
if (map)
bf_map_unpin(map, dir_fd);
}

Copilot uses AI. Check for mistakes.
Comment on lines +240 to +247
bf_list_foreach (&handle->sets, map_node) {
struct bf_map *map = bf_list_node_get_data(map_node);

if (bf_list_is_tail(&handle->sets, map_node))
bf_dump_prefix_last(prefix);

bf_map_dump(map, prefix);
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dump code will crash if there are NULL entries in the sets list. After fixing the set index mapping bug (where NULL entries should be added for empty sets), this code needs to check if the map is NULL before attempting to dump it. Add a check: if (!map) { DUMP(prefix, "<empty set - no map>"); continue; }

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@yaakov-stein yaakov-stein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only reviewed 51bdadb so far, can review the rest soon if that'd be helpful. Re 51bdadb: Mostly a few nits - I had honestly been a bit confused until now about the need for separate steps (new + attach), so this seems much cleaner.

_link = malloc(sizeof(*_link));
if (!*hookopts) {
return bf_err_r(-EINVAL,
"hookopts are required when created a new bf_link");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "when creating"

/** Hook to which the link is attached. */
enum bf_hook hook;

/** Hook options used for the link. Only valid if the link is materialized */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I believe this comment should be updated now that hookopts should always be valid

* @param link `bf_link` object to allocate and initialize. On failure,
* this parameter is unchanged. Can't be NULL.
* @param name Name of the link. Can't be empty or NULL.
* @param hook Hook to attach the link to.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to let github let me comment on unchanged lines above, but I believe line 40/41:

@note This function won't create a new BPF link, but a bpfilter-specific
object used to keep track of a BPF link on the system.

is no longer accurate (this does now create the new BPF link as well as the bpfilter-specific link).

r = bf_bpf_obj_get("bf_link", dir_fd, &_link->fd);
if (r)
return bf_err_r(r, "failed to open pinned BPF link 'bf_link'");
r = bf_bpf_obj_get("bf_link", dir_fd, &_link->fd);
Copy link
Contributor

@yaakov-stein yaakov-stein Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this removes the dir_fd != -1 check (which make sense given the changes) but doesn't update the method's docs (link.h 59/60)

#include <bpfilter/pack.h>

int bf_link_new(struct bf_link **link, const char *name)
static int _bf_link_try_attach_xdp(int prog_fd, int ifindex, enum bf_hook hook);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be simpler and less duplicate code if we just move this entire method here?

Copy link
Contributor

@yaakov-stein yaakov-stein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review for fc16f93 -> 6b9fd02. To ensure we don't need commits like fc16f93 going forward, I added #421, which should enforce formatting. (Also, should we split out the fc16f93 commit, given that it's not too relevant to the rest of the PR?)

Overall this seems much cleaner and easier to follow than before, similar to the bf_link changes.


start_bpfilter
# Verify chain with sets is restored properly
${FROM_NS} bfcli chain get --name test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we verify the set is still there and with the same elements? It seems that all we're verifying is that the chain still exists. (ie we can add a ping and ensure it's dropped or something like that.)

#!/usr/bin/env bash

set -eux
set -o pipefail
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe these are no longer needed as of #417


#define _free_bf_btf_ __attribute__((__cleanup__(_bf_btf_free)))

static void _bf_btf_free(struct bf_btf **btf);
Copy link
Contributor

@yaakov-stein yaakov-stein Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the forward declaration comment in bf_link - is the forward declaration beneficial as opposed to just moving the method here?

}
} else {
_map->fd = -1;
r = bf_bpf_obj_get(_map->name, dir_fd, &_map->fd);
Copy link
Contributor

@yaakov-stein yaakov-stein Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment regarding stale docs as dir_fd == -1 in bf_link (bf_map_new_from_pack docs in bf_map.h L84/85 still say dir_fd can equal -1).

} else {
_map->fd = -1;
r = bf_bpf_obj_get(_map->name, dir_fd, &_map->fd);
if (r < 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unnecessary brackets

r = _bf_program_load_counters_map(prog);
if (r)
return r;
return bf_err_r(r, "failed to load the counter map");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Now that we add these bf_err_r, we're logging the error both in bf_program_load as well as in the _bf_program_load_*_map methods. The error logging granularity could be helpful, but I could also see that being unnecessary. Just wanted to flag, don't have an opinion either way.


void bf_program_unload(struct bf_program *prog)
{
_clean_bf_list_ bf_list list = bf_list_default_from(prog->sets);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prog->sets accesses prog before assert(prog) (L1091)

Comment on lines 827 to 866
@@ -1028,28 +857,19 @@ static int _bf_program_load_sets_maps(struct bf_program *new_prog)
}

r = bf_bpf_map_update_batch(map->fd, keys, values, nelems, BPF_ANY);
if (r < 0) {
bf_err_r(r, "failed to add set elements to the map");
goto err_destroy_maps;
}
if (r)
return bf_err_r(r, "failed to add set elements to the map");

set_node = bf_list_node_next(set_node);
map_node = bf_list_node_next(map_node);
}
r = bf_list_push(&new_prog->handle->sets, (void **)&map);
if (r)
return r;
};
Copy link
Contributor

@yaakov-stein yaakov-stein Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to this copilot comment

bf_wpack_open_object(pack, "cmap");
bf_map_pack(program->cmap, pack);
bf_wpack_close_object(pack);
if (program->cmap) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For L256, L264, L311, L320, L1140, L1148, L1196, L1198 - while an optimization was added for the log map and it is possible it may not be created, it appears that the counter map and printer map are always created. Do we need these null checks?

bf_map_free(&prog->cmap);
bf_map_free(&prog->pmap);
bf_map_free(&prog->lmap);
bf_swap(list, prog->sets);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty cool trick!

@qdeslandes qdeslandes marked this pull request as draft February 16, 2026 17:29
Copy link
Contributor

@yaakov-stein yaakov-stein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review for the rest of the commits. It seems like copilot caught the important thing about the sets - the rest is mostly nits.

if (!_program)
return -ENOMEM;

(void)snprintf(_program->prog_name, BPF_OBJ_NAME_LEN, "%s", _BF_PROG_NAME);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We keep prog_name in the bf_program struct but remove the initialization. program->prog_name is still used (for example in bf_program_dump on L217 and in bf_program_load on 910) and is UB.

{
_clean_bf_list_ bf_list list = bf_list_default_from(handle->sets);

assert(handle);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asset(handle) after using handle on L374

}

void bf_handle_free(struct bf_handle **handle)
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an assert(handle) here?

* used, then the file descriptors are already closed (as
* bf_program_unload() has been called). Otherwise, bf_program_unload()
* won't be called, but the programs are pinned, so they can be closed
* safely. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep this comment but move it to bf_handle_free? (IIUC the behavior is still the same)

void bf_handle_free(struct bf_handle **handle);

/**
* Serialize a bf_handle.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brief ?

bf_wpack_kv_nil(pack, "link");
}

if (handle->cmap) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to a comment from the previous review - handle->cmap and handle->pmap are guaranteed to be non-null IIUC. Should we remove the checks on L152, L160, L202, L211, L265, L273, L316, L318?

bf_cgen_dump(cgen, EMPTY_PREFIX);

cgen->program = TAKE_PTR(prog);
cgen->handle = TAKE_PTR(handle);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we're only taking the pointer here, the bf_cgen_dump on L302 will have stale/null data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants