Skip to content

lib: cgen: keep the log ring buffer across set updates - #605

Open
prashantpcm94 wants to merge 1 commit into
meta:mainfrom
prashantpcm94:log-map-keep-on-update
Open

prashantpcm94 wants to merge 1 commit into
meta:mainfrom
prashantpcm94:log-map-keep-on-update

Conversation

@prashantpcm94

@prashantpcm94 prashantpcm94 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

bf_chain_logs_fd() returns a file descriptor for the chain's log ring buffer. bf_chain_update_set() rebuilds the chain's program through bf_cgen_update(), and the new program created a new log ring buffer. A file descriptor opened before the update still pointed at the old ring buffer, which nothing writes to anymore, so a long-running reader (bfcli chain logs, or a service that opens the FD once) silently stopped getting logs after every set update.

This reuses the old program's log map when bf_chain_update_set() updates the chain (new BF_CGEN_UPDATE_PRESERVE_LOGS flag, next to BF_CGEN_UPDATE_PRESERVE_COUNTERS). bf_chain_update() replaces the chain's rules, so it still creates a new log ring buffer: callers get a new file descriptor from bf_chain_logs_fd(). The bf_chain_logs_fd() documentation and the user docs (explanation/lifecycle.rst, chain logs in reference/bfcli.rst) say so.

Related issue

None.

Testing

CMake release build (-DWITH_DOCS=OFF -DWITH_TESTS_BENCHMARKS=OFF) on CentOS Stream 9, kernel 6.16.1, gcc 11.5, clang 22.1.3, libbpf 1.6.3, then ctest -j16 (without fuzzing.parser.infinite, which runs until stopped):

99% tests passed, 1 tests failed out of 137
The following tests FAILED:
	133 - check.iwyu (Not Run)

check.iwyu didn't run because iwyu_tool.py isn't installed here. make -C $BUILD doc (with -DWITH_DOCS=ON) builds with no warnings, as on main.

  • e2e.actions.log (new, tests/e2e/actions/log.cpp):

    • logsFdSurvivesSetUpdate: opens the logs file descriptor, adds a port to the set with bf_chain_update_set(), sends a packet to that port, and expects one log entry.
    • logsFdSurvivesEmptySet: emptying the set disables the only logging rule; bf_chain_logs_fd() still returns the same ring buffer (same map ID), and refilling the set logs to the file descriptor opened at the start.

    Without the cgen change both fail (no entry on the old file descriptor; no logs buffer once the set is empty). With it they pass 10 runs in a row.

AI disclosure

Claude Code found the issue, drafted the change, the tests, the commit message and this description under my direction, and ran the tests listed under Testing. I reviewed the change and the results.

Checklist

  • I understand every line in this PR and can explain why it is correct
  • I built the project and ran the tests covering this change
  • make -C $BUILD test passes (check.iwyu not run, see Testing) and the code follows the style guide
  • Commits are formatted as component: subcomponent: short description
  • I have disclosed any AI usage above

@meta-cla meta-cla Bot added the cla signed label Oct 13, 2026
@prashantpcm94

Copy link
Copy Markdown
Contributor Author

@claude review

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Claude review of PR #605 (29f52b4)

Suggestions

  • new_fd check doesn't test map reuse — tests/e2e/actions/log_5_tuple.cpp:181 — the >= 0 check passes without the fix too; compare map IDs with bpf_map_get_info_by_fd() or drop new_fd
  • bf_chain_logs_fd() doc is incomplete — src/libbpfilter/include/bpfilter/bpfilter.h:127 — it leaves out bf_ruleset_set(), and it doesn't say the call now succeeds (instead of returning -ENOENT) after an update that removes logging
  • Intermediate empty-set state not tested — tests/e2e/actions/log_5_tuple.cpp:253 — nothing runs the program while the logging rule is disabled to check that it emits no logs
  • "all the maps are destroyed" no longer holds — src/libbpfilter/cgen/program.h:304 — on load failure, a reused log map is only closed, not destroyed
  • User docs not updated — src/libbpfilter/include/bpfilter/bpfilter.h:125 — lifecycle.rst and the chain logs section of bfcli.rst could say that bfcli chain logs now keeps working across chain update and chain update-set

Nits

  • "stay valid" wording — src/libbpfilter/cgen/cgen.h:152 — the FDs were already valid; the regression was that they stopped receiving logs
  • Use BFT_CHAIN_SET() for consistency — tests/e2e/actions/log_5_tuple.cpp:166
  • Missing "Set.hpp" include — tests/e2e/actions/log_5_tuple.cpp:192 — Set.hpp currently comes in only through Chain.hpp
  • Two-line // comment — tests/e2e/actions/log_5_tuple.cpp:248 — the style guide asks for /* */ on comments longer than one line
  • Stale entries' rule index — doc/usage/reference/bfcli.rst:149 — entries the old program published can be read after chain update, and their rule_id refers to the old chain's rules
  • "start chain logs again" after a flush — doc/usage/reference/bfcli.rst:149 — after a flush the chain is gone, so a new chain logs fails until the chain is set again; a running one goes quiet but doesn't exit
  • Move the new tests to their own file — tests/e2e/actions/log_5_tuple.cpp:162 — they test how long the logs buffer lives, not 5-tuple logging (three reviewers flagged this; you offered to move them)
  • Use std::optional<std::string> for the set name — tests/harness/Set.hpp:39 — matches Rule.hpp, and keeps "no name" and "" apart

Errors

  • mcp__github__get_pull_request_comments: this tool wasn't available in the session, so top-level PR comments were only read through mcp__github__get_issue_comments.

Workflow run

Comment thread tests/e2e/actions/log_5_tuple.cpp Outdated
assert_int_equal(1, capture.entries.size());
assert_int_equal(BF_VERDICT_DROP, capture.entries[0].verdict);

new_fd = bf_chain_logs_fd("test_log_update");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: suggestion: new_fd is only checked with >= 0. That check also passes without this fix, because the updated chain logs and the new handle had its own bf_lmap before this PR too. So it doesn't show that bf_chain_logs_fd() now returns the same ring buffer. Could the test compare the map IDs of fd and new_fd with bpf_map_get_info_by_fd() (libbpf 1.2+)? Another option is to open a second ring_buffer on new_fd and check that the same entry shows up there. If neither is wanted, the new_fd lines could be dropped.

*
* The file descriptor keeps receiving the chain's logs after
* `bf_chain_update()` and `bf_chain_update_set()`, until the chain is
* replaced with `bf_chain_set()` or flushed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: suggestion: Two points on this paragraph:

  • "until the chain is replaced with bf_chain_set() or flushed" leaves out bf_ruleset_set(), which also replaces every chain (_bf_ruleset_flush() followed by bf_cgen_new()), so a reader's FD goes stale there too. "Flushed" could mean bf_chain_flush() or bf_ruleset_flush(). Maybe list them all, e.g. "until the chain is replaced (bf_chain_set(), bf_ruleset_set()) or removed (bf_chain_flush(), bf_ruleset_flush())".
  • The return value of bf_chain_logs_fd() changes. Before this PR, after an update that left no logging rule (or a set update that disabled them), the chain had no lmap and the call returned -ENOENT ("chain '%s' has no logs buffer"). Now it succeeds and returns an FD to a ring buffer that nothing writes to, so bfcli chain logs polls with no output. Whether the call fails now depends on the chain's history, not on what it currently contains. Could the doc say so, e.g. "a chain that logged keeps its logs buffer, even after an update that leaves no logging rule"? The commit body could also mention that the ring buffer stays allocated and pinned for as long as the chain exists.

Comment thread tests/e2e/actions/log_5_tuple.cpp Outdated
// Emptying the set disables the only logging rule, refilling it enables
// the rule again
assert_int_equal(0,
bf_chain_update_set("test_log_empty_set", none.get().get(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: suggestion: The intermediate state is never run: the set is empty, BF_CHAIN_LOG is cleared, and lmap is kept although the program doesn't log. Could a bft_assert_prog_run() with dport 53 expecting bft_hook_accept(BF_HOOK_XDP), followed by assert_int_equal(0, ring_buffer__consume(rb)), go between the two bf_chain_update_set() calls? That checks that the disabled rule really emits nothing. A bf_chain_logs_fd() call in that state would also record the behaviour change, since it now succeeds where it used to return -ENOENT.

Comment thread src/libbpfilter/cgen/program.h Outdated
* Prior to loading the BPF program, multiple BPF maps are created to store
* the counters, the debug strings, and the sets. If the program can't be
* loaded, all the maps are destroyed.
* loaded, all the maps are destroyed. If `prog->handle` already has a log

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: suggestion: With this change, the sentence just before the new one ("If the program can't be loaded, all the maps are destroyed.") no longer holds for the log map. When prog->handle->lmap was duplicated from the old handle, freeing the new handle only closes the duplicated FD, and the BPF map stays alive through the old handle. That is what this PR wants, so could the paragraph say it, e.g. "the maps created by this function are destroyed; a reused log map is only closed"?

/**
* @brief Get the file descriptor of a chain's logs buffer.
*
* The file descriptor keeps receiving the chain's logs after

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: suggestion: The user-facing docs don't mention the new behaviour yet. In doc/usage/explanation/lifecycle.rst, "Replacing a chain" lists what chain update keeps and what it resets, and "Updating a set" says chain update-set preserves the counters. Should they also say that a running bfcli chain logs keeps printing entries across chain update and chain update-set, but has to be restarted after chain set, ruleset set, or a flush? A short note in the chain logs section of doc/usage/reference/bfcli.rst would work too. Those files aren't part of this diff, which is why this comment is attached here.

Comment thread src/libbpfilter/cgen/cgen.h Outdated
* On success, the new program is stored in the codegen, and the previous
* program is unloaded and freed.
* program is unloaded and freed. The log ring buffer is kept, so file
* descriptors returned by `bf_chain_logs_fd()` stay valid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: nit: "so file descriptors returned by bf_chain_logs_fd() stay valid": those FDs were already valid before this change, because they hold their own reference to the old map. The regression was that nothing wrote to that map anymore. Could this use the wording from the cgen.c comment and bpfilter.h, "keep receiving the chain's logs"?

Comment thread tests/e2e/actions/log_5_tuple.cpp Outdated

(void)state;

assert_int_equal(0, bf_chain_set(chain.get().get(), nullptr));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: nit: The other tests in this file, including the two new set tests, install the chain with BFT_CHAIN_SET(...). chain isn't used after this call, so could it be built inline in BFT_CHAIN_SET(bf::Chain(...) << bf::Rule(...)) to match? updated still needs to be a variable for bf_chain_update().

Comment thread tests/e2e/actions/log_5_tuple.cpp Outdated
_cleanup_close_ int fd = -1;
struct bft_log_capture capture;
struct ring_buffer *rb;
auto set = bf::Set({BF_MATCHER_UDP_DPORT}, "ports");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: nit: This file now uses bf::Set, but it doesn't include "Set.hpp"; it builds only because Chain.hpp includes it. The other e2e files that use bf::Set (tests/e2e/matchers/udp_dport.cpp, icmp_type.cpp, icmpv6_type.cpp, ...) include "Set.hpp" directly. Could it be added next to "Rule.hpp"?

Comment thread tests/e2e/actions/log_5_tuple.cpp Outdated
fd = bf_chain_logs_fd("test_log_empty_set");
assert_true(fd >= 0);
rb = ring_buffer__new(fd, bft_capture_log, &capture, nullptr);
assert_non_null(rb);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: nit: This comment takes two lines, and doc/developers/style.rst says: "Use // for comments that fit on one line, /* */ for longer comments". tupleLogging() in this file already follows that rule. Could it be /* ... */?

@prashantpcm94
prashantpcm94 force-pushed the log-map-keep-on-update branch from 566f124 to 29f52b4 Compare October 13, 2026 20:51
@prashantpcm94

Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread doc/usage/reference/bfcli.rst Outdated
Print a chain's log entries.

bfcli will print log entries as they are published by the chain. Hit ``Ctrl+C`` to quit.
bfcli will print log entries as they are published by the chain. Hit ``Ctrl+C`` to quit. The log entries keep coming after ``chain update`` and ``chain update-set``. After ``chain set``, ``ruleset set``, or a flush, start ``chain logs`` again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: nit: Now that the ring buffer outlives the program, a reader can still get entries that the old program published before the switch but that are only consumed after chain update. struct bf_log describes itself (log_type, l3_proto, l4_proto) and the hook can't change, so these entries still decode correctly. rule_id is different: it's an index into the chain version that emitted the entry. chain update can reorder, add or remove rules, so a stale entry can print an index that now points to another rule. Could the docs say that each entry's rule index refers to the chain version that published it? This could go here, and maybe in the bf_chain_logs_fd() doc.

Comment thread doc/usage/reference/bfcli.rst Outdated
Print a chain's log entries.

bfcli will print log entries as they are published by the chain. Hit ``Ctrl+C`` to quit.
bfcli will print log entries as they are published by the chain. Hit ``Ctrl+C`` to quit. The log entries keep coming after ``chain update`` and ``chain update-set``. After ``chain set``, ``ruleset set``, or a flush, start ``chain logs`` again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: nit: Two points on "After chain set, ruleset set, or a flush, start chain logs again.":

  • After a flush the chain no longer exists. A new chain logs only works once the chain has been set again; until then it fails with "has no logs buffer".
  • A running chain logs doesn't exit in any of these cases. It keeps polling the orphaned ring buffer and goes quiet.

Could the doc say that, and name chain flush / ruleset flush explicitly, as bpfilter.h does?

Comment thread tests/e2e/actions/log_5_tuple.cpp Outdated
return info.id;
}

static void logsFdSurvivesChainUpdate(void **state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: nit: The three new tests use BF_LOG_OPT_DEFAULT and check how long the logs buffer lives across bf_chain_update() / bf_chain_update_set(), not 5-tuple logging. You offered in the PR description to move them, and a dedicated file would make them easier to find. For example, tests/e2e/actions/log.cpp could be registered with bf_add_e2e_c_test() next to actions/log_5_tuple.cpp in tests/e2e/CMakeLists.txt. log_5_tuple.cpp would then stay scoped to the 5-tuple format.

Comment thread tests/harness/Set.hpp Outdated
public:
Set(std::vector<bf_matcher_type> key):
_key {std::move(key)}
Set(std::vector<bf_matcher_type> key, std::string name = {}):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude: nit: Could the name be a std::optional<std::string> that defaults to std::nullopt? Rule.hpp handles its optional arguments that way (counters, logRateNs). With the empty-string sentinel, a test can't build a set named "", although bf_set_new() accepts any non-NULL name. With std::optional, "no name" and "empty name" stay distinct, and get() becomes _name ? _name->c_str() : nullptr.

@prashantpcm94
prashantpcm94 force-pushed the log-map-keep-on-update branch from 29f52b4 to 4417a42 Compare October 14, 2026 13:35
@prashantpcm94
prashantpcm94 marked this pull request as ready for review October 14, 2026 13:38

@qdeslandes qdeslandes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Test details aren't relevant in the commit message.

It's worth documenting in the code that if the updated chain doesn't log at all, the ring buffer will remain. I'm not sure what to think about that, it's a stale resource that might or might not be used again.

For rules disabled by bpfilter, it makes sense to keep the log buffer, as the rule is technically still there. However, if the chain changes so that no rule logs anymore, having the log buffer around doesn't make sense.

Comment thread tests/e2e/actions/log.cpp Resolved
Comment thread tests/e2e/actions/log.cpp Resolved
@prashantpcm94
prashantpcm94 force-pushed the log-map-keep-on-update branch 2 times, most recently from 6f27e70 to 8ec4b7f Compare October 21, 2026 21:50
@prashantpcm94

Copy link
Copy Markdown
Contributor Author

Thanks, changed as suggested:

  • The log map is now kept only while a rule of the updated chain logs, including rules bpfilter disabled (e.g. an empty set). If an update removes every logging rule, the log map goes away with the old program, and bf_chain_logs_fd() returns -ENOENT as before. The comment in bf_cgen_update() and the bf_chain_logs_fd() doc say so, and a new test, logsBufferRemovedWithoutLoggingRule, covers it.
  • Test details are out of the commit message.
  • Rebased on main to fix the conflict. The harness commit is gone: now that sets are referenced by name, copies of a bf::Set share its name, which is all the bf_chain_update_set() tests need.

@prashantpcm94

Copy link
Copy Markdown
Contributor Author

Ping!

@qdeslandes
qdeslandes force-pushed the log-map-keep-on-update branch from 8ec4b7f to 2ea0d80 Compare November 4, 2026 14:58

@qdeslandes qdeslandes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Few nits, also CI is failing.

Comment thread src/libbpfilter/cgen/cgen.c Outdated
Comment on lines +432 to +442
static bool _bf_cgen_has_log_rule(const struct bf_chain *chain)
{
bf_list_foreach (&chain->rules, rule_node) {
const struct bf_rule *rule = bf_list_node_get_data(rule_node);

if (rule->log)
return true;
}

return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

chain->flags will have BF_CHAIN_LOG bit set if the chain is expected to log.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As discussed, the log ring buffer is now only kept across bf_chain_update_set(), which doesn't change the rules, so this helper is gone.

Comment thread tests/e2e/actions/log.cpp Outdated
Comment on lines +25 to +33
static uint32_t mapId(int fd)
{
struct bpf_map_info info = {};
uint32_t len = sizeof(info);

assert_ok(bpf_map_get_info_by_fd(fd, &info, &len));

return info.id;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This could be a helper a the harness level.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, it's bft_map_id() in the harness.

Comment thread tests/e2e/actions/log.cpp
assert_fd(fd);
rb = ring_buffer__new(fd, bft_capture_log, &capture, nullptr);
assert_non_null(rb);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

    assert_fd(fd = bf_chain_logs_fd("test_log_update_set"));
    assert_non_null(rb = ring_buffer__new(fd, bft_capture_log, &capture, nullptr));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

@prashantpcm94 prashantpcm94 changed the title lib: cgen: keep the log ring buffer across chain updates lib: cgen: keep the log ring buffer across set updates Nov 4, 2026
@prashantpcm94
prashantpcm94 force-pushed the log-map-keep-on-update branch from 2ea0d80 to 66b89a9 Compare November 4, 2026 18:28
@prashantpcm94

Copy link
Copy Markdown
Contributor Author

Reworked as discussed: the log ring buffer is kept across bf_chain_update_set() only, and bf_chain_update() still creates a new one. CI failed because, since #627, copies of an unnamed bf::Set share one anonymous set, so the update-set tests now build separate sets with the same name.

@prashantpcm94 prashantpcm94 self-assigned this Nov 5, 2026
bf_chain_update_set() rebuilds the chain's program through
bf_cgen_update(), and the new program created its own log ring buffer.
A file descriptor returned by bf_chain_logs_fd() before the update
still pointed at the old ring buffer, which nothing writes to anymore,
so its reader silently stopped getting logs.

Reuse the old program's log map when bf_chain_update_set() updates the
chain. bf_chain_update() replaces the chain's rules, so it still
creates a new log ring buffer, and callers get a new file descriptor
from bf_chain_logs_fd().
@qdeslandes
qdeslandes force-pushed the log-map-keep-on-update branch from 66b89a9 to 62ef76b Compare November 6, 2026 12:04

This branch has not been deployed

No deployments
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