Skip to content

Python 3.13 OSS build corrupts PyConfig and breaks embedded Python startup #1334

Description

@vegerot

Summary

OSS builds of Sapling against Python 3.13 can produce an sl binary that fails during embedded Python startup.

The underlying issue appears to be an ABI/layout mismatch: python3-sys 0.7.2 defines PyConfig without fields added by CPython 3.13 (cpu_count and sys_path_0). When Sapling calls PyConfig_InitPythonConfig, CPython writes the full 3.13 C struct into the smaller Rust allocation. That can corrupt adjacent stack memory and/or make later direct field accesses use shifted offsets.

Environment

  • OS: Linux
  • Python: /usr/bin/python3, observed with Python 3.13.5
  • Build mode: OSS
  • Build command: make -j oss from eden/scm

Reproduction

From a fresh working copy on a system where /usr/bin/python3 is Python 3.13:

cd eden/scm
make -j oss
./sl

Python-backed commands can also reproduce the startup failure directly:

cd eden/scm
env CHGDISABLE=1 ./sl debugpython -- -c 'import sys; print(hasattr(sys, "path"))'

Observed Behavior

With a release build, the binary can panic after Python initialization because sys.path is missing:

thread 'main' panicked at lib/commands/cmdpy/src/hgpython.rs:143:69:
called `Result::unwrap()` on an `Err` value: PyErr { ptype: <class 'AttributeError'>, pvalue: Some(AttributeError("module 'sys' has no attribute 'path'")), ptraceback: None }

With a debug build, the failure may happen earlier inside Python initialization:

SystemError: null argument to internal routine
Fatal Python error: _PyImport_InitExternal: external importer setup failed
Python runtime state: core initialized

Current thread ... (most recent call first):
  <no Python frame>

Expected Behavior

The OSS-built ./sl should initialize embedded Python normally on Python 3.13, including valid sys.path and sys.meta_path, and then run commands.

Suspected Cause

lib/commands/cmdpy/src/python.rs allocates ffi::PyConfig using the python3-sys 0.7.2 layout. That crate's final release predates CPython 3.13's added fields, so the Rust struct is smaller than CPython expects. The missing cpu_count field also shifts offsets for fields that come after it, such as executable, prefix, and module_search_paths.

Mitigation Tested Locally

A local workaround that fixed startup was to:

  • allocate padded storage around ffi::PyConfig before calling PyConfig_InitPythonConfig;
  • avoid directly setting/accessing fields whose offsets are shifted on Python 3.13;
  • let PyConfig_Read compute executable automatically for the normal OSS dynamic-libpython path.

After that, this verification succeeded:

env CHGDISABLE=1 ./sl debugpython -- -c 'import sys; print(sys.version); print(hasattr(sys, "path")); print(hasattr(sys, "meta_path"))'

Output included:

3.13.5 (...)
True
True

and make -j oss && ./sl completed successfully.

Disclosure

This issue text was written by Codex, an AI coding agent based on GPT-5.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions