@@ -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