Skip to content

lib: allocate matchers and set elements with calloc() - #612

Merged
qdeslandes merged 1 commit into
meta:mainfrom
prashantpcm94:ipv6-net-zero-padding
Oct 29, 2026
Merged

qdeslandes merged 1 commit into
meta:mainfrom
prashantpcm94:ipv6-net-zero-padding

Conversation

@prashantpcm94

@prashantpcm94 prashantpcm94 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

bfcli chain update-set --remove of an IPv6 prefix returns success but doesn't remove it:

$ bfcli chain set --from-str "chain c BF_HOOK_TC_INGRESS ACCEPT set n6 (ip6.snet) in { 2001:db8::/64; 2001:db9::/64 } rule (ip6.snet) in n6 counter DROP"
$ bfcli chain update-set --name c --set-name n6 --remove 2001:db8::/64
info   : updated set 'n6' in chain 'c'
$ bfcli chain get --name c
chain c BF_HOOK_TC_INGRESS ACCEPT
    counters policy 0 packets 0 bytes; error 0 packets 0 bytes
    set n6 (ip6.snet) in {
        2001:db8::/64
        2001:db9::/64
    }
    ...

The prefix keeps matching, and { 2001:db8::/64; 2001:db8::/64 } is stored as 2 elements. Matcher payloads and set elements are parsed into buffers allocated with malloc(), and _bf_parse_ipv6_net() leaves the 4 bytes of padding at the end of struct bf_ip6_lpm_key unset. Sets compare elements byte for byte, so each parsed ip6.snet/ip6.dnet element carries whatever was in that memory, and the element to remove usually doesn't equal the stored one. This allocates matchers and set elements with calloc(), so the bytes a parser doesn't write are zero.

Related issue

None.

Testing

CMake release build (-DNO_DOCS=1) on CentOS Stream 9, kernel 6.16.1, gcc 11.5, clang 22.1.3, libbpf 1.6.3, then make -C $BUILD test_bin and ctest (without fuzzing.parser.infinite, which runs until stopped):

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

check.iwyu didn't run because iwyu_tool.py isn't installed here (it only checks cgen/program.c). The check.astgrep tests aren't registered because ast-grep isn't installed either.

No test is added: whether the bug shows depends on what malloc() returns, so a test could pass without the fix.

Checked by hand with release builds, the chain above on BF_HOOK_TC_INGRESS, packets run with bpftool prog run, 3 runs each:

  • main: --remove 2001:db8::/64 returns 0, chain get still lists both prefixes, and a packet from 2001:db8::1 is still dropped. The duplicate literal keeps 3 copies.
  • with this PR: only 2001:db9::/64 is left, the 2001:db8::1 packet passes, the 2001:db9::1 packet is still dropped, and the duplicate literal keeps 1 copy.

Notes for the reviewer

  • Debug builds hide the bug: ASan fills new allocations with 0xbe, so every parsed element gets the same padding and they compare equal.
  • Chains created before this fix keep their stored padding (bf_set_pack() writes element bytes as they are), so removing their IPv6 networks keeps missing until the chain is set again. No release is affected: main can't restore chains with sets pinned by any build before 5239340 (v0.7.0 included), so only chains pinned by main builds between 5239340 and this fix are concerned.
  • The kernel's LPM trie only compares the first prefixlen bits and the generated program never reads the padding, so this doesn't change how an element matches. It changes which elements userspace sees as equal: what --remove finds, and duplicates.

AI disclosure

Claude Code found the issue, drafted the change, 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_bin test passes (check.iwyu and check.astgrep 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

@prashantpcm94

Copy link
Copy Markdown
Contributor Author

@claude review

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Claude review of PR #612 (6c17977)

The fix looks correct for newly parsed elements. Reviewers checked every BF_MATCHER_IN parse callback a set key can use, and this is the only one that leaves bytes uninitialized.

Suggestions

  • Existing chains keep the non-zero padding — src/libbpfilter/matcher.c:633 — bf_set_pack() serializes elements raw, padding included, and bf_set_new_from_pack() restores them as-is. On chains created before this fix, --remove still misses the stored element and still reports success. Either note that these chains have to be recreated, or also zero the padding on unpack.

Nits

  • Explain why the padding is zeroed — src/libbpfilter/matcher.c:633 — nothing reads _padding, so a later cleanup could remove this as a dead store.
  • Use calloc() for set elements — src/libbpfilter/matcher.c:633 — bf_set_add_elem_raw() and _bf_set_expand_range() use malloc(set->elem_size). Using calloc() would also cover future parsers or key structs that have padding. This would come on top of the parser fix, not replace it.

Workflow run

Comment thread src/libbpfilter/matcher.c Outdated
Comment thread src/libbpfilter/matcher.c Outdated
Comment thread src/libbpfilter/matcher.c Outdated

@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.

I would calloc the matchers instead, so we apply this across the board.

@prashantpcm94 prashantpcm94 changed the title lib: matcher: zero the IPv6 network padding when parsing lib: allocate matchers and set elements with calloc() Oct 28, 2026
@prashantpcm94

Copy link
Copy Markdown
Contributor Author

Fixed in latest rev

@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.

LGTM, thanks!

Matcher payloads and set elements are parsed into buffers allocated with
malloc(), and nothing guarantees a parser writes every byte.
_bf_parse_ipv6_net() leaves the 4 bytes of padding at the end of
struct bf_ip6_lpm_key unset, so an ip6.snet or ip6.dnet element parsed
from text keeps whatever was in that memory. Set elements are compared
byte for byte, so "chain update-set --remove" of such a prefix doesn't
find it and still returns success, the prefix keeps matching, and the
same prefix can be stored several times in one set.

Allocate matchers and set elements with calloc(), so the bytes a parser
doesn't write are zero.
@qdeslandes
qdeslandes force-pushed the ipv6-net-zero-padding branch from ab2cd93 to fb97a0f Compare October 29, 2026 15:14
@qdeslandes
qdeslandes enabled auto-merge (rebase) October 29, 2026 15:14
@qdeslandes
qdeslandes merged commit 180da87 into meta:main Oct 29, 2026
38 checks passed
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