Fix UnicodeEncodeError for non-ASCII file paths (#1086) - #1087
Merged
Conversation
pywhere.cpp converted code->co_filename with PyUnicode_AsASCIIString on three paths: the stack walker used by the allocator interposer, on_stack(), and the settrace line callback. For any path containing a non-ASCII character -- e.g. a module at .../überschüsse.py -- that call fails, returning NULL *and leaving a UnicodeEncodeError set on the thread*. Since these run from the native allocator hook and from trace callbacks, the stray exception surfaced later at an arbitrary, unrelated Python line, which is why the reporter saw it land in sysconfig.get_path on one run and inside pydantic on the next. Two of the sites also fed the NULL straight into PyBytes_AsString/strstr, giving the reported SIGSEGV. Replace all three with a new encodeFilename() helper that encodes as UTF-8 with surrogate escapes -- the exact inverse of how CPython decodes filesystem paths, so every path Python can represent (including undecodable bytes, which appear as lone surrogates) round-trips. It clears any exception on failure and returns nullptr; every call site now NULL-checks. Also drops a dead encode-and-discard in trace_func, and fixes the same pending-exception leak in TraceConfig's package-path encode, which used "strict". The Python side compounded the problem by decoding native sample records as ASCII: add decode_sample() in scalene_mapfile.py using the matching UTF-8/surrogateescape pair. Verified by reproducing the crash (3/3 runs) on the unfixed build, then confirming a complete profile with the fix, on both --memory and --memory --stacks. tests/test_issue1086_non_ascii_paths.py is picked up by the existing pytest step in tests.yml, so it runs on Ubuntu + macOS across Python 3.9-3.14. It covers decode_sample directly (deterministic, including lone surrogates) and profiles a program importing a non-ASCII module end to end, with the non-ASCII component both in the filename and in a parent directory. The crash assertions are hard on every attempt; only the "file appears in the profile" check retries against sampling flake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reporter's case was "a module in its import tree which is at a path with non-ascii characters in it" -- the umlaut need not be in the filename at all, since the native code encoded the whole path. The previous parametrization only varied whether a non-ASCII *parent* was added on top of an already non-ASCII filename, so it never isolated that spelling. Replace it with three explicit layouts: leaf-only (ASCII directories), directory-only (ASCII module and immediate parent, umlaut solely in an interior directory: optionale_verlängerung/pakete/workload.py), and both. Extend the deterministic decode_sample test the same way. Verified the new directory-only case against the parent commit's native code: it fails there with "'ascii' codec can't encode character '\xe4'", the ä from the interior directory, on an all-ASCII workload.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
|
Added a The three layouts are now explicit:
The native code encoded the whole path, so a single non-ASCII character anywhere in it was enough. Verified That |
Picks up the get_ipython() Optional guard (#1088) so this branch's linters check runs against a fixed master.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1086.
Root cause
pywhere.cppconvertedcode->co_filenamewithPyUnicode_AsASCIIStringon three paths: the stack walker used by the allocator interposer,on_stack(), and the settrace line callback. For any path containing a non-ASCII character — e.g. a module at.../überschüsse.py— that call fails, returningNULLand leaving aUnicodeEncodeErrorset on the thread.Since these run from the native allocator hook and from trace callbacks, the stray exception surfaced later at an arbitrary, unrelated Python line — which is why the reporter saw it land in
sysconfig.get_pathon one run and inside pydantic on the next, and why the failure looked flaky. Two of the sites also fed theNULLstraight intoPyBytes_AsString/strstr, giving the reported SIGSEGV.The Python side compounded it:
ScaleneMapFile.get_str()decoded the native sample records as ASCII, so any allocation attributed to such a file also blew up the sample-draining loop.Only
--memoryruns were affected, matching the reporter's observation that--cpu-onlyworked — that's the mode that loads the native interposer.Changes
src/source/pywhere.cpp— newencodeFilename()helper encoding as UTF-8 withsurrogateescape. That's the exact inverse of how CPython decodes filesystem paths, so every path Python can represent — including undecodable bytes, which appear as lone surrogates — round-trips. It clears any exception on failure and returnsnullptr; all three call sites now NULL-check. Also removes a dead encode-and-discard intrace_func.src/include/traceconfig.hpp— the package-path encode used"strict"and leaked a pending exception on failure; nowsurrogateescape+PyErr_Clear().scalene/scalene_mapfile.py— newdecode_sample()using the matching UTF-8/surrogateescapepair, replacing the ASCII decodes.Test
tests/test_issue1086_non_ascii_paths.pyis picked up by the existingpython3 -m pyteststep intests.yml, so it runs on Ubuntu + macOS across Python 3.9–3.14. Three cases:decode_sampledirectly — deterministic, including lone surrogates.scalene run --memoryon a program importing a non-ASCII module, with the non-ASCII component in the filename and in a parent directory. Asserts no Unicode error, the program ran to completion, exit code 0, and the path comes back through the profile intact (NFC-normalized comparison so an NFD-normalizing filesystem doesn't false-fail).The crash assertions are hard on every attempt; only the "file appears in the profile" check retries against the usual sampling flake.
Verification
'ascii' codec can't encode character '\xfc', same traceback shape as the report.überschüsse.py: 20 MB malloc, 89% CPU);--memory --stacksalso carries the non-ASCII path correctly through the native stack buffer.tests/suite: 445 passed, 13 skipped.ruffclean;mypyshows only the pre-existingscalene_signal_manager.pyerror also present on master.🤖 Generated with Claude Code