Skip to content

fix: best-effort avoid logging non-deadline directories in telemetry stack traces#1295

Open
crowecawcaw wants to merge 7 commits into
aws-deadline:mainlinefrom
crowecawcaw:review-fix/sec-stack-trace-sanitizer
Open

fix: best-effort avoid logging non-deadline directories in telemetry stack traces#1295
crowecawcaw wants to merge 7 commits into
aws-deadline:mainlinefrom
crowecawcaw:review-fix/sec-stack-trace-sanitizer

Conversation

@crowecawcaw

@crowecawcaw crowecawcaw commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Fixes:

What was the problem/requirement? (What/Why)

As a best-effort tidiness measure, the telemetry stack-trace sanitizer trims local file-path content so traces are shorter and less noisy, keeping only the portion from a known package (deadline/openjd/boto3/botocore) onward. It did this by matching on segment name, so any path segment literally named after a package was treated as the package root. A directory that happens to be named deadline (e.g. /home/user/deadline/my_project/file.py) was therefore trimmed at the wrong place, so my_project and the filename stayed in the trace instead of being shortened away — noisier output than intended, and not the behavior we want.

What was the solution? (How)

A segment is treated as a package root only when the reconstructed absolute prefix equals the package's genuine on-disk install location (resolved via importlib.util.find_spec), rather than any path merely containing the package name. The site-packages fallback still covers ordinary venv installs. A package that isn't importable in the running interpreter simply contributes no anchor, so its frames fall through to more trimming — the conservative direction (trim more, keep less).

What is the impact of this change?

Genuine framework frames stay package-relative in telemetry; directories that happen to share a package name get trimmed to the bare filename like any other non-package path.

How was this change tested?

Added tests: genuine framework frames (site-packages/deadline, third-party, Windows path) stay package-relative; a deadline-named directory that isn't the install location (Linux, Windows, rendered traceback, live exception) is trimmed to the bare filename.

  • Have you run the unit tests? Yestest_stack_trace_sanitizer.py (11 passed: 7 new + 4 existing).
  • Have you run the integration tests? No.

Was this change documented?

  • No public-contract change (sanitize_exception signature unchanged).
  • README.md not affected.

Does this PR introduce new dependencies?

  • This PR adds one or more new dependency Python packages.
  • This PR does not add any new dependencies. (importlib.util is stdlib.)

Is this a breaking change?

No.

Does this change impact security?

No. This is a best-effort effort to keep telemetry stack traces tidy by trimming non-deadline directory names; it is not a security control and should not be treated as one. It is not relied on as a boundary, makes no guarantee about what path content a trace may contain, and any shortcoming here is an ordinary bug, not a security issue.

Testing

Beyond the unit tests, the sanitizer was manually checked without live AWS by running fabricated stack traces through _sanitize_path / _sanitize_traceback / sanitize_exception in a scratch harness, on both POSIX-style and Windows-style path sets:

  • Non-package paths trimmed to bare filename: job-bundle trees under a fake home containing a deadline directory (/home/artist/deadline/some-project/job_bundle/render_script.py, C:\Users\bob\deadline\SomeProject\render.py), plain scripts, UNC shares (\\studio-nas\projects\...), deadline.egg-info-spelled dirs, and directories sharing a known package name (/opt/openjd/vendored/...).
  • Genuine framework frames preserved package-relative: the actually-installed deadline frame (hatch env src layout), real botocore site-packages frame, Debian dist-packages frames, Windows Lib\site-packages frames, and Windows user site-packages (...\Python311\site-packages\...).
  • PEP 420 namespace packages: verified frames under multiple submodule_search_locations roots for the deadline namespace all anchor correctly while a deadline-named non-install dir does not.
  • Memoization: confirmed find_spec runs at most once per known package per process across repeated sanitize calls (lru_cache).
  • End-to-end: a fabricated multi-frame TracebackException mixing all of the above rendered with the non-package path segments trimmed and no exception-message text.

The manual pass found one case the earlier logic didn't trim as intended, fixed in this PR: a directory literally named site-packages or dist-packages (e.g. /home/artist/site-packages/SomeProject/tool.py) anchored the generic fallback and kept everything below it. The fallback now requires the corroborating interpreter-layout parent (lib/Lib/lib64/pythonX.Y) before trusting the segment; regression tests added for POSIX and Windows shapes.

  • Windows system-wide (non-venv) layouts — automated: parametrized unit tests cover the python.org all-users install under C:\Program Files\Python311\Lib\site-packages\... (path with a space), Microsoft Store Python under WindowsApps, per-user site-packages (...\AppData\Roaming\Python\Python311\site-packages\...), and a deadline-named directory sitting above a genuine site-packages in the same path — genuine package frames render package-relative and directories above site-packages are trimmed. Since the sanitizer is pure path-string logic, these shapes are fully exercised in unit tests; no hands-on Windows testing is needed.

hatch run fmt, hatch run lint (ruff + mypy) clean; full unit suite passes (2879 passed, 21 skipped).

… dirs

The stack-trace sanitizer classified any path segment named after one of our
framework packages (deadline, openjd, boto3, botocore) as safe-to-keep and
emitted everything from that segment onward. A customer directory that merely
shares a name with one of our packages (e.g. a project tree rooted at
~/deadline/secret_project/...) was therefore misclassified as framework, and
its customer-specific path segments leaked into telemetry, violating the
no-customer-content tenet.

