diff --git a/osbuild/util/linux.py b/osbuild/util/linux.py new file mode 100644 index 00000000..29a69bf0 --- /dev/null +++ b/osbuild/util/linux.py @@ -0,0 +1,93 @@ +"""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 specifc +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 fcntl + + +__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 + + +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) diff --git a/test/test_util_linux.py b/test/test_util_linux.py new file mode 100644 index 00000000..781098ff --- /dev/null +++ b/test/test_util_linux.py @@ -0,0 +1,72 @@ +# +# Tests for the `osbuild.util.linux` module. +# + + +import os +import subprocess +import tempfile +import unittest + +import osbuild.util.linux as linux + + +class TestUtilLinux(unittest.TestCase): + def setUp(self): + self.vartmpdir = tempfile.TemporaryDirectory(dir="/var/tmp") + + def tearDown(self): + self.vartmpdir.cleanup() + + def test_ioctl_get_immutable(self): + # + # Test the `ioctl_get_immutable()` helper and make sure it works + # as intended. + # + + with open(f"{self.vartmpdir.name}/immutable", "x") as f: + assert not linux.ioctl_get_immutable(f.fileno()) + + @unittest.skipUnless(os.geteuid() == 0, "root-only") + def test_ioctl_toggle_immutable(self): + # + # Test the `ioctl_toggle_immutable()` helper and make sure it works + # as intended. + # + + with open(f"{self.vartmpdir.name}/immutable", "x") as f: + # Check the file is mutable by default and if we clear it again. + assert not linux.ioctl_get_immutable(f.fileno()) + linux.ioctl_toggle_immutable(f.fileno(), False) + assert not linux.ioctl_get_immutable(f.fileno()) + + # Set immutable and check for it. Try again to verify with flag set. + linux.ioctl_toggle_immutable(f.fileno(), True) + assert linux.ioctl_get_immutable(f.fileno()) + linux.ioctl_toggle_immutable(f.fileno(), True) + assert linux.ioctl_get_immutable(f.fileno()) + + # Verify immutable files cannot be unlinked. + with self.assertRaises(OSError): + os.unlink(f"{self.vartmpdir.name}/immutable") + + # Check again that clearing the flag works. + linux.ioctl_toggle_immutable(f.fileno(), False) + assert not linux.ioctl_get_immutable(f.fileno()) + + # This time, check that we actually set the same flag as `chattr`. + subprocess.run(["chattr", "+i", + f"{self.vartmpdir.name}/immutable"], check=True) + assert linux.ioctl_get_immutable(f.fileno()) + + # Same for clearing it. + subprocess.run(["chattr", "-i", + f"{self.vartmpdir.name}/immutable"], check=True) + assert not linux.ioctl_get_immutable(f.fileno()) + + # Verify we can unlink the file again, once the flag is cleared. + os.unlink(f"{self.vartmpdir.name}/immutable") + + +if __name__ == "__main__": + unittest.main()