debian-forge/osbuild/util/linux.py
2022-09-12 13:32:51 +02:00

276 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