Tighten the match: a segment is only treated as framework when the
reconstructed absolute prefix actually equals where that package is installed
on this machine (resolved via importlib.util.find_spec). Genuine framework
frames are still kept package-relative; the existing site-packages branch
continues to cover ordinary venv installs. A customer "deadline" directory now
redacts down to the bare filename.

Adds example-based tests covering both the framework-kept and
customer-redacted cases (Linux and Windows path shapes) alongside the existing
property-based fuzz tests.

Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com>
@github-actions github-actions Bot added the waiting-on-maintainers Waiting on the maintainers to review. label Jul 21, 2026
@crowecawcaw crowecawcaw added the security Pull requests that could impact security label Jul 21, 2026
# site-packages are also covered by the generic site-packages branch
# below; this branch additionally handles embedded / custom-sys.path
# layouts where the package isn't under a site-packages directory.)
install_dirs = _known_package_install_dirs()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_known_package_install_dirs() is called once per _sanitize_path(), i.e. once per stack frame. Each call loops over every known package and runs importlib.util.find_spec(), which for not-yet-imported packages hits the import machinery/filesystem. The install locations are fixed for the life of the process, so a deep traceback pays this cost N times over for no benefit.

Consider memoizing — e.g. @functools.lru_cache(maxsize=None) on _known_package_install_dirs, or computing it once in _sanitize_traceback and threading it in. lru_cache also naturally handles the fact that the result never changes within a process.

@crowecawcaw crowecawcaw Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_known_package_install_dirs is memoized with @functools.lru_cache(maxsize=None), so the find_spec scan runs once per process rather than once per frame.

locations = list(spec.submodule_search_locations or [])
pkg_dir = None
if locations:
pkg_dir = locations[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deadline and openjd are PEP 420 namespace packages (there is no src/deadline/__init__.py, and pyproject.toml sets namespace_packages = true). For a namespace package, submodule_search_locations can contain more than one path — e.g. when the namespace is contributed by several distributions installed into different sys.path roots (system site + user site, or an editable src tree alongside a wheel). This code records only locations[0] as the single anchor.

Consequence: a genuine framework frame that lives under one of the other namespace locations will not equal the anchor and will fall through — collapsing to a bare filename (or, if it happens to sit under a site-packages segment, salvaged by the later branch). This never leaks customer content (the failure is in the safe direction), but it does silently degrade the usefulness of the telemetry for legitimate framework frames.

Consider anchoring against all entries in submodule_search_locations, e.g. store Dict[str, List[str]] (or a set of normalized dirs) and match if the reconstructed prefix equals any of them.

@crowecawcaw crowecawcaw Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The anchor map is Dict[str, List[str]] and records every entry in submodule_search_locations, so a genuine framework frame under any namespace-package root is recognized; _sanitize_path matches the reconstructed prefix against all recorded anchors.

@crowecawcaw crowecawcaw removed the security Pull requests that could impact security label Jul 21, 2026
Memoize _known_package_install_dirs with lru_cache (install locations are
fixed per process) and record all submodule_search_locations for PEP 420
namespace packages so genuine framework frames under any namespace root are
recognized.

Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com>
@crowecawcaw crowecawcaw changed the title fix(security): stop stack-trace sanitizer leaking customer deadline dirs fix: stop stack-trace sanitizer leaking customer deadline dirs Jul 22, 2026
@crowecawcaw
crowecawcaw marked this pull request as ready for review July 22, 2026 19:56
@crowecawcaw
crowecawcaw requested a review from a team as a code owner July 22, 2026 19:56
…kages

The telemetry test_sanitize_path cases assumed a bare package-name segment
was enough to anchor the trim, but the sanitizer now only keeps a
package-relative path when the on-disk prefix is the package's genuine
install directory. Patch _known_package_install_dirs in the test so the
fixture paths count as genuine install locations, making the cases
machine-independent.

Also treat Debian/Ubuntu dist-packages like site-packages in the generic
fallback so system-Python installs keep the library-relative subpath.

Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com>
…s anchor

Manual verification of the sanitizer found a remaining leak: a customer
directory literally named "site-packages" (or "dist-packages") anywhere
in a path — e.g. /home/artist/site-packages/ClientSecretShow/tool.py —
anchored the generic fallback and leaked every segment below it into
telemetry.

Real interpreter layouts always place site-packages/dist-packages under a
lib/Lib/lib64 or pythonX.Y parent (POSIX venv: lib/python3.11/site-packages,
Windows venv: Lib\site-packages, Debian: lib/python3/dist-packages, Windows
user site: Python311\site-packages), so require that corroborating parent
before trusting the segment. Paths that fail the check fall through to the
bare-filename branch — dropping detail is safe, leaking is not.

Adds regression tests for the customer-named site-packages/dist-packages
cases on POSIX and Windows path shapes, plus coverage for Debian
dist-packages and Windows user site-packages layouts being kept relative.

Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com>
Parametrized cases for Windows layouts outside a venv: python.org
all-users install under Program Files (path with a space), Microsoft
Store Python under WindowsApps, and a customer-named deadline directory
sitting above a genuine site-packages in the same path. Genuine package
frames must render package-relative while no path segment above
site-packages leaks. Replaces the manual spot-check previously flagged
for a real Windows box — the sanitizer is pure path-string logic, so
these shapes are fully exercisable in unit tests.

Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com>
@crowecawcaw crowecawcaw changed the title fix: stop stack-trace sanitizer leaking customer deadline dirs fix: best-effort avoid logging non-deadline directories in telemetry stack traces Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on-maintainers Waiting on the maintainers to review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant