From d8ecc510108426fba8f9a53e0d5fa54d5942e75f Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 15 Feb 2024 22:31:51 +0100 Subject: MountFlags: support conversion to and from a textual representation The textual representation matches util-linux. Not all flag values can be represented textually. --- examples/cgroup.py | 6 +-- linuxnamespaces/syscalls.py | 117 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_simple.py | 18 +++++++ 3 files changed, 136 insertions(+), 5 deletions(-) diff --git a/examples/cgroup.py b/examples/cgroup.py index 06eb3b3..0c52efb 100755 --- a/examples/cgroup.py +++ b/examples/cgroup.py @@ -123,11 +123,7 @@ def main() -> None: "tmpfs", "/sys", "tmpfs", - linuxnamespaces.MountFlags.REMOUNT - | linuxnamespaces.MountFlags.RDONLY - | linuxnamespaces.MountFlags.NOEXEC - | linuxnamespaces.MountFlags.NOSUID - | linuxnamespaces.MountFlags.NODEV, + linuxnamespaces.MountFlags.fromstr("remount,ro,noexec,nosuid,nodev"), "mode=0755", ) linuxnamespaces.move_mount(cgroupfd, "/sys/fs/cgroup") diff --git a/linuxnamespaces/syscalls.py b/linuxnamespaces/syscalls.py index 0e33a44..338e602 100644 --- a/linuxnamespaces/syscalls.py +++ b/linuxnamespaces/syscalls.py @@ -112,6 +112,123 @@ class MountFlags(enum.IntFlag): PROPAGATION_FLAGS = UNBINDABLE | PRIVATE | SLAVE | SHARED + # Map each flag to: + # * The flag value + # * Whether the flag value is negated + # * Whether the flag must be negated + # * Whether the flag can be negated + __flagstrmap = { + "acl": (POSIXACL, False, False, False), + "async": (SYNCHRONOUS, True, False, False), + "atime": (NOATIME, True, False, True), + "bind": (BIND, False, False, False), + "dev": (NODEV, True, False, True), + "diratime": (NODIRATIME, True, False, True), + "dirsync": (DIRSYNC, False, False, False), + "exec": (NOEXEC, True, False, True), + "iversion": (I_VERSION, False, False, True), + "lazytime": (LAZYTIME, False, False, True), + "loud": (SILENT, True, False, False), + "mand": (MANDLOCK, False, False, True), + "private": (PRIVATE, False, False, False), + "rbind": (BIND | REC, False, False, False), + "relatime": (RELATIME, False, False, True), + "remount": (REMOUNT, False, False, True), + "ro": (RDONLY, False, False, False), + "rprivate": (PRIVATE | REC, False, False, False), + "rshared": (SHARED | REC, False, False, False), + "rslave": (SLAVE | REC, False, False, False), + "runbindable": (UNBINDABLE | REC, False, False, False), + "rw": (RDONLY, True, False, False), + "shared": (SHARED, False, False, False), + "silent": (SILENT, False, False, False), + "slave": (SLAVE, False, False, False), + "strictatime": (STRICTATIME, False, False, True), + "suid": (NOSUID, True, False, True), + "symfollow": (NOSYMFOLLOW, True, False, True), + "sync": (SYNCHRONOUS, False, False, False), + "unbindable": (UNBINDABLE, False, False, False), + } + + def change(self, flagsstr: str) -> "MountFlags": + """Return modified mount flags after applying comma-separated mount + flags represented as a str. Raise a ValueError if any given flag + does not correspond to a textual mount flag. + """ + ret = self + for flagstr in flagsstr.split(","): + if not flagstr: + continue + flag, negated, mustnegate, cannegate = self.__flagstrmap.get( + flagstr.removeprefix("no"), + (MountFlags.NONE, False, True, False), + ) + if mustnegate <= flagstr.startswith("no") <= cannegate: + if negated ^ flagstr.startswith("no"): + ret &= ~flag + else: + if flag & MountFlags.PROPAGATION_FLAGS: + ret &= ~MountFlags.PROPAGATION_FLAGS + ret |= flag + else: + raise ValueError(f"not a valid mount flag: {flagstr!r}") + return ret + + @staticmethod + def fromstr(flagsstr: str) -> "MountFlags": + """Construct mount flags by changing flags according to the passed + flagsstr using the change method on an initial value with all flags + cleared. + """ + return MountFlags.NONE.change(flagsstr) + + __flagvals: list[tuple[int, str]] = sorted( + [ + (RDONLY, "ro"), + (NOSUID, "nosuid"), + (NODEV, "nodev"), + (NOEXEC, "noexec"), + (SYNCHRONOUS, "sync"), + (REMOUNT, "remount"), + (MANDLOCK, "mand"), + (DIRSYNC, "dirsync"), + (NOSYMFOLLOW, "nosymfollow"), + (NOATIME, "noatime"), + (NODIRATIME, "nodiratime"), + (BIND, "bind"), + (BIND | REC, "rbind"), + (SILENT, "silent"), + (POSIXACL, "acl"), + (UNBINDABLE, "unbindable"), + (UNBINDABLE | REC, "runbindable"), + (PRIVATE, "private"), + (PRIVATE | REC, "rprivate"), + (SLAVE, "slave"), + (SLAVE | REC, "rslave"), + (SHARED, "shared"), + (SHARED | REC, "rshared"), + (RELATIME, "relatime"), + (I_VERSION, "iversion"), + (STRICTATIME, "strictatime"), + (LAZYTIME, "lazytime"), + ], + reverse=True + ) + + def tostr(self) -> str: + """Attempt to represent the flags in a comma-separated, textual way.""" + if (self & MountFlags.PROPAGATION_FLAGS).bit_count() > 1: + raise ValueError("cannot represent conflicting propagtion flags") + parts: list[str] = [] + remain = self + for val, text in MountFlags.__flagvals: + if remain & val == val: + parts.insert(0, text) + remain &= ~val + if remain: + raise ValueError("cannot represent flags {remain}") + return ",".join(parts) + class MountSetattrFlags(enum.IntFlag): """This value may be supplied as flags to mount_setattr(2).""" diff --git a/tests/test_simple.py b/tests/test_simple.py index cb654aa..960bf02 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -12,6 +12,24 @@ import pytest import linuxnamespaces +class MountFlagsTest(unittest.TestCase): + def test_tostrfromstr(self) -> None: + for bit1 in range(32): + for bit2 in range(bit1, 32): + flag = ( + linuxnamespaces.MountFlags(1 << bit1) + | linuxnamespaces.MountFlags(1 << bit2) + ) + try: + text = flag.tostr() + except ValueError: + continue + self.assertEqual( + linuxnamespaces.MountFlags.fromstr(text), + flag + ) + + class IDAllocationTest(unittest.TestCase): def test_idalloc(self) -> None: alloc = linuxnamespaces.IDAllocation() -- cgit v1.2.3