Skip to content

Commit 677e3e7

Browse files
vegerotcodex
andcommitted
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 <codex@openai.com>
1 parent de1e87e commit 677e3e7

1 file changed

Lines changed: 48 additions & 20 deletions

File tree

‎eden/scm/lib/commands/cmdpy/src/python.rs‎

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,40 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) {
134134

135135
check_status!(ffi::Py_PreInitialize(&pre_config), None);
136136

137-
let mut config = ffi::PyConfig::default();
137+
// python3-sys 0.7.2 is missing fields added in Python 3.13 (cpu_count,
138+
// sys_path_0), making the Rust PyConfig struct ~12 bytes too small.
139+
// PyConfig_InitPythonConfig writes the full C struct, so we need extra
140+
// space to prevent a buffer overflow. The overflow_guard absorbs this.
141+
//
142+
// The missing cpu_count field also shifts the C offsets of all later
143+
// fields (executable, prefix, module_search_paths, etc.) relative to
144+
// the Rust struct. We avoid accessing any shifted field directly:
145+
// - executable: computed automatically by PyConfig_Read
146+
// - module_search_paths: only set for Python 3.10
147+
// - prefix: only set for static_libpython builds
148+
// Fields we DO set (install_signal_handlers, site_import, parse_argv,
149+
// user_site_directory, argv) all precede cpu_count and are unaffected.
150+
#[repr(C, align(8))]
151+
struct PyConfigStorage {
152+
inner: ffi::PyConfig,
153+
_overflow_guard: [u64; 8],
154+
}
155+
let mut storage = PyConfigStorage {
156+
inner: ffi::PyConfig::default(),
157+
_overflow_guard: [0u64; 8],
158+
};
159+
let config = (&mut storage as *mut PyConfigStorage).cast::<ffi::PyConfig>();
138160

139161
// Ideally we could use PyConfig_InitIsolatedConfig, but we rely on some
140162
// of the vanilla initialization logic to find the std lib, at least.
141-
ffi::PyConfig_InitPythonConfig(&mut config);
163+
ffi::PyConfig_InitPythonConfig(config);
142164

143-
config.install_signal_handlers = 0;
144-
config.site_import = 0;
145-
config.parse_argv = 0;
165+
(*config).install_signal_handlers = 0;
166+
(*config).site_import = 0;
167+
(*config).parse_argv = 0;
146168

147169
// This allows IPython to be installed in user site dir.
148-
config.user_site_directory = 1;
170+
(*config).user_site_directory = 1;
149171

150172
// This assumes Python has been pre-initialized, and filesystem encoding
151173
// is utf-8 (both done above).
@@ -156,15 +178,15 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) {
156178
}
157179
}
158180

159-
check_status!(
160-
ffi::PyConfig_SetString(&mut config, &mut config.executable, to_wide(&args[0])),
161-
Some(config)
162-
);
163-
181+
// Note: we intentionally skip setting config.executable here. On
182+
// Python < 3.13 it was set to args[0] (current_exe), but PyConfig_Read
183+
// computes it automatically. On Python >= 3.13, the field offset is
184+
// wrong in python3-sys 0.7.2 due to the missing cpu_count field.
185+
// argv is before cpu_count in the struct layout, so it's safe.
164186
for arg in args.iter() {
165187
check_status!(
166-
ffi::PyWideStringList_Append(&mut config.argv, to_wide(arg)),
167-
Some(config)
188+
ffi::PyWideStringList_Append(&mut (*config).argv, to_wide(arg)),
189+
None
168190
);
169191
}
170192

@@ -178,13 +200,19 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) {
178200
let exe_dir = exe_dir.to_str().expect("utf-8 exe dir");
179201
// "prefix" affects many other paths.
180202
// See https://docs.python.org/3/c-api/init_config.html#init-path-config
203+
//
204+
// WARNING: On Python 3.13+, config.prefix has the wrong Rust offset
205+
// due to the missing cpu_count field in python3-sys 0.7.2. This code
206+
// path (static_libpython) is not used on standard OSS builds, so this
207+
// is acceptable for now. A proper fix requires vendoring python3-sys
208+
// or migrating to PyO3.
181209
check_status!(
182210
ffi::PyConfig_SetString(&mut config, &mut config.prefix, to_wide(exe_dir)),
183-
Some(config)
211+
None
184212
);
185213
}
186214

187-
check_status!(ffi::PyConfig_Read(&mut config), Some(config));
215+
check_status!(ffi::PyConfig_Read(config), None);
188216

189217
// "3.10.9 (v3.10.9:1dd9be6584, Dec 6 2022, 14:37:36) [Clang 13.0.0 (clang-1300.0.29.30)]"
190218
let version = CStr::from_ptr(ffi::Py_GetVersion());
@@ -203,10 +231,10 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) {
203231
if minor_version == Some(10) {
204232
if let Some(home) = sapling_home {
205233
// This tells Py_Main to not overwrite sys.path and to copy our below value.
206-
config.module_search_paths_set = 1;
234+
(*config).module_search_paths_set = 1;
207235
check_status!(
208-
ffi::PyWideStringList_Append(&mut config.module_search_paths, to_wide(home)),
209-
Some(config)
236+
ffi::PyWideStringList_Append(&mut (*config).module_search_paths, to_wide(home)),
237+
None
210238
);
211239
}
212240
}
@@ -217,9 +245,9 @@ pub fn py_initialize(args: &[String], sapling_home: Option<&String>) {
217245
ffi::PyImport_FrozenModules = EXTRA_FROZEN_MODULES.0.as_ptr() as _;
218246
}
219247

220-
check_status!(ffi::Py_InitializeFromConfig(&config), Some(config));
248+
check_status!(ffi::Py_InitializeFromConfig(config), None);
221249

222-
ffi::PyConfig_Clear(&mut config);
250+
ffi::PyConfig_Clear(config);
223251
}
224252
}
225253

0 commit comments

Comments
 (0)