Repository: python/cpython
Pull request #127820: gh-127794: Validate header name according to rfc-5322
Author: login: srinivasreddy
Labels: name: topic-email
Created: 2024-12-11T11:59:14Z | Merged: 2025-03-30T12:29:29Z


<!--
Thanks for your contribution!
Please read this comment in its entirety. It's quite important.

# Pull Request title

It should be in the following format:

```
gh-NNNNN: Summary of the changes made
```

Where: gh-NNNNN refers to the GitHub issue number.

Most PRs will require an issue number. Trivial changes, like fixing a typo, do not need an issue.

# Backport Pull Request title

If this is a backport PR (PR made against branches other than `main`),
please ensure that the PR title is in the following format:

```
[X.Y] <title from the original PR> (GH-NNNN)
```

Where: [X.Y] is the branch name, e.g. [3.6].

GH-NNNN refers to the PR number from `main`.

-->


<!-- gh-issue-number: gh-127794 -->
* Issue: gh-127794
<!-- /gh-issue-number -->



Diff:
diff --git a/Lib/email/_policybase.py b/Lib/email/_policybase.py
index 4b63b97217a835..95e79b8938bb4c 100644
--- a/Lib/email/_policybase.py
+++ b/Lib/email/_policybase.py
@@ -4,6 +4,7 @@
 """
 
 import abc
+import re
 from email import header
 from email import charset as _charset
 from email.utils import _has_surrogates
@@ -14,6 +15,14 @@
     'compat32',
     ]
 
+# validation regex from RFC 5322, equivalent to pattern re.compile("[!-9;-~]+$")
+valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
+
+def validate_header_name(name):
+    # Validate header name according to RFC 5322
+    if not valid_header_name_re.match(name):
+        raise ValueError(
+            f"Header field name contains invalid characters: {name!r}")
 
 class _PolicyBase:
 
@@ -314,6 +323,7 @@ def header_store_parse(self, name, value):
         """+
         The name and value are returned unmodified.
         """
+        validate_header_name(name)
         return (name, value)
 
     def header_fetch_parse(self, name, value):
diff --git a/Lib/email/policy.py b/Lib/email/policy.py
index 6e109b65011a44..4169150101a29d 100644
--- a/Lib/email/policy.py
+++ b/Lib/email/policy.py
@@ -4,7 +4,13 @@
 
 import re
 import sys
-from email._policybase import Policy, Compat32, compat32, _extend_docstrings
+from email._policybase import (
+    Compat32,
+    Policy,
+    _extend_docstrings,
+    compat32,
+    validate_header_name
+)
 from email.utils import _has_surrogates
 from email.headerregistry import HeaderRegistry as HeaderRegistry
 from email.contentmanager import raw_data_manager
@@ -138,6 +144,7 @@ def header_store_parse(self, name, value):
         CR or LF characters.
 
         """
+        validate_header_name(name)
         if hasattr(value, 'name') and value.name.lower() == name.lower():
             return (name, value)
         if isinstance(value, str) and len(value.splitlines())>1:
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
index 2deb35721576b8..724af3b787d38b 100644
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -728,6 +728,31 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for policy in (email.policy.default, email.policy.compat32):
+            for setter in (Message.__setitem__, Message.add_header):
+                for name, value in invalid_headers:
+                    self.do_test_invalid_header_names(
+                        policy, setter,name, value)
+
+    def do_test_invalid_header_names(self, policy, setter, name, value):
+        with self.subTest(policy=policy, setter=setter, name=name, value=value):
+            message = Message(policy=policy)
+            pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+            with self.assertRaisesRegex(ValueError, pattern) as cm:
+                 setter(message, name, value)
+            self.assertIn(f"{name!r}", str(cm.exception))
+
     def test_binary_quopri_payload(self):
         for charset in ('latin-1', 'ascii'):
             msg = Message()
diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py
index 96979db27f3a21..23c39775a8b2e5 100644
--- a/Lib/test/test_email/test_message.py
+++ b/Lib/test/test_email/test_message.py
@@ -1004,6 +1004,30 @@ def test_folding_with_long_nospace_http_policy_1(self):
         parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
         self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for email_policy in (policy.default, policy.compat32):
+            for setter in (EmailMessage.__setitem__, EmailMessage.add_header):
+                for name, value in invalid_headers:
+                    self.do_test_invalid_header_names(email_policy, setter, name, value)
+
+    def do_test_invalid_header_names(self, policy, setter, name, value):
+        with self.subTest(policy=policy, setter=setter, name=name, value=value):
+            message = EmailMessage(policy=policy)
+            pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+            with self.assertRaisesRegex(ValueError, pattern) as cm:
+                 setter(message, name, value)
+            self.assertIn(f"{name!r}", str(cm.exception))
+
     def test_get_body_malformed(self):
         """test for bpo-42892"""
         msg = textwrap.dedent("""\
diff --git a/Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst b/Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
new file mode 100644
index 00000000000000..b6e39d66d04221
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
@@ -0,0 +1,4 @@
+When headers are added to :class:`email.message.Message` objects, either through
+:meth:`email.message.Message.__setitem__` or :meth:`email.message.Message.add_header`,
+the field name is now validated according to :rfc:`RFC 5322, Section 2.2 <5322#section-2.2>`
+and a :exc:`ValueError` is raised if the field name contains any invalid characters.


Reviews:
user: login: ZeroIntensity
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-12T14:08:43Z
user: login: bitdancer
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2024-12-12T20:26:57Z
user: login: bitdancer
body: Thanks for the prompt response.  Hopefully I can be reasonably prompt in return, but unfortunately no guarantees :(
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2024-12-13T23:08:37Z
user: login: bitdancer
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2024-12-17T21:04:11Z
user: login: bitdancer
body: Looking good, just some line wrapping needed yet.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2024-12-20T19:03:33Z
user: login: picnixz
body: The fact that a ValueError is now raised should be documented in the main documentation file. In addition a What's New entry is needed since it's an incompatible change with 3.13.

So I think we should rather treat this as a feature rather than a bug for now maybe? (if however this is not a feature, we still need a documentation update IMO)
state: DISMISSED
author_association: MEMBER
submitted_at: 2024-12-21T12:26:55Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-21T13:16:33Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-21T13:54:50Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-24T16:59:29Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-30T09:31:57Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-30T09:33:20Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-30T09:34:53Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-30T09:54:35Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-31T19:30:57Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-31T19:33:05Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-31T19:38:56Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-02T06:35:15Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-02T06:37:43Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-02T07:12:21Z
user: login: picnixz
body: Some final thoughts. Otherwise, it looks fine to me.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-02T09:53:36Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-02T10:35:03Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-02T13:08:12Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-05T21:27:41Z
user: login: picnixz
body: I forgot to post my review 3 days ago..
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-06T09:49:06Z
user: login: picnixz
body: Once RTD is back online, we can merge this one I think.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-01-06T10:45:37Z
user: login: picnixz
body: Friendly ping @bitdancer

I still don't know whether to treat it as a bug or a feature. I would say a bugfix because before it didn't work at all. However, we are now raising an exception; yet the code that previously didn't work also didn't raise (at least, according to the issue description). We can say that it's an example of "garbage in -> garbage out" (even though the garbage out is not the expected garbage out). Now that we raise, existing (but faulty) code could actually be raising exceptions which could not be handled by an application. And this can be considered a breaking change.

Therefore, should we backport this? 3.12 is nearing it's EOL in terms of bugfix so maybe it's a bit late to backport to it? I don't really know the policy in this case. cc @encukou @serhiy-storchaka  @vstinner as they know better what to do in this situation.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T18:42:52Z
user: login: bitdancer
body: Sorry it took me so long to get back to this :(
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-03T22:32:44Z
user: login: srinivasreddy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:01:09Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T20:56:32Z
user: login: bitdancer
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-29T21:02:49Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-30T11:47:59Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-30T11:57:48Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-30T12:02:09Z

Review comments:
diff_hunk: @@ -564,6 +564,14 @@ def add_header(self, _name, _value, **_params):
         msg.add_header('content-disposition', 'attachment',
                        filename='Fußballer.ppt'))
         """
+        # Validate header name according to RFC 5322
+        if not _name or ' ' in _name or '\t' in _name or ':' in _name:
path: Lib/email/message.py
user: login: ZeroIntensity
body: Could we split this up across multiple lines for readability?
created_at: 2024-12-12T14:08:42Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,29 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_headers(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+        ]
+        for name, value in invalid_headers:
+            with self.assertRaises(ValueError) as cm:
+               Message().add_header(name, value)
+            self.assertIn("Invalid header field name", str(cm.exception))
+
+        invalid_headers = [
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x1F', 'control character'),
+        ]
+        for name, value in invalid_headers:
+            with self.assertRaises(ValueError) as cm:
+               Message().add_header(name, value)
+            self.assertIn(f"Header field name {name!r} contains invalid characters", str(cm.exception))
+
path: Lib/test/test_email/test_email.py
user: login: bitdancer
body: Unfortunately this only tests your change to add_header, and does not actually test the underlying problem.  If you implement test methods that test `msg['test header'] = 'foo'`, you will find that the invalid field name will be accepted.  add_header is not used in the `__setitem__` path.

The correct location for the fix is in the two header_store_parse methods in policy.py and _policybase.py.  I'd put a helper method in _policybase and call it from those two methods.  I would also consider using a regex to implement the check...I *think* that will be faster, but you might want to test it.

Also, the tests should test both Message (old API) and EmailMessage (new API).  Since add_header uses the `__setitem__` syntax in its implementation you don't actually need to test it directly, but it wouldn't be bad to keep the tests to make sure that path doesn't get broken in the future.  More tests are good unless they are slow ;)  But not required.
created_at: 2024-12-12T20:26:24Z
author_association: MEMBER
diff_hunk: @@ -90,6 +90,14 @@ def __add__(self, other):
         """
         return self.clone(**other.__dict__)
 
+def validate_header(name):
path: Lib/email/_policybase.py
user: login: bitdancer
body: s/validate_header/validate_header_name/ for clarity.
created_at: 2024-12-13T22:58:52Z
author_association: MEMBER
diff_hunk: @@ -90,6 +90,14 @@ def __add__(self, other):
         """
         return self.clone(**other.__dict__)
 
+def validate_header(name):
+    # Validate header name according to RFC 5322
+    import re
+    if not re.match(r'^[^\s:]+$', name):
path: Lib/email/_policybase.py
user: login: bitdancer
body: It is better to do the import at the top of the module and compile the regex.  It's a micro optimization, but it is an obvous one.  (Note that utils, which _policybase imports, already imports re, so there's no extra overhead for importing it at the top level here.)
created_at: 2024-12-13T22:58:56Z
author_association: MEMBER
diff_hunk: @@ -90,6 +90,14 @@ def __add__(self, other):
         """
         return self.clone(**other.__dict__)
 
+def validate_header(name):
+    # Validate header name according to RFC 5322
+    import re
+    if not re.match(r'^[^\s:]+$', name):
+        raise ValueError(f"Invalid header field name {name!r}")
+    # Only allow printable ASCII characters
+    if any(ord(c) < 33 or ord(c) > 126 for c in name):
+        raise ValueError(f"Invalid header field name {name!r}")
path: Lib/email/_policybase.py
user: login: bitdancer
body: You are using the same error message for both cases here, so there is no advantage to having the tests be separate.  I'd use a variation on the RFC recommended regex for a single test: 
```
header_re = re.compile("[\041-\071\073-\176]+$")
```
Then this whole section becomes:
```
if not header_re.match(name):
    raise ValueError(f"Header field name contains invalid characters: {name!r}")
```

Also, I'd be inclined to put the function def above _PolicyBase, but that's not all that important.
created_at: 2024-12-13T22:58:58Z
author_association: MEMBER
diff_hunk: @@ -4,7 +4,7 @@
 
 import re
 import sys
-from email._policybase import Policy, Compat32, compat32, _extend_docstrings
+from email._policybase import Policy, Compat32, compat32, _extend_docstrings, validate_header
path: Lib/email/policy.py
user: login: bitdancer
body: Time to wrap this into a multiline expression to keep it under the PEP8 line length limit:
````
from email._policybase import (
    _extend_docstrings,
    Compat32,
    compat32,
    Policy,
    validate_header_name,
    )
```
You'll note that I sorted them alphabetically while I was at it (ignoring case).
created_at: 2024-12-13T22:59:03Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,29 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_headers(self):
path: Lib/test/test_email/test_email.py
user: login: bitdancer
body: test_invalid_header_names
created_at: 2024-12-13T22:59:14Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,29 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_headers(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+        ]
+        for name, value in invalid_headers:
+            with self.assertRaises(ValueError) as cm:
+               Message().add_header(name, value)
+            self.assertIn(f"Invalid header field name {name!r}", str(cm.exception))
+
+        invalid_headers = [
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x1F', 'control character'),
+        ]
path: Lib/test/test_email/test_email.py
user: login: bitdancer
body: What is the reason for having these separate from the list above?  If you did it because you have the checks as two separate clauses in the function, keep in mind that the structure of the tests should be independent of the structure of the code.  It looks like it makes more sense to have them all in one list.
created_at: 2024-12-13T23:01:41Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,29 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_headers(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+        ]
+        for name, value in invalid_headers:
+            with self.assertRaises(ValueError) as cm:
+               Message().add_header(name, value)
+            self.assertIn("Invalid header field name", str(cm.exception))
+
+        invalid_headers = [
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x1F', 'control character'),
+        ]
+        for name, value in invalid_headers:
+            with self.assertRaises(ValueError) as cm:
+               Message().add_header(name, value)
+            self.assertIn(f"Header field name {name!r} contains invalid characters", str(cm.exception))
+
path: Lib/test/test_email/test_email.py
user: login: bitdancer
body: Tests still need to be converted to test `__setitem__` and both `Message` and `EmailMessage`, but I assume you were waiting for that until I approved the code changes.
created_at: 2024-12-13T23:03:40Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,29 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_headers(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+        ]
+        for name, value in invalid_headers:
path: Lib/test/test_email/test_email.py
user: login: bitdancer
body: Add: `with self.subTest(name=name, problem=value):` and indent and wrap the remaining lines as appropriate.
created_at: 2024-12-13T23:06:45Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,15 @@
     'compat32',
     ]
 
+header_re  = re.compile(r'^[^\s:]+$')
path: Lib/email/_policybase.py
user: login: bitdancer
body: This re is redundant.  The second re catches spaces as well as non-ascii characters.  Call the second one valid_header_name_re, perhaps.
created_at: 2024-12-17T20:57:37Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,15 @@
     'compat32',
     ]
 
+header_re  = re.compile(r'^[^\s:]+$')
+header_ascii_re = re.compile("[\041-\071\073-\176]+$")
+
+def validate_header_name(name):
+    # Validate header name according to RFC 5322
+    if not header_re.match(name):
+        raise ValueError(f"Invalid header field name {name!r}")
path: Lib/email/_policybase.py
user: login: bitdancer
body: This if is redundant.  The second if will catch these cases as well.  While I see that you have made the error messages different, there is not IMO enough motivation for doing so.  A whatspace character is still an invalid character, just like a non-ascii or non-printible character is invalid.
created_at: 2024-12-17T20:58:11Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,46 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+        ]
+        for name, value in invalid_headers:
+            with self.subTest(name=name, problem=value):
+                with self.assertRaises(ValueError) as cm:
+                    Message().add_header(name, value)
+                self.assertIn(f"Invalid header field name {name!r}", str(cm.exception))
path: Lib/test/test_email/test_email.py
user: login: bitdancer
body: It is better not to so highly constrain the error message.  We really don't care what exactly the error message says.  The important bits are the words 'invalid', 'header', and 'name', and the repr of the header value.  To make the test place only those constraints on the error message, we can write:
```
msg = str(cm.exception)
self.assertRegex(
    msg,
    '(?i)(?=.*invalid)(?=.*header)(?=.*name)',
    )
self.assertIn(f"{name!r}", msg)
````
This uses lookahead assertions to check for each of the substrings in any position in the target string.  Using this as the tests, both versions of your error message pass, as would several other variations that would be equally valid.  And that makes it easy to see that if you delete your first 'if' test in the validator, all the tests still pass.
created_at: 2024-12-17T21:00:47Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,29 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_headers(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+        ]
+        for name, value in invalid_headers:
+            with self.assertRaises(ValueError) as cm:
+               Message().add_header(name, value)
+            self.assertIn("Invalid header field name", str(cm.exception))
+
+        invalid_headers = [
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x1F', 'control character'),
+        ]
+        for name, value in invalid_headers:
+            with self.assertRaises(ValueError) as cm:
+               Message().add_header(name, value)
+            self.assertIn(f"Header field name {name!r} contains invalid characters", str(cm.exception))
+
path: Lib/test/test_email/test_email.py
user: login: bitdancer
body: We have a four way combinatoric here: `__setitem__`, `add_header`, `policy=default`, and `policy=compat32`.  We can get clever and test them all in one nested set of loops:
```
for thispolicy in (policy.default, policy.compat32):
    for method in ('__setitem__', add_header):
        for problem, value in invalid_headers:
            with self.subTest(
                    name=name, 
                    problem=problem,
                    policy=thispolicy.__class__.__name__,
                    method=method,
                    ):
                with self.assertRaises(ValueError) as cm:
                    gatattr( EmailMessage(policy=thispolicy), method(
                        name,
                        problem,
                        )
                ...
```
created_at: 2024-12-17T21:03:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,2 @@
+The :meth:`email.message.Message.add_header` method now validates header
+field names according to :rfc:`5322`.
path: Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
user: login: bitdancer
body: This needs rewriting now, too.  Something like ":meth:email.message.Message now validates header field names according to :rfc`5322` when headers are added via `add_header` or `__setitem__` and raises a ValueError if they contain invalid characters."
created_at: 2024-12-17T21:03:51Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,12 @@
     'compat32',
     ]
 
+valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
+
+def validate_header_name(name):
+    # Validate header name according to RFC 5322
+    if not valid_header_name_re.match(name):
+        raise ValueError(f"Header field name contains invalid characters: {name!r}")
path: Lib/email/_policybase.py
user: login: bitdancer
body: Wrap <80, please
created_at: 2024-12-20T18:58:18Z
author_association: MEMBER
diff_hunk: @@ -1004,6 +1004,32 @@ def test_folding_with_long_nospace_http_policy_1(self):
         parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
         self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for thispolicy in (policy.default, policy.compat32):
+            for method in ('__setitem__', 'add_header'):
+                for name, value in invalid_headers:
+                    with self.subTest(
+                            name=name,
+                            description=value,
+                            policy=thispolicy.__class__.__name__,
+                            method=method,
+                            ):
+                        with self.assertRaises(ValueError) as cm:
+                            getattr(EmailMessage(policy=thispolicy), method)(name, value)
path: Lib/test/test_email/test_message.py
user: login: bitdancer
body: Please wrap <80
created_at: 2024-12-20T18:58:27Z
author_association: MEMBER
diff_hunk: @@ -1004,6 +1004,32 @@ def test_folding_with_long_nospace_http_policy_1(self):
         parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
         self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for thispolicy in (policy.default, policy.compat32):
+            for method in ('__setitem__', 'add_header'):
+                for name, value in invalid_headers:
+                    with self.subTest(
+                            name=name,
+                            description=value,
+                            policy=thispolicy.__class__.__name__,
+                            method=method,
+                            ):
+                        with self.assertRaises(ValueError) as cm:
+                            getattr(EmailMessage(policy=thispolicy), method)(name, value)
+                        msg = str(cm.exception)
+                        self.assertRegex( msg, '(?i)(?=.*invalid)(?=.*header)(?=.*name)')
path: Lib/test/test_email/test_message.py
user: login: bitdancer
body: Please wrap <80
created_at: 2024-12-20T18:58:29Z
author_association: MEMBER
diff_hunk: @@ -1004,6 +1004,32 @@ def test_folding_with_long_nospace_http_policy_1(self):
         parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
         self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for thispolicy in (policy.default, policy.compat32):
+            for method in ('__setitem__', 'add_header'):
+                for name, value in invalid_headers:
+                    with self.subTest(
+                            name=name,
+                            description=value,
+                            policy=thispolicy.__class__.__name__,
+                            method=method,
+                            ):
+                        with self.assertRaises(ValueError) as cm:
+                            getattr(EmailMessage(policy=thispolicy), method)(name, value)
path: Lib/test/test_email/test_message.py
user: login: picnixz
body: In addition, avoid having multiple calls within the `assertRaises` (you can also use `assertRaisesRegex`). For instance:

```py
message = EmailMessage(policy=thispolicy)
method = getattr(message, method)
pattern = re.escape(...)
with self.assertRaisesRegex(ValueError, pattern) as cm:
	method(name, value)
self.assertIn(repr(name), str(cm.exception))
```
created_at: 2024-12-21T12:20:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,3 @@
+The :meth:`email.message.Message.add_header` method now validates header
+field names according to :rfc:`5322` when headers are added via add_header
+or __setitem__ and raises a ValueError if they contain invalid characters.
path: Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
user: login: picnixz
body: ```suggestion
The :meth:`email.message.Message.add_header` method now validates header
field names according to :rfc:`5322` and raises a :exc:`ValueError` if
they contain invalid characters. A similar argument applies if headers
are added through :meth:`Message.__setitem__ <object.__setitem__>`.
```
created_at: 2024-12-21T12:22:14Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,12 @@
     'compat32',
     ]
 
+valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
path: Lib/email/_policybase.py
user: login: picnixz
body: Can't we have a comment saying what those octal values represent? (e.g., "equivalent to pattern "[!-9;-~]+$"). By the way `072` is `:`, so it's still printable by the way (maybe the title of the PR should be changed)
created_at: 2024-12-21T12:26:52Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,12 @@
     'compat32',
     ]
 
+valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
path: Lib/email/_policybase.py
user: login: bitdancer
body: Well, technically "Validate header name according to RFC 5322" is a very precise specification, you just have to refer to the RFC to find out what it is :).  Consequently the PR title *could* be shortened to omit the part after the rfc reference.

The reason to use the numeric version of the regex is because that's how the RFC specifies it.  I have no objection to adding a comment with the ascii version of the regex.
created_at: 2024-12-21T13:16:33Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,12 @@
     'compat32',
     ]
 
+valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
path: Lib/email/_policybase.py
user: login: picnixz
body: > because that's how the RFC specifies

Oh I see (I don't mind keeping the octal values, just maybe a one-line comment would be nice for future readers)
created_at: 2024-12-21T13:54:50Z
author_association: MEMBER
diff_hunk: @@ -4,7 +4,13 @@
 
 import re
 import sys
-from email._policybase import Policy, Compat32, compat32, _extend_docstrings
+from email._policybase import (
+    _extend_docstrings,
+    Compat32,
+    compat32,
+    Policy,
+    validate_header_name
+)
path: Lib/email/policy.py
user: login: picnixz
body: can you keep a case-sentitive alphabetical order actually? (I think that's how sorted imports elsewhere do it, but check that beforehand)
created_at: 2024-12-24T16:45:10Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,36 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for thispolicy in (email.policy.default, email.policy.compat32):
+            for method in ('__setitem__', 'add_header'):
+                for name, value in invalid_headers:
+                    with self.subTest(
+                            name=name,
+                            description=value,
+                            policy=thispolicy.__class__.__name__,
+                            method=method,
+                            ):
+                        message = Message(policy=thispolicy)
+                        class_method = getattr(message, method)
+                        with self.assertRaisesRegex(
+                            ValueError,
+                            '(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+                            ) as cm:
+                            class_method(name, value)
+                        self.assertIn(f"{name!r}", str(cm.exception))
path: Lib/test/test_email/test_email.py
user: login: picnixz
body: ```suggestion
        for policy in (email.policy.default, email.policy.compat32):
            for setter in (Message.__setitem__, Message.add_header):
                for name, value in invalid_headers:
                    self.do_test_invalid_header_names(policy, setter, name, value)

    def do_test_invalid_header_names(self, policy, setter, name, value):
        with self.subTest(policy=policy, setter=setter, name=name, value=value):
            message = Message(policy=policy)
            pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
            with self.assertRaisesRegex(ValueError, pattern) as cm:
                 setter(message, name, value)
            self.assertIn(f"{name!r}", str(cm.exception))
```

This could reduce the number of lines.
created_at: 2024-12-24T16:56:35Z
author_association: MEMBER
diff_hunk: @@ -1004,6 +1004,35 @@ def test_folding_with_long_nospace_http_policy_1(self):
         parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
         self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for thispolicy in (policy.default, policy.compat32):
path: Lib/test/test_email/test_message.py
user: login: picnixz
body: Ditto for this test (IMO it would be a bit clearer)
created_at: 2024-12-24T16:57:29Z
author_association: MEMBER
diff_hunk: @@ -140,6 +146,7 @@ def header_store_parse(self, name, value):
         """
         if hasattr(value, 'name') and value.name.lower() == name.lower():
             return (name, value)
