Linux maintainer tooling and workflows
 help / color / mirror / Atom feed
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


  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