fix: close workspace ownership and API escapes
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import fcntl
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -14,6 +17,9 @@ def test_verifier_checks_fd_identity(tmp_path):
|
||||
root=tmp_path / "root"; root.mkdir(mode=0o700); writer=root / "writer.lock"; writer.touch(mode=0o600)
|
||||
r=os.open(root, os.O_RDONLY); w=os.open(writer, os.O_RDWR)
|
||||
try:
|
||||
fcntl.flock(w, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
if hasattr(fcntl, "F_OFD_SETLK") and sys.platform.startswith("linux"):
|
||||
fcntl.fcntl(w, fcntl.F_OFD_SETLK, struct.pack("hhqqi", fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0))
|
||||
st=os.fstat(r); env={"THOTH_WORKSPACE_ID":"abc-workspace", "THOTH_WORKSPACE_REVISION":"a"*40, "THOTH_WORKSPACE_DEVICE":str(st.st_dev), "THOTH_WORKSPACE_INODE":str(st.st_ino)}
|
||||
cap=verify_workspace_writer_fds(writer_fd=w, root_fd=r, env=env)
|
||||
assert cap.inode == st.st_ino
|
||||
@@ -33,3 +39,17 @@ def test_verifier_rejects_unrelated_lock(tmp_path):
|
||||
verify_workspace_writer_fds(writer_fd=forged_fd, root_fd=root_fd, env=env)
|
||||
finally:
|
||||
os.close(forged_fd); os.close(root_fd)
|
||||
|
||||
|
||||
def test_verifier_rejects_independently_opened_unlocked_writer_fd(tmp_path):
|
||||
root = tmp_path / "root"; root.mkdir(mode=0o700)
|
||||
writer = root / "writer.lock"; writer.touch(mode=0o600)
|
||||
root_fd = os.open(root, os.O_RDONLY)
|
||||
writer_fd = os.open(writer, os.O_RDWR)
|
||||
try:
|
||||
st = os.fstat(root_fd)
|
||||
env = {"THOTH_WORKSPACE_ID": "abc-workspace", "THOTH_WORKSPACE_REVISION": "a" * 40, "THOTH_WORKSPACE_DEVICE": str(st.st_dev), "THOTH_WORKSPACE_INODE": str(st.st_ino)}
|
||||
with pytest.raises(WorkspaceWriterConflict):
|
||||
verify_workspace_writer_fds(writer_fd=writer_fd, root_fd=root_fd, env=env)
|
||||
finally:
|
||||
os.close(writer_fd); os.close(root_fd)
|
||||
|
||||
@@ -11,6 +11,8 @@ import fcntl
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -76,14 +78,34 @@ def verify_workspace_writer_fds(*, writer_fd: int = 3, root_fd: int = 4, env: di
|
||||
lock = _fstat(lock_fd)
|
||||
if (lock.st_dev, lock.st_ino) != (writer.st_dev, writer.st_ino) or not stat.S_ISREG(lock.st_mode) or lock.st_uid != uid or (lock.st_mode & 0o777) != 0o600 or lock.st_nlink != 1:
|
||||
raise WorkspaceWriterConflict()
|
||||
# A duplicate of the locked open description is re-lockable. An
|
||||
# independently-opened description receives EWOULDBLOCK.
|
||||
# Probe using a *different* open file description. An inherited FD 3
|
||||
# is valid only when its lock is already held: the independent probe
|
||||
# must therefore receive EWOULDBLOCK. We deliberately never flock(3)
|
||||
# here: doing so would turn an unheld, independently opened descriptor
|
||||
# into an apparently valid capability.
|
||||
try:
|
||||
fcntl.flock(writer_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
# Linux OFD locks identify the open file description rather than
|
||||
# the process. The inherited FD 3 must already own this lock;
|
||||
# an independent unlocked description can acquire the probe and
|
||||
# is rejected. Darwin has no OFD constants, so retain flock's
|
||||
# equivalent open-description probe there.
|
||||
ofd_setlk = getattr(fcntl, "F_OFD_SETLK", None) if sys.platform.startswith("linux") else None
|
||||
if ofd_setlk is not None:
|
||||
lock_record = struct.pack("hhqqi", fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0)
|
||||
fcntl.fcntl(lock_fd, ofd_setlk, lock_record)
|
||||
unlock_record = struct.pack("hhqqi", fcntl.F_UNLCK, os.SEEK_SET, 0, 0, 0)
|
||||
fcntl.fcntl(lock_fd, ofd_setlk, unlock_record)
|
||||
raise WorkspaceWriterConflict()
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError as exc:
|
||||
if exc.errno in (errno.EACCES, errno.EAGAIN, errno.EWOULDBLOCK):
|
||||
raise WorkspaceWriterConflict() from None
|
||||
raise WorkspaceWriterConflict() from exc
|
||||
if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EWOULDBLOCK):
|
||||
raise WorkspaceWriterConflict() from exc
|
||||
else:
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
except OSError:
|
||||
pass
|
||||
raise WorkspaceWriterConflict()
|
||||
finally:
|
||||
try:
|
||||
os.close(lock_fd)
|
||||
|
||||
Reference in New Issue
Block a user