From 677e3e76e0f6f18e5e311fc5f51f09ee600afcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20=F0=9F=91=A8=F0=9F=8F=BD=E2=80=8D=F0=9F=92=BB=20Copl?= =?UTF-8?q?an?= Date: Wed, 10 Jun 2026 21:46:00 -0700 Subject: [PATCH] fix: support Python 3.13 PyConfig layout python3-sys 0.7.2 is missing fields added to CPython 3.13's PyConfig, so PyConfig_InitPythonConfig can write past the Rust struct and corrupt embedded Python startup. Allocate guarded PyConfig storage and avoid directly touching fields whose offsets are shifted on Python 3.13. Fixes #1334 Co-authored-by: Codex --- eden/scm/lib/commands/cmdpy/src/python.rs | 68 ++++++++++++++++------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/eden/scm/lib/commands/cmdpy/src/python.rs b/eden/scm/lib/commands/cmdpy/src/python.rs index 1217821476a05..0b95f70713283 100644 --- a/eden/scm/lib/commands/cmdpy/src/python.rs +++ b/eden/scm/lib/commands/cmdpy/src/python.rs @@ -134,18 +134,40 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) { check_status!(ffi::Py_PreInitialize(&pre_config), None); - let mut config = ffi::PyConfig::default(); + // python3-sys 0.7.2 is missing fields added in Python 3.13 (cpu_count, + // sys_path_0), making the Rust PyConfig struct ~12 bytes too small. + // PyConfig_InitPythonConfig writes the full C struct, so we need extra + // space to prevent a buffer overflow. The overflow_guard absorbs this. + // + // The missing cpu_count field also shifts the C offsets of all later + // fields (executable, prefix, module_search_paths, etc.) relative to + // the Rust struct. We avoid accessing any shifted field directly: + // - executable: computed automatically by PyConfig_Read + // - module_search_paths: only set for Python 3.10 + // - prefix: only set for static_libpython builds + // Fields we DO set (install_signal_handlers, site_import, parse_argv, + // user_site_directory, argv) all precede cpu_count and are unaffected. + #[repr(C, align(8))] + struct PyConfigStorage { + inner: ffi::PyConfig, + _overflow_guard: [u64; 8], + } + let mut storage = PyConfigStorage { + inner: ffi::PyConfig::default(), + _overflow_guard: [0u64; 8], + }; + let config = (&mut storage as *mut PyConfigStorage).cast::(); // Ideally we could use PyConfig_InitIsolatedConfig, but we rely on some // of the vanilla initialization logic to find the std lib, at least. - ffi::PyConfig_InitPythonConfig(&mut config); + ffi::PyConfig_InitPythonConfig(config); - config.install_signal_handlers = 0; - config.site_import = 0; - config.parse_argv = 0; + (*config).install_signal_handlers = 0; + (*config).site_import = 0; + (*config).parse_argv = 0; // This allows IPython to be installed in user site dir. - config.user_site_directory = 1; + (*config).user_site_directory = 1; // This assumes Python has been pre-initialized, and filesystem encoding // is utf-8 (both done above). @@ -156,15 +178,15 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) { } } - check_status!( - ffi::PyConfig_SetString(&mut config, &mut config.executable, to_wide(&args[0])), - Some(config) - ); - + // Note: we intentionally skip setting config.executable here. On + // Python < 3.13 it was set to args[0] (current_exe), but PyConfig_Read + // computes it automatically. On Python >= 3.13, the field offset is + // wrong in python3-sys 0.7.2 due to the missing cpu_count field. + // argv is before cpu_count in the struct layout, so it's safe. for arg in args.iter() { check_status!( - ffi::PyWideStringList_Append(&mut config.argv, to_wide(arg)), - Some(config) + ffi::PyWideStringList_Append(&mut (*config).argv, to_wide(arg)), + None ); } @@ -178,13 +200,19 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) { let exe_dir = exe_dir.to_str().expect("utf-8 exe dir"); // "prefix" affects many other paths. // See https://docs.python.org/3/c-api/init_config.html#init-path-config + // + // WARNING: On Python 3.13+, config.prefix has the wrong Rust offset + // due to the missing cpu_count field in python3-sys 0.7.2. This code + // path (static_libpython) is not used on standard OSS builds, so this + // is acceptable for now. A proper fix requires vendoring python3-sys + // or migrating to PyO3. check_status!( ffi::PyConfig_SetString(&mut config, &mut config.prefix, to_wide(exe_dir)), - Some(config) + None ); } - check_status!(ffi::PyConfig_Read(&mut config), Some(config)); + check_status!(ffi::PyConfig_Read(config), None); // "3.10.9 (v3.10.9:1dd9be6584, Dec 6 2022, 14:37:36) [Clang 13.0.0 (clang-1300.0.29.30)]" let version = CStr::from_ptr(ffi::Py_GetVersion()); @@ -203,10 +231,10 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) { if minor_version == Some(10) { if let Some(home) = sapling_home { // This tells Py_Main to not overwrite sys.path and to copy our below value. - config.module_search_paths_set = 1; + (*config).module_search_paths_set = 1; check_status!( - ffi::PyWideStringList_Append(&mut config.module_search_paths, to_wide(home)), - Some(config) + ffi::PyWideStringList_Append(&mut (*config).module_search_paths, to_wide(home)), + None ); } } @@ -217,9 +245,9 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) { ffi::PyImport_FrozenModules = EXTRA_FROZEN_MODULES.0.as_ptr() as _; } - check_status!(ffi::Py_InitializeFromConfig(&config), Some(config)); + check_status!(ffi::Py_InitializeFromConfig(config), None); - ffi::PyConfig_Clear(&mut config); + ffi::PyConfig_Clear(config); } }