+        validate_header_name(name)
path: Lib/email/policy.py
user: login: picnixz
body: Should we validate the header name if the value has a "name"?
created_at: 2024-12-24T16:59:26Z
author_association: MEMBER
diff_hunk: @@ -4,7 +4,13 @@
 
 import re
 import sys
-from email._policybase import Policy, Compat32, compat32, _extend_docstrings
+from email._policybase import (
+    _extend_docstrings,
+    Compat32,
+    compat32,
+    Policy,
+    validate_header_name
+)
path: Lib/email/policy.py
user: login: srinivasreddy
body: Current looks good.
created_at: 2024-12-30T09:31:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +146,7 @@ def header_store_parse(self, name, value):
         """
         if hasattr(value, 'name') and value.name.lower() == name.lower():
             return (name, value)
+        validate_header_name(name)
path: Lib/email/policy.py
user: login: srinivasreddy
body: Do you mean it would be better to change the function name to `validate_header(name)` ? 
created_at: 2024-12-30T09:33:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -728,6 +728,36 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for thispolicy in (email.policy.default, email.policy.compat32):
+            for method in ('__setitem__', 'add_header'):
+                for name, value in invalid_headers:
+                    with self.subTest(
+                            name=name,
+                            description=value,
+                            policy=thispolicy.__class__.__name__,
+                            method=method,
+                            ):
+                        message = Message(policy=thispolicy)
+                        class_method = getattr(message, method)
+                        with self.assertRaisesRegex(
+                            ValueError,
+                            '(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+                            ) as cm:
+                            class_method(name, value)
+                        self.assertIn(f"{name!r}", str(cm.exception))
path: Lib/test/test_email/test_email.py
user: login: srinivasreddy
body: I am not sure about this optimisation. I would always have a full context instead of splitting into another function that reduces lines. 
created_at: 2024-12-30T09:34:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -728,6 +728,36 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for thispolicy in (email.policy.default, email.policy.compat32):
+            for method in ('__setitem__', 'add_header'):
+                for name, value in invalid_headers:
+                    with self.subTest(
+                            name=name,
+                            description=value,
+                            policy=thispolicy.__class__.__name__,
+                            method=method,
+                            ):
+                        message = Message(policy=thispolicy)
+                        class_method = getattr(message, method)
+                        with self.assertRaisesRegex(
+                            ValueError,
+                            '(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+                            ) as cm:
+                            class_method(name, value)
+                        self.assertIn(f"{name!r}", str(cm.exception))
path: Lib/test/test_email/test_email.py
user: login: srinivasreddy
body: What do you think @bitdancer ?
created_at: 2024-12-30T09:54:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +146,7 @@ def header_store_parse(self, name, value):
         """
         if hasattr(value, 'name') and value.name.lower() == name.lower():
             return (name, value)
+        validate_header_name(name)
path: Lib/email/policy.py
user: login: picnixz
body: No, I meant: should we also call `validate_header_name` in the `if hasattr(value, 'name') and value.name.lower() == name.lower():` block before returning `(name, value)` as is? namely:

```py
if hasattr(value, 'name') and value.name.lower() == name.lower():
    validate_header_name(name)
    return (name, value)
validate_header_name(name)
```
created_at: 2024-12-31T19:30:57Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,36 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for thispolicy in (email.policy.default, email.policy.compat32):
+            for method in ('__setitem__', 'add_header'):
+                for name, value in invalid_headers:
+                    with self.subTest(
+                            name=name,
+                            description=value,
+                            policy=thispolicy.__class__.__name__,
+                            method=method,
+                            ):
+                        message = Message(policy=thispolicy)
+                        class_method = getattr(message, method)
+                        with self.assertRaisesRegex(
+                            ValueError,
+                            '(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+                            ) as cm:
+                            class_method(name, value)
+                        self.assertIn(f"{name!r}", str(cm.exception))
path: Lib/test/test_email/test_email.py
user: login: picnixz
body: > I am not sure about this optimisation. I would always have a full context instead of splitting into another function that reduces lines.

It's for readability and to ease the addition of more cases (also, please do not mark the discussion as resolved until it is really (you asked for bitdancer's opinion without waiting for them to reply)) (and for me to check).

FTR: if you want to acknowledge the suggestion, you can also comment with "Done in COMMIT_SHA".

created_at: 2024-12-31T19:33:05Z
author_association: MEMBER
diff_hunk: @@ -4,7 +4,13 @@
 
 import re
 import sys
-from email._policybase import Policy, Compat32, compat32, _extend_docstrings
+from email._policybase import (
+    _extend_docstrings,
+    Compat32,
+    compat32,
+    Policy,
+    validate_header_name
+)
path: Lib/email/policy.py
user: login: picnixz
body: I would still prefer a case-sensitive alphabetical order. We prefer consistency and/or smaller diffs when possible (does "Current looks good." actually tells me that it's how it's done elsewheere?)
created_at: 2024-12-31T19:35:06Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,31 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for policy in (email.policy.default, email.policy.compat32):
+            for setter in (Message.__setitem__, Message.add_header):
+                for name, value in invalid_headers:
+                    self.do_test_invalid_header_names(policy, setter, name, value)
+
+    def do_test_invalid_header_names(self, policy, setter, name, value):
+        with self.subTest(policy=policy, setter=setter, name=name, value=value):
+            message = Message(policy=policy)
+            pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+            with self.assertRaisesRegex(ValueError, pattern) as cm:
+                 setter(message, name, value)
+            self.assertIn(f"{name!r}", str(cm.exception))
+
path: Lib/test/test_email/test_email.py
user: login: picnixz
body: ```suggestion
```
created_at: 2024-12-31T19:37:02Z
author_association: MEMBER
diff_hunk: @@ -4,7 +4,13 @@
 
 import re
 import sys
-from email._policybase import Policy, Compat32, compat32, _extend_docstrings
+from email._policybase import (
+    _extend_docstrings,
+    Compat32,
+    compat32,
+    Policy,
+    validate_header_name
+)
path: Lib/email/policy.py
user: login: srinivasreddy
body: Adressed in e45165fb873
created_at: 2025-01-02T06:35:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -728,6 +728,31 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for policy in (email.policy.default, email.policy.compat32):
+            for setter in (Message.__setitem__, Message.add_header):
+                for name, value in invalid_headers:
+                    self.do_test_invalid_header_names(policy, setter, name, value)
+
+    def do_test_invalid_header_names(self, policy, setter, name, value):
+        with self.subTest(policy=policy, setter=setter, name=name, value=value):
+            message = Message(policy=policy)
+            pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+            with self.assertRaisesRegex(ValueError, pattern) as cm:
+                 setter(message, name, value)
+            self.assertIn(f"{name!r}", str(cm.exception))
+
path: Lib/test/test_email/test_email.py
user: login: srinivasreddy
body: addressed in 8233e928a8d
created_at: 2025-01-02T06:37:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,6 +146,7 @@ def header_store_parse(self, name, value):
         """
         if hasattr(value, 'name') and value.name.lower() == name.lower():
             return (name, value)
+        validate_header_name(name)
path: Lib/email/policy.py
user: login: srinivasreddy
body: Addressed in 2550011a71e. I do not remember the reason for placing `validate_header_name` between two if blocks. Make sense to move it up instead of calling twice?
created_at: 2025-01-02T07:12:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +15,14 @@
     'compat32',
     ]
 
+# equivalent to pattern re.compile("[!-9;-~]+$")
+valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
+
+def validate_header_name(name):
path: Lib/email/_policybase.py
user: login: picnixz
body: Actually, I just observed that we also have `email.utils`. I don't know whether this one is better to be placed here or in the utils, next to `_has_surrogates`. @bitdancer any thoughts? Also, we seem to use underscore names for private utils even if the module is already private so maybe `_validate_header_name` can be used as well (this is just a suggestion, but I'd like to hear others' thoughts)
created_at: 2025-01-02T09:48:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,4 @@
+The :meth:`email.message.Message.add_header` method now validates header
+field names according to :rfc:`5322` and raises a :exc:`ValueError` if
path: Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
user: login: picnixz
body: Can we have the dedicated section with field names specifications? If you find it, you can link it as <code>:rfc:\`5322#section-X.Y\`</code> and this would users knowing about it instead of going through the entire RFC.
created_at: 2025-01-02T09:53:33Z
author_association: MEMBER
diff_hunk: @@ -140,6 +146,7 @@ def header_store_parse(self, name, value):
         """
         if hasattr(value, 'name') and value.name.lower() == name.lower():
             return (name, value)
+        validate_header_name(name)
path: Lib/email/policy.py
user: login: picnixz
body: Totally. I just wanted to be verbose but yes it just makes sense to call it directly.
created_at: 2025-01-02T10:35:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,4 @@
+The :meth:`email.message.Message.add_header` method now validates header
+field names according to :rfc:`5322` and raises a :exc:`ValueError` if
path: Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
user: login: srinivasreddy
body: Addressed in 56a77e93524
created_at: 2025-01-02T13:08:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+The :meth:`email.message.Message.add_header` method now validates header
+field names according to :rfc:`RFC 5322 section 3.6.8 <5322#section-3.6.8>`
path: Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
user: login: picnixz
body: ```suggestion
field names according to :rfc:`RFC 5322, Section 3.6.8 <5322#section-3.6.8>`
```

How does

```rst
:rfc:`5322#section-3.6.8`
```

renders? it should be picked up automatically by Sphinx but I don't remember how it's being rendered. If it's rendered fine, we can keep a shorter formulation.
created_at: 2025-01-02T13:13:00Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,14 @@
     'compat32',
     ]
 
+# equivalent to pattern re.compile("[!-9;-~]+$")
+valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
+
+def validate_header_name(name):
path: Lib/email/_policybase.py
user: login: bitdancer
body: Well, the function has no leading underscore because it is a "public" function of the `_policybase` module; that is, it is part of the `_policybase` API and won't change.  But it is not a *public* function because `_policybase` is not public/documented.  That said, it wouldn't be crazy to put it in utils and document it.  But let's not do that absent an actual use case.  `_has_surrorages` is in utils because lots of other modules use it.  `validate_header_name` should, in theory, be used only by modules derived from `_policybase`, since the policy is what determines whether a header name is valid.  I'm inclined to leave it here absent a use case, and since it is private it can be more easily moved later if there  is a need.
created_at: 2025-01-05T21:27:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,5 @@
+The :meth:`email.message.Message.add_header` method now validates header
+field names according to :rfc:`RFC 5322, Section 3.6.8 <5322#section-3.6.8>`
+and raises a :exc:`ValueError` if they contain invalid characters. A similar
+argument applies if headers are added through :meth:`Message.__setitem__
+<object.__setitem__>`.
path: Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
user: login: bitdancer
body: I would phrase this as follows:

