Optional types were provided in places but were not always correct. Add mypy checking and fix those that fail(ed).
277 lines
7.2 KiB
Python
277 lines
7.2 KiB
Python
"""Linux API Access
|
|
|
|
This module provides access to linux system-calls and other APIs, in particular
|
|
those not provided by the python standard library. The idea is to provide
|
|
universal wrappers with broad access to linux APIs. Convenience helpers and
|
|
higher-level abstractions are beyond the scope of this module.
|
|
|
|
In some cases it is overly complex to provide universal access to a specific
|
|
API. Hence, the API might be restricted to a reduced subset of its
|
|
functionality, just to make sure we can actually implement the wrappers in a
|
|
reasonable manner.
|
|
"""
|
|
|
|
|
|
import array
|
|
import ctypes
|
|
import ctypes.util
|
|
import fcntl
|
|
import os
|
|
import platform
|
|
import threading
|
|
|
|
|
|
__all__ = [
|
|
"ioctl_get_immutable",
|
|
"ioctl_toggle_immutable",
|
|
]
|
|
|
|
|
|
# NOTE: These are wrong on at least ALPHA and SPARC. They use different
|
|
# ioctl number setups. We should fix this, but this is really awkward
|
|
# in standard python.
|
|
# Our tests will catch this, so we will not accidentally run into this
|
|
# on those architectures.
|
|
FS_IOC_GETFLAGS = 0x80086601
|
|
FS_IOC_SETFLAGS = 0x40086602
|
|
|
|
FS_IMMUTABLE_FL = 0x00000010
|
|
|
|
|
|
if platform.machine() == "ppc64le":
|
|
BLK_IOC_FLSBUF = 0x20001261
|
|
else:
|
|
BLK_IOC_FLSBUF = 0x00001261
|
|
|
|
|
|
def ioctl_get_immutable(fd: int):
|
|
"""Query FS_IMMUTABLE_FL
|
|
|
|
This queries the `FS_IMMUTABLE_FL` flag on a specified file.
|
|
|
|
Arguments
|
|
---------
|
|
fd
|
|
File-descriptor to operate on.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
Whether the `FS_IMMUTABLE_FL` flag is set or not.
|
|
|
|
Raises
|
|
------
|
|
OSError
|
|
If the underlying ioctl fails, a matching `OSError` will be raised.
|
|
"""
|
|
|
|
if not isinstance(fd, int) or fd < 0:
|
|
raise ValueError()
|
|
|
|
flags = array.array('L', [0])
|
|
fcntl.ioctl(fd, FS_IOC_GETFLAGS, flags, True)
|
|
return bool(flags[0] & FS_IMMUTABLE_FL)
|
|
|
|
|
|
def ioctl_toggle_immutable(fd: int, set_to: bool):
|
|
"""Toggle FS_IMMUTABLE_FL
|
|
|
|
This toggles the `FS_IMMUTABLE_FL` flag on a specified file. It can both set
|
|
and clear the flag.
|
|
|
|
Arguments
|
|
---------
|
|
fd
|
|
File-descriptor to operate on.
|
|
set_to
|
|
Whether to set the `FS_IMMUTABLE_FL` flag or not.
|
|
|
|
Raises
|
|
------
|
|
OSError
|
|
If the underlying ioctl fails, a matching `OSError` will be raised.
|
|
"""
|
|
|
|
if not isinstance(fd, int) or fd < 0:
|
|
raise ValueError()
|
|
|
|
flags = array.array('L', [0])
|
|
fcntl.ioctl(fd, FS_IOC_GETFLAGS, flags, True)
|
|
if set_to:
|
|
flags[0] |= FS_IMMUTABLE_FL
|
|
else:
|
|
flags[0] &= ~FS_IMMUTABLE_FL
|
|
fcntl.ioctl(fd, FS_IOC_SETFLAGS, flags, False)
|
|
|
|
|
|
def ioctl_blockdev_flushbuf(fd: int):
|
|
"""Flush the block device buffer cache
|
|
|
|
NB: This function needs the `CAP_SYS_ADMIN` capability.
|
|
|
|
Arguments
|
|
---------
|
|
fd
|
|
File-descriptor of a block device to operate on.
|
|
|
|
Raises
|
|
------
|
|
OSError
|
|
If the underlying ioctl fails, a matching `OSError`
|
|
will be raised.
|
|
"""
|
|
|
|
if not isinstance(fd, int) or fd < 0:
|
|
raise ValueError(f"Invalid file descriptor: '{fd}'")
|
|
|
|
fcntl.ioctl(fd, BLK_IOC_FLSBUF, 0)
|
|
|
|
|
|
class LibCap:
|
|
"""Wrapper for libcap (capabilities commands and library) project"""
|
|
|
|
cap_value_t = ctypes.c_int
|
|
_lock = threading.Lock()
|
|
_inst = None
|
|
|
|
def __init__(self, lib: ctypes.CDLL) -> None:
|
|
self.lib = lib
|
|
|
|
# process-wide bounding set
|
|
get_bound = lib.cap_get_bound
|
|
get_bound.argtypes = (self.cap_value_t,)
|
|
get_bound.restype = ctypes.c_int
|
|
get_bound.errcheck = self._check_result # type: ignore
|
|
self._get_bound = get_bound
|
|
|
|
from_name = lib.cap_from_name
|
|
from_name.argtypes = (ctypes.c_char_p, ctypes.POINTER(self.cap_value_t),)
|
|
from_name.restype = ctypes.c_int
|
|
from_name.errcheck = self._check_result # type: ignore
|
|
self._from_name = from_name
|
|
|
|
to_name = lib.cap_to_name
|
|
to_name.argtypes = (ctypes.c_int,)
|
|
to_name.restype = ctypes.POINTER(ctypes.c_char)
|
|
to_name.errcheck = self._check_result # type: ignore
|
|
self._to_name = to_name
|
|
|
|
free = lib.cap_free
|
|
free.argtypes = (ctypes.c_void_p,)
|
|
free.restype = ctypes.c_int
|
|
free.errcheck = self._check_result # type: ignore
|
|
self._free = free
|
|
|
|
@staticmethod
|
|
def _check_result(result, func, args):
|
|
if result is None or (isinstance(result, int) and result == -1):
|
|
err = ctypes.get_errno()
|
|
msg = f"{func.__name__}{args} -> {result}: error ({err}): {os.strerror(err)}"
|
|
raise OSError(err, msg)
|
|
return result
|
|
|
|
@staticmethod
|
|
def make():
|
|
path = ctypes.util.find_library("cap")
|
|
if not path:
|
|
return None
|
|
|
|
try:
|
|
lib = ctypes.CDLL(path, use_errno=True)
|
|
except (OSError, ImportError):
|
|
return None
|
|
|
|
return LibCap(lib)
|
|
|
|
@staticmethod
|
|
def last_cap() -> int:
|
|
"""Return the int value of the highest valid capability"""
|
|
try:
|
|
with open("/proc/sys/kernel/cap_last_cap", "rb") as f:
|
|
data = f.read()
|
|
return int(data)
|
|
except FileNotFoundError:
|
|
return 0
|
|
|
|
@classmethod
|
|
def get_default(cls) -> "LibCap":
|
|
"""Return a singleton instance of the library"""
|
|
with cls._lock:
|
|
if cls._inst is None:
|
|
cls._inst = cls.make()
|
|
return cls._inst
|
|
|
|
def get_bound(self, capability: int) -> bool:
|
|
"""Return the current value of the capability in the thread's bounding set"""
|
|
# cap = self.cap_value_t(capability)
|
|
return self._get_bound(capability) == 1
|
|
|
|
def to_name(self, value: int) -> str:
|
|
"""Translate from the capability's integer value to the its symbolic name"""
|
|
raw = self._to_name(value)
|
|
val = ctypes.cast(raw, ctypes.c_char_p).value
|
|
|
|
if val is None:
|
|
raise RuntimeError("Failed to cast.")
|
|
|
|
res = str(val, encoding="utf-8")
|
|
self._free(raw)
|
|
return res.upper()
|
|
|
|
def from_name(self, value: str) -> int:
|
|
"""Translate from the symbolic name to its integer value"""
|
|
cap = self.cap_value_t()
|
|
self._from_name(value.encode("utf-8"), ctypes.pointer(cap))
|
|
return int(cap.value)
|
|
|
|
|
|
def cap_is_supported(capability: str = "CAP_CHOWN") -> bool:
|
|
"""Return whether a given capability is supported by the system"""
|
|
lib = LibCap.get_default()
|
|
if not lib:
|
|
return False
|
|
|
|
try:
|
|
value = lib.from_name(capability)
|
|
lib.get_bound(value)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def cap_bound_set() -> set:
|
|
"""Return the calling thread's capability bounding set
|
|
|
|
If capabilities are not supported this function will return the empty set.
|
|
"""
|
|
lib = LibCap.get_default()
|
|
if not lib:
|
|
return set()
|
|
|
|
res = set(
|
|
lib.to_name(cap)
|
|
for cap in range(lib.last_cap() + 1)
|
|
if lib.get_bound(cap)
|
|
)
|
|
|
|
return res
|
|
|
|
|
|
def cap_mask_to_set(mask: int) -> set:
|
|
lib = LibCap.get_default()
|
|
if not lib:
|
|
return set()
|
|
|
|
def bits(n):
|
|
count = 0
|
|
while n:
|
|
if n & 1:
|
|
yield count
|
|
count += 1
|
|
n >>= 1
|
|
|
|
res = {
|
|
lib.to_name(cap) for cap in bits(mask)
|
|
}
|
|
|
|
return res
|