From b6170c11f0599eb85e9a91770d514d84a8bd0e44 Mon Sep 17 00:00:00 2001 From: Andreas Karatzas Date: Sat, 21 Mar 2026 23:55:37 +0000 Subject: [PATCH 1/2] Fix sf_error(NULL) race condition under concurrent open failures Signed-off-by: Andreas Karatzas --- soundfile.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/soundfile.py b/soundfile.py index f17b3cc..65769bf 100644 --- a/soundfile.py +++ b/soundfile.py @@ -12,6 +12,7 @@ import os as _os import sys as _sys +import threading as _threading import numpy.typing from os import SEEK_SET, SEEK_CUR, SEEK_END from ctypes.util import find_library as _find_library @@ -1269,6 +1270,13 @@ def close(self) -> None: self._file = None _error_check(err) + # sf_error(NULL) returns a global (non-thread-safe) error code. + # When an sf_open* call fails (returns NULL), we must call + # sf_error(NULL) to retrieve the error code, but another thread + # may clear the global error between our open and our sf_error. + # This lock serialises the open+sf_error(NULL) pair to prevent that. + _open_lock = _threading.Lock() + def _open(self, file, mode_int, closefd): """Call the appropriate sf_open*() function from libsndfile.""" if isinstance(file, (_unicode, bytes)): @@ -1284,18 +1292,26 @@ def _open(self, file, mode_int, closefd): openfunction = _snd.sf_wchar_open else: file = file.encode(_sys.getfilesystemencoding()) - file_ptr = openfunction(file, mode_int, self._info) + with self._open_lock: + file_ptr = openfunction(file, mode_int, self._info) + if file_ptr == _ffi.NULL: + err = _snd.sf_error(file_ptr) + raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) elif isinstance(file, int): - file_ptr = _snd.sf_open_fd(file, mode_int, self._info, closefd) + with self._open_lock: + file_ptr = _snd.sf_open_fd(file, mode_int, self._info, closefd) + if file_ptr == _ffi.NULL: + err = _snd.sf_error(file_ptr) + raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) elif _has_virtual_io_attrs(file, mode_int): - file_ptr = _snd.sf_open_virtual(self._init_virtual_io(file), - mode_int, self._info, _ffi.NULL) + with self._open_lock: + file_ptr = _snd.sf_open_virtual(self._init_virtual_io(file), + mode_int, self._info, _ffi.NULL) + if file_ptr == _ffi.NULL: + err = _snd.sf_error(file_ptr) + raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) else: raise TypeError("Invalid file: {0!r}".format(self.name)) - if file_ptr == _ffi.NULL: - # get the actual error code - err = _snd.sf_error(file_ptr) - raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) if mode_int == _snd.SFM_WRITE: # Due to a bug in libsndfile version <= 1.0.25, frames != 0 # when opening a named pipe in SFM_WRITE mode. From 002126dd6e2403fdbdc0e2b1d24aa751e1976f79 Mon Sep 17 00:00:00 2001 From: Andreas Karatzas Date: Sun, 19 Apr 2026 23:58:57 +0000 Subject: [PATCH 2/2] Factor out openfunction to dedupe lock+error handling --- soundfile.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/soundfile.py b/soundfile.py index 65769bf..3d75b4e 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1275,7 +1275,7 @@ def close(self) -> None: # sf_error(NULL) to retrieve the error code, but another thread # may clear the global error between our open and our sf_error. # This lock serialises the open+sf_error(NULL) pair to prevent that. - _open_lock = _threading.Lock() + _sf_error_lock = _threading.Lock() def _open(self, file, mode_int, closefd): """Call the appropriate sf_open*() function from libsndfile.""" @@ -1292,26 +1292,19 @@ def _open(self, file, mode_int, closefd): openfunction = _snd.sf_wchar_open else: file = file.encode(_sys.getfilesystemencoding()) - with self._open_lock: - file_ptr = openfunction(file, mode_int, self._info) - if file_ptr == _ffi.NULL: - err = _snd.sf_error(file_ptr) - raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) elif isinstance(file, int): - with self._open_lock: - file_ptr = _snd.sf_open_fd(file, mode_int, self._info, closefd) - if file_ptr == _ffi.NULL: - err = _snd.sf_error(file_ptr) - raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) + openfunction = lambda file, mode_int, info: _snd.sf_open_fd(file, mode_int, info, closefd) elif _has_virtual_io_attrs(file, mode_int): - with self._open_lock: - file_ptr = _snd.sf_open_virtual(self._init_virtual_io(file), - mode_int, self._info, _ffi.NULL) - if file_ptr == _ffi.NULL: - err = _snd.sf_error(file_ptr) - raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) + openfunction = lambda file, mode_int, info: _snd.sf_open_virtual(self._init_virtual_io(file), + mode_int, info, _ffi.NULL) else: raise TypeError("Invalid file: {0!r}".format(self.name)) + + with self._sf_error_lock: + file_ptr = openfunction(file, mode_int, self._info) + if file_ptr == _ffi.NULL: + err = _snd.sf_error(file_ptr) + raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) if mode_int == _snd.SFM_WRITE: # Due to a bug in libsndfile version <= 1.0.25, frames != 0 # when opening a named pipe in SFM_WRITE mode.