```
When headers are added to :meth:`email.message.Message` objects, either through
:meth:`email.message.Message.__setitem__` or :meth:`email.message.Message.add_header`,
the field name is now validated according to  :rfc:`RFC 5322, Section 3.6.8 <5322#section-3.6.8>`
and a :exc:`ValueError` is raised if the field name contains any invalid characters.
```
created_at: 2025-03-03T19:29:20Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,12 @@
     'compat32',
     ]
 
+valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
path: Lib/email/_policybase.py
user: login: bitdancer
body: That comment is a tad confusing without any context.  How about `validation regex from RFC, equivalent to ...`
created_at: 2025-03-03T22:15:52Z
author_association: MEMBER
diff_hunk: @@ -728,6 +728,30 @@ def test_nonascii_add_header_with_tspecial(self):
             "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
             msg['Content-Disposition'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for policy in (email.policy.default, email.policy.compat32):
+            for setter in (Message.__setitem__, Message.add_header):
+                for name, value in invalid_headers:
+                    self.do_test_invalid_header_names(policy, setter, name, value)
path: Lib/test/test_email/test_email.py
user: login: bitdancer
body: Technically this should be wrapped <80.
created_at: 2025-03-03T22:21:18Z
author_association: MEMBER
diff_hunk: @@ -1004,6 +1004,30 @@ def test_folding_with_long_nospace_http_policy_1(self):
         parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
         self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for email_policy in (policy.default, policy.compat32):
+            for setter in (EmailMessage.__setitem__, EmailMessage.add_header):
+                for name, value in invalid_headers:
+                    self.do_test_invalid_header_names(email_policy, setter, name, value)
+
+    def do_test_invalid_header_names(self, policy, setter, name, value):
+        with self.subTest(policy=policy, setter=setter, name=name, value=value):
+            message = EmailMessage(policy=policy)
+            pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+            with self.assertRaisesRegex(ValueError, pattern) as cm:
+                 setter(message, name, value)
+            self.assertIn(f"{name!r}", str(cm.exception))
+
path: Lib/test/test_email/test_message.py
user: login: bitdancer
body: Duplicated test?  Did you forget to delete it from test_email?  test_message does seem to be the better location for it.
created_at: 2025-03-03T22:31:51Z
author_association: MEMBER
diff_hunk: @@ -1004,6 +1004,30 @@ def test_folding_with_long_nospace_http_policy_1(self):
         parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
         self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for email_policy in (policy.default, policy.compat32):
+            for setter in (EmailMessage.__setitem__, EmailMessage.add_header):
+                for name, value in invalid_headers:
+                    self.do_test_invalid_header_names(email_policy, setter, name, value)
+
+    def do_test_invalid_header_names(self, policy, setter, name, value):
+        with self.subTest(policy=policy, setter=setter, name=name, value=value):
+            message = EmailMessage(policy=policy)
+            pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+            with self.assertRaisesRegex(ValueError, pattern) as cm:
+                 setter(message, name, value)
+            self.assertIn(f"{name!r}", str(cm.exception))
+
path: Lib/test/test_email/test_message.py
user: login: srinivasreddy
body: I was asked to write tests here as well - Please refer https://github.com/python/cpython/pull/127820#discussion_r1884632081
created_at: 2025-03-05T09:01:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1004,6 +1004,30 @@ def test_folding_with_long_nospace_http_policy_1(self):
         parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
         self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
 
+    def test_invalid_header_names(self):
+        invalid_headers = [
+            ('Invalid Header', 'contains space'),
+            ('Tab\tHeader', 'contains tab'),
+            ('Colon:Header', 'contains colon'),
+            ('', 'Empty name'),
+            (' LeadingSpace', 'starts with space'),
+            ('TrailingSpace ', 'ends with space'),
+            ('Header\x7F', 'Non-ASCII character'),
+            ('Header\x80', 'Extended ASCII'),
+        ]
+        for email_policy in (policy.default, policy.compat32):
+            for setter in (EmailMessage.__setitem__, EmailMessage.add_header):
+                for name, value in invalid_headers:
+                    self.do_test_invalid_header_names(email_policy, setter, name, value)
+
+    def do_test_invalid_header_names(self, policy, setter, name, value):
+        with self.subTest(policy=policy, setter=setter, name=name, value=value):
+            message = EmailMessage(policy=policy)
+            pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
+            with self.assertRaisesRegex(ValueError, pattern) as cm:
+                 setter(message, name, value)
+            self.assertIn(f"{name!r}", str(cm.exception))
+
path: Lib/test/test_email/test_message.py
user: login: bitdancer
body: Ah, right, I forgot that I left Message tests in test_email and only put EmailMessage tests in test_message.  So what you did is correct.
created_at: 2025-03-29T20:56:32Z
author_association: MEMBER
diff_hunk: @@ -14,6 +15,14 @@
     'compat32',
     ]
 
+# validation regex from RFC 5322,equivalent to pattern re.compile("[!-9;-~]+$")
path: Lib/email/_policybase.py
user: login: picnixz
body: ```suggestion
# validation regex from RFC 5322, equivalent to pattern re.compile("[!-9;-~]+$")
```
created_at: 2025-03-30T11:47:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,4 @@
+When headers are added to :meth:`email.message.Message` objects, either through
path: Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
user: login: picnixz
body: ```suggestion
When headers are added to :class:`email.message.Message` objects, either through
```
created_at: 2025-03-30T11:57:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,4 @@
+When headers are added to :class:`email.message.Message` objects, either through
+:meth:`email.message.Message.__setitem__` or :meth:`email.message.Message.add_header`,
+the field name is now validated according to  :rfc:`RFC 5322, Section 3.6.8 <5322#section-3.6.8>`
+and a :exc:`ValueError` is raised if the field name contains any invalid characters.
path: Misc/NEWS.d/next/Library/2024-12-11-17-44-36.gh-issue-127794.VwmRsp.rst
user: login: picnixz
body: ```suggestion
When headers are added to :class:`email.message.Message` objects, either through
:meth:`email.message.Message.__setitem__` or :meth:`email.message.Message.add_header`,
the field name is now validated according to :rfc:`RFC 5322, Section 2.2 <5322#section-2.2>`
and a :exc:`ValueError` is raised if the field name contains any invalid characters.
```

Actually, the definition of a header field name is in §2.2. 3.6.8 was only for optional fields but the requirements are identical for the name (2.2 says "all characters from 33 to 126 except colon and 3.6.8 says all printable characters except space and colon, but space is 32 so it's the same).
created_at: 2025-03-30T12:02:09Z
author_association: MEMBER

PR comments:
user: login: bedevere-app[bot]
created_at: 2024-12-12T20:27:02Z
body: <!-- changes-requested -->
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase `I have made the requested changes; please review again`. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.
<!-- /changes-requested -->

And if you don't make the requested changes, you will be put in the comfy chair!


author_association: NONE
user: login: picnixz
created_at: 2025-01-06T10:42:21Z
body: Great. The suggestion I proposed was meant to be checked with the online docs but RTD is currently failing for all other PRs. Have you tried rendering <code>:rfc:\`5322#section-3.6.8\`</code>? If this renders better (and I think we use this elsewhere), then I would recommend it above the title + ref form that we currently have.

Can you check it locally & show me a screenshot comparing the two? Thanks in advance.
author_association: MEMBER
user: login: picnixz
created_at: 2025-01-06T10:44:07Z
body: Nevermind, we use the long form anyway. No need to change:

```sh
$ git grep -P ':rfc:`.+ <\d{4}#section.+>' Doc/
Doc/howto/logging-cookbook.rst::rfc:`relevant section of the specification <5424#section-6>`.)
Doc/library/codecs.rst:based on the separator characters defined in :rfc:`section 3.1 of RFC 3490 <3490#section-3.1>`
Doc/library/http.client.rst:   an absolute path to conform with :rfc:`RFC 2616 §5.1.2 <2616#section-5.1.2>`
Doc/library/http.client.rst:   with the request. A :rfc:`Host header <2616#section-14.23>`
Doc/library/http.client.rst:   must be provided to conform with :rfc:`RFC 2616 §5.1.2 <2616#section-5.1.2>`
Doc/library/uuid.rst:   :rfc:`RFC 9562, §5.8 <9562#section-5.8>`.
Doc/library/xmlrpc.client.rst:      :rfc:`RFC 2045 section 6.8 <2045#section-6.8
```
author_association: MEMBER
user: login: srinivasreddy
created_at: 2025-01-08T12:13:34Z
body: @picnixz  RTD passed as well. We can merge 🚀 🚀  
author_association: CONTRIBUTOR
user: login: picnixz
created_at: 2025-01-13T11:21:21Z
body: @bitdancer Is there anything else you want the OP to address or is the PR good for you?
author_association: MEMBER
user: login: bitdancer
created_at: 2025-03-03T22:35:10Z
body: Oh, and the bug question: I think it is technically a "maybe breaking change" bugfix, and therefor should not be backported.  Code that "worked" before won't be *improved* by this fix, so better to leave any (unlikely!) possible breakage to a new release only.
author_association: MEMBER
user: login: bedevere-bot
created_at: 2025-03-30T14:26:15Z
body: :warning::warning::warning: Buildbot failure :warning::warning::warning:
------------------------------------------------------------------------

Hi! The buildbot **aarch64 Fedora Stable LTO 3.x** (tier-2) has failed when building commit c432d0147bdf1a66604e7a3d6a71660ae79b5f45.

What do you need to do:

1. Don't panic.
2. Check [the buildbot page in the devguide](https://devguide.python.org/buildbots/) if you don't know what the buildbots are or how they work.
3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/336/builds/6595) and take a look at the build logs.
4. Check if the failure is related to this commit (c432d0147bdf1a66604e7a3d6a71660ae79b5f45) or if it is a false positive.
5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/336/builds/6595

Failed tests:

- test_perf_profiler

Failed subtests:

- test_python_calls_appear_in_the_stack_if_perf_activated - test.test_perf_profiler.TestPerfProfilerWithDwarf.test_python_calls_appear_in_the_stack_if_perf_activated


Summary of the results of the build (if available):

== 

<details>
<summary>Click to see traceback logs</summary>

```python-traceback
Traceback (most recent call last):
  File "/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/Lib/test/test_perf_profiler.py", line 364, in test_python_calls_appear_in_the_stack_if_perf_activated
    self.assertIn(f"py::foo:{script}", stdout)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 'py::foo:/tmp/test_python__owl0o7b/tmpe64hz943/perftest.py' not found in 'python 2537981 500356.758635:          1 cycles:Pu: \n\t    ffff82e61ac0 _start+0x0 (/usr/lib/ld-linux-aarch64.so.1)\n\npython 2537981 500356.758659:          1 cycles:Pu: \n\t    ffff82e61ac0 _start+0x0 (/usr/lib/ld-linux-aarch64.so.1)\n\npython 2537981 500356.759050:          1 cycles:Pu: \n\t    ffff82e4ed8c do_lookup_x+0x10c (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e4f833 _dl_lookup_symbol_x+0x113 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e536ff resolve_map+0x79f (inlined)\n\t    ffff82e536ff elf_machine_rela+0x79f (inlined)\n\t    ffff82e536ff elf_dynamic_do_Rela+0x79f (inlined)\n\t    ffff82e536ff _dl_relocate_object+0x79f (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e5f3d3 dl_main+0x15d3 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e5c5ff _dl_sysdep_start+0x1df (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e5db17 _dl_start_final+0x5ab (inlined)\n\t    ffff82e5db17 _dl_start+0x5ab (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e61ad3 _start+0x13 (/usr/lib/ld-linux-aarch64.so.1)\n\npython 2537981 500356.759069:        428 cycles:Pu: \n\t    ffff82e4efd0 do_lookup_x+0x350 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e4f833 _dl_lookup_symbol_x+0x113 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e536ff resolve_map+0x79f (inlined)\n\t    ffff82e536ff elf_machine_rela+0x79f (inlined)\n\t    ffff82e536ff elf_dynamic_do_Rela+0x79f (inlined)\n\t    ffff82e536ff _dl_relocate_object+0x79f (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e5f3d3 dl_main+0x15d3 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e5c5ff _dl_sysdep_start+0x1df (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e5db17 _dl_start_final+0x5ab (inlined)\n\t    ffff82e5db17 _dl_start+0x5ab (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffff82e61ad3 _start+0x13 (/usr/lib/ld-linux-aarch64.so.1)\n\npython 2537981 500356.760078:       1755 cycles:Pu: \n\t          4437c4 mult+0x120 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          6c4f6b _PyDtoa_Init+0xab (inlined)\n\t          6c4f6b pycore_interp_init+0xab (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7292ff pyinit_config+0xb4f (inlined)\n\t          7292ff pyinit_core.constprop.0+0xb4f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7294b7 Py_InitializeFromConfig+0x37 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729c9b pymain_init+0x137 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729d57 pymain_main+0x27 (inlined)\n\t          729d57 Py_BytesMain+0x27 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t    ffff82bf625b __libc_start_call_main+0x7b (/usr/lib64/libc.so.6)\n\t    ffff82bf633b __libc_start_main
rch64.lto/build/python)\n\t          7294b7 Py_InitializeFromConfig+0x37 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729c9b pymain_init+0x137 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729d57 pymain_main+0x27 (inlined)\n\t          729d57 Py_BytesMain+0x27 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t    ffff82bf625b __libc_start_call_main+0x7b (/usr/lib64/libc.so.6)\n\t    ffff82bf633b __libc_start_main@@GLIBC_2.34+0x9b (/usr/lib64/libc.so.6)\n\t          41f42f _start+0x2f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\npython 2537981 500356.761198:     218500 cycles:Pu: \n\t          520e38 managed_static_type_state_get+0x1a54 (inlined)\n\t          520e38 _PyStaticType_GetState+0x1a54 (inlined)\n\t          520e38 lookup_tp_dict+0x1a54 (inlined)\n\t          520e38 lookup_tp_dict+0x1a54 (inlined)\n\t          520e38 type_add_members+0x1a54 (inlined)\n\t          520e38 type_ready_fill_dict+0x1a54 (inlined)\n\t          520e38 type_ready+0x1a54 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          6bf63f init_static_type+0x55f (inlined)\n\t          6bf63f _PyStaticType_InitBuiltin+0x55f (inlined)\n\t          6bf63f _PyExc_InitTypes+0x55f (inlined)\n\t          6bf63f pycore_init_types+0x55f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          6c4fc3 pycore_interp_init+0x103 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7292ff pyinit_config+0xb4f (inlined)\n\t          7292ff pyinit_core.constprop.0+0xb4f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7294b7 Py_InitializeFromConfig+0x37 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729c9b pymain_init+0x137 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729d57 pymain_main+0x27 (inlined)\n\t          729d57 Py_BytesMain+0x27 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t    ffff82bf625b __libc_start_call_main+0x7b (/usr/lib64/libc.so.6)\n\t    ffff82bf633b __libc_start_main@@GLIBC_2.34+0x9b (/usr/lib64/libc.so.6)\n\t          41f42f _start+0x2f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\npython 2537981 500356.762658:    2932104 cycles:Pu: \n\t    ffff82c7ac14 __strlen_asimd+0x14 (/usr/lib64/libc.so.6)\n\t          6be9ef _PyUnicode_EqualToASCIIString+0x10f (inlined)\n\t          6be9ef create_builtin+0x10f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          6c4c6f bootstrap_imp+0x14b (inlined)\n\t          6c4c6f init_importlib+0x14b (inlined)\n\t          6c4c6f _PyImport_InitCore+0x14b (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          6c7a5b pycore_interp_init+0x2b9b (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7292ff pyinit_config+0xb4f (inlined)\n\t          7292ff pyinit_core.constprop.0+0xb4f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7294b7 Py_InitializeFromConfig+0x37 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729c9b pymain_init+0x137 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729d57 pymain_main+0x27 (inlined)\n\t          729d57 Py_BytesMain+0x27 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t    ffff82bf625b __libc_start_call_main+0x7b (/usr/lib64/libc.so.6)\n\t    ffff82bf633b __libc_start_main@@GLIBC_2.34+0x9b (/usr/lib64/libc.so.6)\n\t          41f42f _start+0x2f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\npython 2537981 500356.766352:    7635427 cycles:Pu: \n\t          53846c Py_DECREF+0x9c (inlined)\n\t          53846c


Traceback (most recent call last):
  File "/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/Lib/test/test_perf_profiler.py", line 364, in test_python_calls_appear_in_the_stack_if_perf_activated
    self.assertIn(f"py::foo:{script}", stdout)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 'py::foo:/tmp/test_python_6x8nqwfr/tmpn1pyajvj/perftest.py' not found in 'python 2512322 500122.895564:          1 cycles:Pu: \n\tffffaa282d37fc78 [unknown] ([unknown])\n\tffffaa282d38049c [unknown] ([unknown])\n\tffffaa282bf215e4 [unknown] ([unknown])\n\t    ffffbdafcac0 _start+0x0 (/usr/lib/ld-linux-aarch64.so.1)\n\npython 2512322 500122.895622:          1 cycles:Pu: \n\t    ffffbdafcac0 _start+0x0 (/usr/lib/ld-linux-aarch64.so.1)\n\npython 2512322 500122.896080:          1 cycles:Pu: \n\t    ffffbdaee65c resolve_map+0x6fc (inlined)\n\t    ffffbdaee65c elf_machine_rela+0x6fc (inlined)\n\t    ffffbdaee65c elf_dynamic_do_Rela+0x6fc (inlined)\n\t    ffffbdaee65c _dl_relocate_object+0x6fc (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdafa3d3 dl_main+0x15d3 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdaf75ff _dl_sysdep_start+0x1df (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdaf8b17 _dl_start_final+0x5ab (inlined)\n\t    ffffbdaf8b17 _dl_start+0x5ab (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdafcad3 _start+0x13 (/usr/lib/ld-linux-aarch64.so.1)\n\npython 2512322 500122.896101:        385 cycles:Pu: \n\t    ffffbdae9d84 do_lookup_x+0x104 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdaea833 _dl_lookup_symbol_x+0x113 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdaee6ff resolve_map+0x79f (inlined)\n\t    ffffbdaee6ff elf_machine_rela+0x79f (inlined)\n\t    ffffbdaee6ff elf_dynamic_do_Rela+0x79f (inlined)\n\t    ffffbdaee6ff _dl_relocate_object+0x79f (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdafa3d3 dl_main+0x15d3 (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdaf75ff _dl_sysdep_start+0x1df (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdaf8b17 _dl_start_final+0x5ab (inlined)\n\t    ffffbdaf8b17 _dl_start+0x5ab (/usr/lib/ld-linux-aarch64.so.1)\n\t    ffffbdafcad3 _
bjects+0x1d43 (inlined)\n\t          6c6c03 pycore_init_global_objects+0x1d43 (inlined)\n\t          6c6c03 pycore_interp_init+0x1d43 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7292ff pyinit_config+0xb4f (inlined)\n\t          7292ff pyinit_core.constprop.0+0xb4f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7294b7 Py_InitializeFromConfig+0x37 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729c9b pymain_init+0x137 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729d57 pymain_main+0x27 (inlined)\n\t          729d57 Py_BytesMain+0x27 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t    ffffbd89625b __libc_start_call_main+0x7b (/usr/lib64/libc.so.6)\n\t    ffffbd89633b __libc_start_main@@GLIBC_2.34+0x9b (/usr/lib64/libc.so.6)\n\t          41f42f _start+0x2f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\npython 2512322 500122.899615:     158658 cycles:Pu: \n\t          6bf1b0 _Py_IsMainInterpreter+0xd0 (inlined)\n\t          6bf1b0 _PyStaticType_InitBuiltin+0xd0 (inlined)\n\t          6bf1b0 _PyTypes_InitTypes+0xd0 (inlined)\n\t          6bf1b0 pycore_init_types+0xd0 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          6c4fc3 pycore_interp_init+0x103 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7292ff pyinit_config+0xb4f (inlined)\n\t          7292ff pyinit_core.constprop.0+0xb4f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7294b7 Py_InitializeFromConfig+0x37 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729c9b pymain_init+0x137 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          729d57 pymain_main+0x27 (inlined)\n\t          729d57 Py_BytesMain+0x27 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t    ffffbd89625b __libc_start_call_main+0x7b (/usr/lib64/libc.so.6)\n\t    ffffbd89633b __libc_start_main@@GLIBC_2.34+0x9b (/usr/lib64/libc.so.6)\n\t          41f42f _start+0x2f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\npython 2512322 500122.900877:    2120226 cycles:Pu: \n\t          5337ac _PyDict_GetItemRef_KnownHash+0x19c (inlined)\n\t          5337ac find_name_in_mro+0x19c (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          535b8f update_one_slot+0x8f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          621e47 fixup_slot_dispatchers+0xf17 (inlined)\n\t          621e47 type_new_impl+0xf17 (inlined)\n\t          621e47 type_new+0xf17 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          4f727f type_call+0x57 (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          53d863 _PyObject_MakeTpCall+0xdb (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          4937d3 _PyObject_VectorcallTstate+0x1af (inlined)\n\t          4937d3 _PyObject_CallFunctionVa+0x1af (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          49390b PyObject_CallFunction+0x6b (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          57100f PyErr_NewException+0xef (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          6c7a3f init_heap_exctypes+0x2b7f (inlined)\n\t          6c7a3f _Py_xi_state_init+0x2b7f (inlined)\n\t          6c7a3f _PyXI_Init+0x2b7f (inlined)\n\t          6c7a3f pycore_interp_init+0x2b7f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.lto/build/python)\n\t          7292ff pyinit_config+0xb4f (inlined)\n\t          7292ff pyinit_core.constprop.0+0xb4f (/home/buildbot/buildarea/3.x.cstratak-fedora-stable-aarch64.

```

</details>

author_association: NONE