Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/filelock/_soft_rw/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,11 +781,16 @@ def _read_marker(name: str, *, dir_fd: int | None = None) -> tuple[_MarkerInfo |
if fd is None:
return None
try:
try:
st = os.fstat(fd)
data = os.read(fd, _MAX_MARKER_SIZE + 1)
except OSError: # pragma: no cover - e.g. EAGAIN from a hostile FIFO that has a writer attached
return None
st = os.fstat(fd)
# A legitimate marker is always a regular file, so anything else at the path -- above all a FIFO -- is
# reported as a malformed marker (its mtime still drives stale eviction) without being read. Reading is
# where platforms diverge: an empty non-blocking read yields 0 bytes on Linux/macOS but EAGAIN on
# FreeBSD, and the EAGAIN used to abort the stale-break and wedge the acquire until timeout (#587).
if not stat.S_ISREG(st.st_mode):
return None, st.st_mtime
data = os.read(fd, _MAX_MARKER_SIZE + 1)
except OSError: # pragma: no cover - marker vanished or turned unreadable between open and read
return None
finally:
os.close(fd)
return _parse_marker_bytes(data), st.st_mtime
Expand Down
24 changes: 23 additions & 1 deletion tests/soft_rw/test_soft_rw_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ def test_stale_malformed_marker_is_evicted(lock_file: str, content: bytes) -> No

def test_fifo_write_marker_does_not_block(lock_file: str) -> None:
if sys.platform == "win32":
pytest.skip("os.mkfifo is unix-only")
pytest.skip("os.mkfifo is unix-only") # also narrows sys.platform so ty resolves os.mkfifo below
marker = f"{lock_file}.write"
os.mkfifo(marker)
past = time.time() - 1000
Expand All @@ -772,6 +772,28 @@ def test_fifo_write_marker_does_not_block(lock_file: str) -> None:
lock.close()


def test_fifo_write_marker_with_writer_is_evicted(lock_file: str) -> None:
if sys.platform == "win32":
pytest.skip("os.mkfifo is unix-only") # also narrows sys.platform so ty resolves os.mkfifo below
marker = f"{lock_file}.write"
os.mkfifo(marker)
past = time.time() - 1000
os.utime(marker, (past, past))
# A writer attached to the FIFO makes the non-blocking read raise EAGAIN on every platform, matching how a
# writerless FIFO already behaves on FreeBSD (#587). The stale marker must still be evicted by mtime, not
# read, so the acquire completes instead of timing out.
reader_fd = os.open(marker, os.O_RDONLY | os.O_NONBLOCK)
writer_fd = os.open(marker, os.O_WRONLY)
lock = _make_lock(lock_file)
try:
with lock.write_lock(timeout=2):
pass
finally:
lock.close()
os.close(writer_fd)
os.close(reader_fd)


@pytest.mark.skipif(not hasattr(os, "O_NOFOLLOW"), reason="O_NOFOLLOW required")
def test_symlinked_write_marker_is_refused(lock_file: str, tmp_path: Path) -> None:
victim = tmp_path / "victim"
Expand Down