From: Dmitry Torokhov <dmitry.torokhov@gmail.com>
To: "Kernel.org Tools" <tools@kernel.org>
Cc: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
Subject: [PATCH b4 v2 4/5] b4: reject Bcc trailers in patches and cover letter
Date: Sun, 11 Oct 2026 22:58:29 -0700 [thread overview]
Message-ID: <20261011-send-series-bcc-v2-4-cd17584c40fb@gmail.com> (raw)
In-Reply-To: <20261011-send-series-bcc-v2-0-cd17584c40fb@gmail.com>
b4 send does not support per-patch or cover-letter Bcc: trailers.
Without an explicit check, a Bcc: trailer containing an email address
is parsed as a person trailer and silently added to the public Cc:
header, leaking the intended blind recipient.
Explicitly abort with a critical error in get_cover_dests() and
cmd_send() when a person Bcc: trailer is encountered, checking before
commit-only, deduplication, or --no-trailer-to-cc / --preview-to
filters while stripping the patch diff first so diff lines and
non-person prose do not trigger false positives.
Assisted-by: LLM
Signed-off-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
---
src/b4/ez.py | 62 +++++++++++++++++++++++++---------------
src/tests/test_ez.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 120 insertions(+), 23 deletions(-)
diff --git a/src/b4/ez.py b/src/b4/ez.py
index 71b4648..b2c33fa 100644
--- a/src/b4/ez.py
+++ b/src/b4/ez.py
@@ -2278,6 +2278,12 @@ def get_cover_dests(
elif mtr.lname == 'cc' and mtr.addr is not None:
ccs.append(mtr.addr)
mtrs.remove(mtr)
+ elif mtr.lname == 'bcc' and mtr.addr is not None:
+ logger.critical('CRITICAL: Bcc trailers in patches are not supported.')
+ logger.critical(
+ ' Please remove them and use --bcc or send-series-bcc configuration.'
+ )
+ sys.exit(1)
cbody = b4.LoreMessage.rebuild_message(htrs, cmsg, mtrs, basement, sig)
return tos, ccs, cbody
@@ -2935,31 +2941,41 @@ def cmd_send(cmdargs: argparse.Namespace) -> None:
else:
seen.update([x[1] for x in todests])
seen.update([x[1] for x in ccdests])
- # Go through the messages to make to/cc headers
- for commit, msg in patches:
- if not msg:
+
+ # Go through the messages to make to/cc headers
+ for commit, msg in patches:
+ if not msg:
+ continue
+ body, _ = b4.LoreMessage.get_payload(msg)
+ btrs, _ = b4.LoreMessage.find_trailers(b4.DIFF_RE.split(body, maxsplit=1)[0])
+ for btr in btrs:
+ if btr.type != 'person':
continue
- body, _ = b4.LoreMessage.get_payload(msg)
- btrs, _ = b4.LoreMessage.find_trailers(body)
- for btr in btrs:
- if btr.type != 'person':
- continue
- if btr.addr is None:
- logger.debug('Skipping trailer %s with no address', btr.lname)
- continue
- if btr.addr[1] in seen:
- continue
- if commit:
- cpccs = pccs[commit]
- if btr.addr not in cpccs:
- cpccs.append(btr.addr)
- continue
- seen.add(btr.addr[1])
- if btr.lname == 'to':
- todests.append(btr.addr)
- continue
- ccdests.append(btr.addr)
+ if btr.lname == 'bcc':
+ logger.critical('CRITICAL: Bcc trailers in patches are not supported.')
+ logger.critical(
+ ' Please remove them and use --bcc or send-series-bcc configuration.'
+ )
+ sys.exit(1)
+ if cmdargs.preview_to or cmdargs.no_trailer_to_cc:
+ continue
+ if btr.addr is None:
+ logger.debug('Skipping trailer %s with no address', btr.lname)
+ continue
+ if btr.addr[1] in seen:
+ continue
+ if commit:
+ cpccs = pccs[commit]
+ if btr.addr not in cpccs:
+ cpccs.append(btr.addr)
+ continue
+ seen.add(btr.addr[1])
+ if btr.lname == 'to':
+ todests.append(btr.addr)
+ continue
+ ccdests.append(btr.addr)
+ if not (cmdargs.preview_to or cmdargs.no_trailer_to_cc):
excludes = b4.get_excluded_addrs()
conf_me_too = str(config.get('send-me-too', 'yes')).lower()
if cmdargs.not_me_too or conf_me_too in {'no', 'n', 'false', '0'}:
diff --git a/src/tests/test_ez.py b/src/tests/test_ez.py
index ccfe0dc..6a9a883 100644
--- a/src/tests/test_ez.py
+++ b/src/tests/test_ez.py
@@ -1483,3 +1483,84 @@ def test_prep_cover_and_deps_flags_are_mutually_exclusive() -> None:
parser.parse_args(['prep', '--edit-cover', '--cover-from-file', '-'])
with pytest.raises(SystemExit):
parser.parse_args(['prep', '--show-deps', '--deps-from-file', '-'])
+
+
+def test_send_rejects_per_patch_bcc_trailer(
+ prepdir_commit: str, caplog: pytest.LogCaptureFixture
+) -> None:
+ """A Bcc: trailer in a patch commit must be rejected by `b4 send` rather
+ than silently added to the per-patch / cover-letter Cc list.
+ """
+ with open(os.path.join(prepdir_commit, 'file1.txt'), 'a') as fh:
+ fh.write('diff context line\nBcc: diff-context@example.com\n')
+ b4.git_run_command(prepdir_commit, ['add', 'file1.txt'])
+ valid_commit_msg = (
+ 'feat: test bcc false positives\n\n'
+ 'Bcc: headers are stripped before signing.\n\n'
+ 'Signed-off-by: Test Author <author@example.com>\n'
+ )
+ ecode, _ = b4.git_run_command(prepdir_commit, ['commit', '-m', valid_commit_msg])
+ assert ecode == 0
+
+ parser = b4.command.setup_parser()
+ cmdargs = parser.parse_args(
+ [
+ '--no-stdin',
+ '--no-interactive',
+ '--offline-mode',
+ 'send',
+ '--dry-run',
+ '--to',
+ 'list@example.com',
+ ]
+ )
+ b4.ez.cmd_send(cmdargs)
+
+ with open(os.path.join(prepdir_commit, 'file1.txt'), 'a') as fh:
+ fh.write('bcc trailer test\n')
+ b4.git_run_command(prepdir_commit, ['add', 'file1.txt'])
+ commit_msg = (
+ 'feat: test bcc trailer rejection\n\n'
+ 'Bcc: Secret Person <secret@example.com>\n'
+ 'Signed-off-by: Test Override <test-override@example.com>\n'
+ )
+ ecode, _ = b4.git_run_command(prepdir_commit, ['commit', '-m', commit_msg])
+ assert ecode == 0
+
+ flag_sets = [
+ ['--dry-run'],
+ ['--dry-run', '--no-trailer-to-cc'],
+ ['--preview-to', 'preview@example.com'],
+ ]
+ for extra_flags in flag_sets:
+ caplog.clear()
+ cmdargs = parser.parse_args(
+ [
+ '--no-stdin',
+ '--no-interactive',
+ '--offline-mode',
+ 'send',
+ '--to',
+ 'list@example.com',
+ ]
+ + extra_flags
+ )
+ with caplog.at_level(logging.CRITICAL, logger='b4'):
+ with pytest.raises(SystemExit) as ex:
+ b4.ez.cmd_send(cmdargs)
+ assert ex.value.code == 1
+ assert 'Bcc trailers in patches are not supported' in caplog.text
+
+
+def test_cover_rejects_bcc_trailer(caplog: pytest.LogCaptureFixture) -> None:
+ cbody = (
+ 'Subject: [PATCH 0/1] Cover letter\n\n'
+ 'Cover letter body\n\n'
+ 'Bcc: Secret Person <secret@example.com>\n'
+ 'Signed-off-by: Test Author <author@example.com>\n'
+ )
+ with caplog.at_level(logging.CRITICAL, logger='b4'):
+ with pytest.raises(SystemExit) as ex:
+ b4.ez.get_cover_dests(cbody)
+ assert ex.value.code == 1
+ assert 'Bcc trailers in patches are not supported' in caplog.text
--
2.56.0.385.gd3acb90ef8-goog
next prev parent reply other threads:[~2026-10-12 5:58 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-12 5:58 [PATCH b4 v2 0/5] b4: Implement BCC support for patch submissions and review Dmitry Torokhov
2026-10-12 5:58 ` [PATCH b4 v2 1/5] misc/send-receive: support BCC in web submission endpoint Dmitry Torokhov
2026-10-12 5:58 ` [PATCH b4 v2 2/5] b4: implement --bcc and send-series-bcc config option Dmitry Torokhov
2026-10-12 5:58 ` [PATCH b4 v2 3/5] b4: handle per-message Bcc headers in send_mail and TUIs Dmitry Torokhov
2026-10-12 5:58 ` Dmitry Torokhov [this message]
2026-10-12 5:58 ` [PATCH b4 v2 5/5] misc/send-receive: require messages payload to be a list Dmitry Torokhov
2026-11-13 2:00 ` [PATCH b4 v2 0/5] b4: Implement BCC support for patch submissions and review Dmitry Torokhov
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261011-send-series-bcc-v2-4-cd17584c40fb@gmail.com \
--to=dmitry.torokhov@gmail.com \
--cc=konstantin@linuxfoundation.org \
--cc=tools@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox