Repository navigation
gateway, pycode: split the fsencode boundary; report the byte-exact co_filename from every reader - #1018
Conversation
WalkthroughThe interpreter now preserves filesystem bytes and surrogate-escaped filenames across path conversion, traceback and frame diagnostics, warnings, coroutine origins, and AF_UNIX socket addresses. Parity tests cover embedded NUL rejection, abstract sockets, and diagnostic output. ChangesFilesystem surrogateescape preservation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant PythonSocket
participant AF_UNIXPacking
participant Kernel
participant AF_UNIXUnpacking
PythonSocket->>AF_UNIXPacking: provide filesystem or abstract address
AF_UNIXPacking->>Kernel: bind or send sockaddr with calculated length
Kernel-->>AF_UNIXUnpacking: return sockaddr and address length
AF_UNIXUnpacking-->>PythonSocket: preserve abstract bytes or decode filesystem path
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🤖 Codex parity reviewStatic analysis of this diff vs the local RPython/PyPy sources (commit 71298bc). Files in the reviewed diff1. Regressions to PyPy parity introduced by this patchNone. 2. Other mismatches introduced by this patchNone. 3. Pre-existing mismatches (already present before this patch)
4. Structural adaptations
|
|
Adjudicated the Codex parity review. Three findings fixed, one refuted, one deferred with the blocker named. §1 — refuted
CPython does diverge: it accepts the name and stores §3 — two fixed, one deferred
§4 — documented, no changeNon-stable Verification
— commented by Claude |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pyre/extra_tests/parity_tests/traceback_filename_surrogateescape.py`:
- Around line 66-78: Strengthen the assertions after the subprocess invocation
in the traceback test: verify that child.returncode is nonzero, and require the
expected filename value from the _imp._fix_co_filename call to appear in stderr,
while retaining the surrogateescape decoding and U+FFFD check.
In `@pyre/pyre-interpreter/src/module/_socket/interp_socket.rs`:
- Around line 2205-2218: Update the AF_UNIX address handling around
abstract_name to recognize a leading NUL on both Linux and Android, then reject
any NUL occurring in all other paths before copying bytes into sun.sun_path.
Ensure Android abstract names use abstract sizing and addrlen without a
regular-path terminator, while preserving the existing capacity and error
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 41e4aefc-b739-4288-b70d-b91cd95cc77e
📒 Files selected for processing (11)
pyre/extra_tests/parity_tests/os_boundary_surrogateescape.pypyre/extra_tests/parity_tests/traceback_filename_surrogateescape.pypyre/pyre-interpreter/src/builtins.rspyre/pyre-interpreter/src/display.rspyre/pyre-interpreter/src/error.rspyre/pyre-interpreter/src/gateway.rspyre/pyre-interpreter/src/module/_socket/interp_socket.rspyre/pyre-interpreter/src/module/_warnings/mod.rspyre/pyre-interpreter/src/pycode.rspyre/pyre-interpreter/src/pyframe.rspyre/pyre-interpreter/src/typedef.rs
| child = subprocess.run( | ||
| [ | ||
| sys.executable, | ||
| "-c", | ||
| "import _imp\n" | ||
| "code = compile(\"raise ValueError('boom')\", 'orig.py', 'exec')\n" | ||
| "_imp._fix_co_filename(code, 'x\\udcff.py')\n" | ||
| "exec(code)\n", | ||
| ], | ||
| capture_output=True, | ||
| ) | ||
| stderr = child.stderr.decode(errors="surrogateescape") | ||
| assert "�" not in stderr, ascii(stderr) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Assert the child failure and the exact filename.
Lines 77-78 only reject U+FFFD. An empty stderr or a successful child process passes this test. Assert a nonzero return code and require FILENAME in stderr.
Proposed test fix
child = subprocess.run(
[
sys.executable,
@@
],
capture_output=True,
+ check=False,
)
stderr = child.stderr.decode(errors="surrogateescape")
+ assert child.returncode != 0
+ assert FILENAME in stderr, ascii(stderr)
assert "�" not in stderr, ascii(stderr)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| child = subprocess.run( | |
| [ | |
| sys.executable, | |
| "-c", | |
| "import _imp\n" | |
| "code = compile(\"raise ValueError('boom')\", 'orig.py', 'exec')\n" | |
| "_imp._fix_co_filename(code, 'x\\udcff.py')\n" | |
| "exec(code)\n", | |
| ], | |
| capture_output=True, | |
| ) | |
| stderr = child.stderr.decode(errors="surrogateescape") | |
| assert "�" not in stderr, ascii(stderr) | |
| child = subprocess.run( | |
| [ | |
| sys.executable, | |
| "-c", | |
| "import _imp\n" | |
| "code = compile(\"raise ValueError('boom')\", 'orig.py', 'exec')\n" | |
| "_imp._fix_co_filename(code, 'x\\udcff.py')\n" | |
| "exec(code)\n", | |
| ], | |
| capture_output=True, | |
| check=False, | |
| ) | |
| stderr = child.stderr.decode(errors="surrogateescape") | |
| assert child.returncode != 0 | |
| assert FILENAME in stderr, ascii(stderr) | |
| assert "�" not in stderr, ascii(stderr) |
🧰 Tools
🪛 Ruff (0.16.0)
[warning] 66-66: subprocess.run without explicit check argument
Add explicit check=False
(PLW1510)
[warning] 70-73: Unparenthesized implicit string concatenation in collection
Did you forget a comma?
(ISC004)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pyre/extra_tests/parity_tests/traceback_filename_surrogateescape.py` around
lines 66 - 78, Strengthen the assertions after the subprocess invocation in the
traceback test: verify that child.returncode is nonzero, and require the
expected filename value from the _imp._fix_co_filename call to appear in stderr,
while retaining the surrogateescape decoding and U+FFFD check.
Source: Linters/SAST tools
| let abstract_name = cfg!(target_os = "linux") && path_bytes_vec.first() == Some(&0); | ||
| // `rsocket.py:412-420 UNIXAddress.__init__`: an abstract name may fill | ||
| // `sun_path` exactly, a regular one has to leave room for its | ||
| // terminator. | ||
| let capacity = sun.sun_path.len() - usize::from(!abstract_name); | ||
| if path_bytes_vec.len() > capacity { | ||
| return Err(crate::PyError::os_error("AF_UNIX path too long")); | ||
| } | ||
| for (i, &b) in path_bytes_vec.iter().enumerate() { | ||
| sun.sun_path[i] = b as libc::c_char; | ||
| } | ||
| return Ok(( | ||
| storage, | ||
| (core::mem::size_of::<libc::sa_family_t>() + path_bytes_vec.len() + 1) | ||
| as libc::socklen_t, | ||
| )); | ||
| // `rsocket.py:409-410 self.setdata(sun, baseofs + len(path))`: the | ||
| // terminator is counted only for a regular name that has one. | ||
| let addrlen = SUN_PATH_OFFSET + path_bytes_vec.len() + usize::from(!abstract_name); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reject NULs in regular AF_UNIX names.
A non-abstract path such as b"a\0b" passes this branch and reaches libc. The OS then treats the NUL as the pathname boundary instead of raising the required ValueError.
Treat a leading NUL as abstract on both Linux and Android. Reject any NUL in every other path. This also prevents Android abstract names from receiving a regular-path terminator.
Proposed fix
- let abstract_name = cfg!(target_os = "linux") && path_bytes_vec.first() == Some(&0);
+ let abstract_name =
+ cfg!(any(target_os = "linux", target_os = "android"))
+ && path_bytes_vec.first() == Some(&0);
+ if !abstract_name && path_bytes_vec.contains(&0) {
+ return Err(crate::PyError::value_error("embedded null byte"));
+ }As per coding guidelines, “Preserve interpreter semantics exactly in the generated JIT.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let abstract_name = cfg!(target_os = "linux") && path_bytes_vec.first() == Some(&0); | |
| // `rsocket.py:412-420 UNIXAddress.__init__`: an abstract name may fill | |
| // `sun_path` exactly, a regular one has to leave room for its | |
| // terminator. | |
| let capacity = sun.sun_path.len() - usize::from(!abstract_name); | |
| if path_bytes_vec.len() > capacity { | |
| return Err(crate::PyError::os_error("AF_UNIX path too long")); | |
| } | |
| for (i, &b) in path_bytes_vec.iter().enumerate() { | |
| sun.sun_path[i] = b as libc::c_char; | |
| } | |
| return Ok(( | |
| storage, | |
| (core::mem::size_of::<libc::sa_family_t>() + path_bytes_vec.len() + 1) | |
| as libc::socklen_t, | |
| )); | |
| // `rsocket.py:409-410 self.setdata(sun, baseofs + len(path))`: the | |
| // terminator is counted only for a regular name that has one. | |
| let addrlen = SUN_PATH_OFFSET + path_bytes_vec.len() + usize::from(!abstract_name); | |
| let abstract_name = | |
| cfg!(any(target_os = "linux", target_os = "android")) | |
| && path_bytes_vec.first() == Some(&0); | |
| if !abstract_name && path_bytes_vec.contains(&0) { | |
| return Err(crate::PyError::value_error("embedded null byte")); | |
| } | |
| // `rsocket.py:412-420 UNIXAddress.__init__`: an abstract name may fill | |
| // `sun_path` exactly, a regular one has to leave room for its | |
| // terminator. | |
| let capacity = sun.sun_path.len() - usize::from(!abstract_name); | |
| if path_bytes_vec.len() > capacity { | |
| return Err(crate::PyError::os_error("AF_UNIX path too long")); | |
| } | |
| for (i, &b) in path_bytes_vec.iter().enumerate() { | |
| sun.sun_path[i] = b as libc::c_char; | |
| } | |
| // `rsocket.py:409-410 self.setdata(sun, baseofs + len(path))`: the | |
| // terminator is counted only for a regular name that has one. | |
| let addrlen = SUN_PATH_OFFSET + path_bytes_vec.len() + usize::from(!abstract_name); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pyre/pyre-interpreter/src/module/_socket/interp_socket.rs` around lines 2205
- 2218, Update the AF_UNIX address handling around abstract_name to recognize a
leading NUL on both Linux and Android, then reject any NUL occurring in all
other paths before copying bytes into sun.sun_path. Ensure Android abstract
names use abstract sizing and addrlen without a regular-path terminator, while
preserving the existing capacity and error behavior.
Source: Coding guidelines
…ncode `fsencode_bytes_w` now follows `baseobjspace.py:1970-1983 fsencode_w`: it resolves a readable buffer before `__fspath__`, warns `path should be string, bytes, or os.PathLike, not <type>` as a `DeprecationWarning` for a buffer that is not `bytes`, names the types in both `TypeError`s (`interp_posix.py:3043` and `:3053`), and rejects an embedded null with `ValueError: embedded null byte` (`baseobjspace.py:2005-2018 bytesbuf0_w`). `gateway::fsencode` is `baseobjspace.py:1962 space.fsencode` with none of that. The AF_UNIX address packer takes it, per `interp_socket.py:157`'s note that Linux allows embedded NULs in an abstract-namespace path. `open()` drops its own str / bytes-like / `__fspath__` resolution for the converter, the way `interp_fileio.py:134-137 _open_fd` reaches `interp_posix.py:64 space.fsencode_w` through `dispatch_filename`. Assisted-by: Claude
…ce_path `w_code_filename_obj` realizes `pycode.py:135 self.co_filename` the way `w_code_name_obj` realizes `co_name`, decoding the owned filesystem bytes with `objspace.py:438 newfilename`. It does not cache the result on the `PyCode`: the wrapper is prebuilt-immortal and the decode hands back a movable managed string. `code_get_field`'s `co_filename` arm now goes through it. The consumers that previously read `CodeObject::source_path` — the lossy UTF-8 spelling, where a path byte with no UTF-8 form has already become U+FFFD — take the exact name instead: the warning location, a coroutine's `cr_origin` summary, the rendered traceback and `pyframe.py:849-853 descr_repr`. The traceback writer keys its `StackSummary.format` dedup on the filename bytes and emits them with `write_all`; `read_source_line` keeps the lossy spelling, which simply fails to resolve, as `error.py:150 linecache.getline` does when the source cannot be read. `descr_repr` returns a `Wtf8Buf`, since a Rust `String` cannot hold the lone surrogate, and `py_repr_wtf8` takes it directly rather than through `py_repr`'s `String`. Assisted-by: Claude
`interp_posix.py:3049-3051 fspath` narrows the result to those two; a `bytearray` exports a readable buffer but is not a path, so the resolution now reports `expected <type>.__fspath__() to return str or bytes, not bytearray` instead of taking its bytes. Assisted-by: Claude
… bytes `read_source_line` took the lossy UTF-8 spelling, so a path byte with no UTF-8 form reached the source provider as U+FFFD and named a file that does not exist. `error.py:150 linecache.getline` is handed `co_filename` itself, whose surrogateescape spelling reopens the byte-exact path, so the bytes are carried through and turned into a `Path` with `OsStr::from_bytes`. Platforms with no bytes spelling of a path keep the lossy form, where such a name does not resolve either way. Assisted-by: Claude
… name An AF_UNIX name is not always NUL-terminated: on Linux a path whose first byte is `\0` names the abstract namespace, where the name runs to the end of the address, embedded NULs included, and its length is only derivable from the address length. `unpack_inet_addr` now takes that length and follows `rsocket.py:430-443 UNIXAddress.get_path`: `maxlength` is the address length past `offsetof(sockaddr_un, sun_path)`, an abstract name takes all of it, and a regular name is the NUL scan bounded by it. `interp_socket.py:41-47` returns the abstract name as `bytes` and the regular one through the filesystem decoding. Previously the scan stopped at byte 0, so `getsockname()` on an abstract socket answered `''`. Each of the nine callers passes the length the OS wrote back — `slen` for `accept`/`recvfrom`/`getsockname`/`getpeername`, `msg_namelen` for the two `recvmsg` forms, `ai_addrlen` for `getaddrinfo`. `pack_inet_addr` reports `rsocket.py:409-410`'s `baseofs + len(path)`, counting the terminator only for a regular name that has one, and takes the offset from `sockaddr_un` rather than from `sa_family_t`, which is not the same on every platform. An abstract name may fill `sun_path` exactly (`rsocket.py:412-420`). Assisted-by: Claude
There was a problem hiding this comment.
💡 Codex Review
https://github.com/youknowone/pyre/blob/71298bcfe33a52d1dbc6b8cd1d4c7e37e91bf310/pyre-interpreter/src/error.rs#L2749
Preserve the filename when writing through sys.stderr
When this traceback is routed through a live Python text stream—for example, by calling sys.__excepthook__ with sys.stderr = io.StringIO()—writing the raw filename byte here loses the surrogate: builtins.rs:3940 converts the rendered buffer with String::from_utf8_lossy, so x\udcff.py becomes x�.py before stderr.write; write_unraisable_to_sys_stderr has the same conversion at error.rs:1424. Render the filename as WTF-8/text through these paths (as upstream's text formatting does), rather than inserting filesystem bytes into a buffer later decoded as UTF-8.
AGENTS.md reference: AGENTS.md:L194-L196
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pyre/pyre-interpreter/src/pycode.rs (1)
2018-2063: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winFree
filename_bytesinpycode_destructor.
set_filename_bytesstores the custom filename infilename_bytesviaBox::into_raw, sopycode_destructorshould drop thatBoxto keep custom filename allocations from leaking when the GC reclaims thePyCodewrapper.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pyre/pyre-interpreter/src/pycode.rs` around lines 2018 - 2063, Update pycode_destructor to release the allocation held by code.filename_bytes, mirroring the existing Box::from_raw cleanup for globals_caches, mapdict_caches, and co_consts_w. Guard the cleanup for a null pointer and reset the field afterward.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pyre/extra_tests/parity_tests/traceback_filename_surrogateescape.py`:
- Around line 42-43: Update the assertions after constructing frame_repr to
first verify that it contains the expected FILENAME value, then retain the
assertion rejecting U+FFFD. Ensure the test fails when the frame reports orig.py
instead of the expected filename.
---
Outside diff comments:
In `@pyre/pyre-interpreter/src/pycode.rs`:
- Around line 2018-2063: Update pycode_destructor to release the allocation held
by code.filename_bytes, mirroring the existing Box::from_raw cleanup for
globals_caches, mapdict_caches, and co_consts_w. Guard the cleanup for a null
pointer and reset the field afterward.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 770cab40-d7b7-4627-a5ae-627b4e62377a
📒 Files selected for processing (11)
pyre/extra_tests/parity_tests/os_boundary_surrogateescape.pypyre/extra_tests/parity_tests/traceback_filename_surrogateescape.pypyre/pyre-interpreter/src/builtins.rspyre/pyre-interpreter/src/display.rspyre/pyre-interpreter/src/error.rspyre/pyre-interpreter/src/gateway.rspyre/pyre-interpreter/src/module/_socket/interp_socket.rspyre/pyre-interpreter/src/module/_warnings/mod.rspyre/pyre-interpreter/src/pycode.rspyre/pyre-interpreter/src/pyframe.rspyre/pyre-interpreter/src/typedef.rs
| frame_repr = repr(namespace["frame"]) | ||
| assert "�" not in frame_repr, ascii(frame_repr) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the expected frame filename.
Line 43 only rejects U+FFFD. A frame representation that still reports orig.py passes. Assert FILENAME before the replacement-character assertion.
Proposed fix
frame_repr = repr(namespace["frame"])
+assert FILENAME in frame_repr, ascii(frame_repr)
assert "�" not in frame_repr, ascii(frame_repr)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| frame_repr = repr(namespace["frame"]) | |
| assert "�" not in frame_repr, ascii(frame_repr) | |
| frame_repr = repr(namespace["frame"]) | |
| assert FILENAME in frame_repr, ascii(frame_repr) | |
| assert "�" not in frame_repr, ascii(frame_repr) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pyre/extra_tests/parity_tests/traceback_filename_surrogateescape.py` around
lines 42 - 43, Update the assertions after constructing frame_repr to first
verify that it contains the expected FILENAME value, then retain the assertion
rejecting U+FFFD. Ensure the test fails when the frame reports orig.py instead
of the expected filename.
|
Rebased onto Attributionmain's own CI run at the same base sha (
Diffing the two ubuntu FAIL lists leaves nothing on either side — same benches, same counters, same numbers ( That is what you would expect from #950: it moved I deliberately did not re-record any baseline here — that would absorb main's drift into this diff. Rebase notesTwo conflicts, both blended rather than taken from one side:
The other three commits are byte-identical across the rebase (same patch-ids). Verification after the rebaseLLBC re-extracted, release rebuilt, full — commented by Claude |
Follow-up to #1001, closing the two items its review left open.
1. Split the fsencode boundary into the converter and the raw encode
Upstream has two distinct things here and the difference is load-bearing:
baseobjspace.py:1970-1983 space.fsencode_w— the argument converter. It warnspath should be %s, not %sas aDeprecationWarningfor a readable bufferthat is not
bytes, and ends inbytesbuf0_w(:2005-2018), which rejects anembedded null with
ValueError: embedded null byte.baseobjspace.py:1962 space.fsencode— the plain unicode→bytes filesystemencode, with neither.
interp_socket.py:157takes this one deliberately:"Not using space.fsencode_w since Linux allows embedded NULs."
pyre collapsed both into one encoder, so it had neither the warning nor the null
check anywhere, and could not express the socket carve-out at all.
fsencode_bytes_wnow followsfsencode_w: it resolves a readable buffer before__fspath__, warns for a buffer that is notbytes, names the types in bothTypeErrors (interp_posix.py:3043and:3053), and applies the null check.gateway::fsencodeis the raw form, and the AF_UNIX address packer takes it.open()drops its own hand-rolled str / bytes-like /__fspath__resolution forthe converter, the way
interp_fileio.py:134-137 _open_fdreachesinterp_posix.py:64 space.fsencode_wthroughdispatch_filename.The four resulting messages match CPython 3.14 character for character:
2. Report the byte-exact
co_filenamefrom every readerPyCodeowns the raw path (pycode.py:135 self.co_filename = filename) and theco_filenamegetter decodes it withobjspace.py:438 newfilename. Every otherreader still went to
CodeObject::source_path— the lossy UTF-8 spelling, wherea path byte with no UTF-8 form has already become U+FFFD. So a module imported
from
x\udcff.pyshowedx<?>.pyin its traceback, its warnings and its framerepr while
code.co_filenameshowed the correct name.w_code_filename_objrealizes the filename the wayw_code_name_objrealizesco_name. It deliberately does not cache on thePyCode: the wrapper isprebuilt-immortal and the decode hands back a movable managed string, which is
exactly the combination the
filename_bytesfield doc rules out.Routed through it: the warning location, a coroutine's
cr_originsummary, therendered traceback, and
pyframe.py:849-853 descr_repr.The last two were the half that looked blocked, because a Rust
Stringcannothold the lone surrogate:
std::io::Write, so it keys itsStackSummary.formatdedup on the filename bytes and emits them withwrite_all.read_source_linekeeps the lossy spelling, which simply fails toresolve —
error.py:150 linecache.getlinerenders nothing then too.descr_reprreturns aWtf8Buf, andpy_repr_wtf8takes it directly insteadof folding through
py_repr'sString. Upstream interpolates the filename(
"%s" % code.co_filename) rather than its repr, so the surrogate reaches theresult unescaped; CPython spells the same field with
%R. Both keep the byte.Verification
All four readers now report the exact name (
x\udcff.py), and the traceback'sbytes are
78 ff 2e 70 79:New parity test
traceback_filename_surrogateescape.py, plus assertions added toos_boundary_surrogateescape.pyfor the null boundary and for the AF_UNIXabstract-namespace carve-out. Both green on CPython 3.14 and on pyre.
🤖 Generated with Claude Code
Summary by CodeRabbit