util/linux: add explicit FS_IMMUTABLE_FL helpers

The FS_IOC_{GET,SET}FLAGS ioctl numbers are not stable across different
architectures. Most of them use the asm-generic versions, but ALPHA and
SPARC in particular use completely different IOC number setups (see the
definition of _IOC, _IOR, _IOW, etc. in the kernel).

This commit moves the helpers for `FS_IMMUTABLE_FL` into
`osbuild/util/` and adds explicit tests. This will make sure that we
catch any ioctl mismatches as soon as possible when we run the osbuild
test-suite on other architectures. Until then, we will have to live with
this mismatch.
This commit is contained in:
David Rheinsberg 2020-04-20 11:52:57 +02:00 committed by Tom Gundersen
parent 2cc9160099
commit e5ff287f5a
2 changed files with 165 additions and 0 deletions

93
osbuild/util/linux.py Normal file
View file

@ -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)

72
test/test_util_linux.py Normal file
View file

@ -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()