Severity: Enhancement (security-sensitive callers cannot prohibit an implicit change from a kernel lock to a file-existence protocol)
Summary
I would like an opt-in native-only policy for FileLock. On Unix, an ENOSYS
from flock() currently warns, unlinks the native lock path, changes the live
object's class to SoftFileLock, and continues acquisition under a different
protocol.
Automatic fallback is useful and should remain the backward-compatible
default. Some callers, however, require a kernel-enforced lock and need
acquisition to fail if that guarantee is unavailable.
Environment
- package/version:
filelock 3.29.7
- source: commit
1efb8932c08789deb08ad93a91d8996cc36bb9cc
- platforms: Unix-like systems and filesystems where
flock() returns ENOSYS
- affected APIs:
FileLock, AsyncFileLock
Reproduction
-
Run the attached filelock_enosys_implicit_protocol_change.py with
filelock==3.29.7.
-
Observe:
type_before=UnixFileLock
type_after=SoftFileLock
filelock_version=3.29.7
class_changed=True
warning_count=1
warning=flock not supported on this filesystem, falling back to SoftFileLock
locked=True
Observed vs expected
Observed:
FileLock always accepts the weaker fallback after ENOSYS. A caller can observe
only a warning and cannot request fail-closed behavior.
Expected:
An opt-in policy makes FileLock raise when the requested native backend is
unavailable, without changing the existing automatic default.
Root cause (verified in source, 3.29.7)
UnixFileLock._acquire()
handles ENOSYS by unlinking the path, calling _fallback_to_soft_lock(), and
acquiring again.
_fallback_to_soft_lock()
changes self.__class__ after emitting a warning. This automatic behavior was
introduced intentionally in PR #480 to resolve #289 and #349, and it addresses
the same runtime ENOSYS failure mode reported in #67. The missing contract is
an opt-out for callers that cannot accept the fallback.
Suggested fix
Add a keyword-only backend policy with a backward-compatible default, for
example backend_policy="auto" with values such as "auto", "native-only",
and "soft".
For "native-only", propagate a specific unsupported-backend error on
ENOSYS before unlinking the path or changing the object class. Include the
policy in singleton compatibility checks and thread it through async wrappers.
Verification
- Existing calls retain automatic fallback and the current warning.
- Native-only calls raise a documented exception on
ENOSYS.
- Native-only failure does not change
type(lock) or acquire a soft lock.
- Singleton construction rejects incompatible backend policies.
- Sync and async APIs behave consistently.
Current workaround
Instantiate UnixFileLock directly and preflight the filesystem, or convert
the fallback warning into an exception with a process-wide warning filter.
Neither workaround is a direct per-lock contract.
Reproduction script
filelock_enosys_implicit_protocol_change.py
"""Show that UnixFileLock changes lock protocol after ENOSYS with no opt-out."""
from __future__ import annotations
import errno
import sys
import tempfile
import warnings
from pathlib import Path
from unittest.mock import patch
import filelock
from filelock import FileLock
from filelock import _unix as unix_backend
def main() -> int:
if sys.platform == "win32":
print("This reproduction targets UnixFileLock.")
return 2
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "resource.lock"
lock = FileLock(path, timeout=0)
before = type(lock).__name__
with (
patch.object(
unix_backend.fcntl,
"flock",
side_effect=OSError(errno.ENOSYS, "injected unsupported flock"),
),
warnings.catch_warnings(record=True) as caught,
):
warnings.simplefilter("always")
lock.acquire()
try:
after = type(lock).__name__
messages = [str(item.message) for item in caught]
warning = messages[0] if messages else None
class_changed = before != after
print(f"type_before={before}")
print(f"type_after={after}")
print(f"filelock_version={filelock.__version__}")
print(f"class_changed={class_changed}")
print(f"warning_count={len(messages)}")
print(f"warning={warning}")
print(f"locked={lock.is_locked}")
reproduced = (
before == "UnixFileLock"
and after == "SoftFileLock"
and class_changed
and len(messages) == 1
and "falling back to SoftFileLock" in str(warning)
and lock.is_locked
)
finally:
if lock.is_locked:
lock.release()
return 0 if reproduced else 1
if __name__ == "__main__":
raise SystemExit(main())
Severity: Enhancement (security-sensitive callers cannot prohibit an implicit change from a kernel lock to a file-existence protocol)
Summary
I would like an opt-in native-only policy for
FileLock. On Unix, anENOSYSfrom
flock()currently warns, unlinks the native lock path, changes the liveobject's class to
SoftFileLock, and continues acquisition under a differentprotocol.
Automatic fallback is useful and should remain the backward-compatible
default. Some callers, however, require a kernel-enforced lock and need
acquisition to fail if that guarantee is unavailable.
Environment
filelock 3.29.71efb8932c08789deb08ad93a91d8996cc36bb9ccflock()returnsENOSYSFileLock,AsyncFileLockReproduction
Run the attached
filelock_enosys_implicit_protocol_change.pywithfilelock==3.29.7.Observe:
Observed vs expected
Observed:
Expected:
Root cause (verified in source, 3.29.7)
UnixFileLock._acquire()handles
ENOSYSby unlinking the path, calling_fallback_to_soft_lock(), andacquiring again.
_fallback_to_soft_lock()changes
self.__class__after emitting a warning. This automatic behavior wasintroduced intentionally in PR #480 to resolve #289 and #349, and it addresses
the same runtime
ENOSYSfailure mode reported in #67. The missing contract isan opt-out for callers that cannot accept the fallback.
Suggested fix
Add a keyword-only backend policy with a backward-compatible default, for
example
backend_policy="auto"with values such as"auto","native-only",and
"soft".For
"native-only", propagate a specific unsupported-backend error onENOSYSbefore unlinking the path or changing the object class. Include thepolicy in singleton compatibility checks and thread it through async wrappers.
Verification
ENOSYS.type(lock)or acquire a soft lock.Current workaround
Instantiate
UnixFileLockdirectly and preflight the filesystem, or convertthe fallback warning into an exception with a process-wide warning filter.
Neither workaround is a direct per-lock contract.
Reproduction script
filelock_enosys_implicit_protocol_change.py