Skip to content

API reference

Rulesets

landlockpy.Ruleset

A Landlock ruleset under construction.

A ruleset declares which access rights it handles. Handled rights are denied by default once the ruleset is enforced. The allow_path() and allow_port() methods grant back specific rights for specific objects:

with Ruleset() as ruleset:
    ruleset.allow_path("/usr", AccessFS.READ_FILE | AccessFS.READ_DIR)
    ruleset.allow_port(443, AccessNet.CONNECT_TCP)
    ruleset.restrict()

Rights the running kernel does not support are dropped in best-effort mode (the default), or rejected with UnsupportedError when best_effort is False.

The ruleset owns a kernel file descriptor. Use it as a context manager or call close() to release it.

Kernel reference: https://docs.kernel.org/userspace-api/landlock.html

Source code in src/landlockpy/ruleset.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
class Ruleset:
    """A Landlock ruleset under construction.

    A ruleset declares which access rights it handles. Handled rights are
    denied by default once the ruleset is enforced. The allow_path()
    and allow_port() methods grant back specific rights for specific objects:

        with Ruleset() as ruleset:
            ruleset.allow_path("/usr", AccessFS.READ_FILE | AccessFS.READ_DIR)
            ruleset.allow_port(443, AccessNet.CONNECT_TCP)
            ruleset.restrict()

    Rights the running kernel does not support are dropped in best-effort
    mode (the default), or rejected with UnsupportedError when best_effort
    is False.

    The ruleset owns a kernel file descriptor. Use it as a context manager
    or call close() to release it.

    Kernel reference: https://docs.kernel.org/userspace-api/landlock.html
    """

    __slots__ = (
        "_abi",
        "_best_effort",
        "_closed",
        "_enforced",
        "_fd",
        "_handled_fs",
        "_handled_net",
        "_scoped",
    )

    def __init__(
        self,
        *,
        handled_fs: AccessFS | None = None,
        handled_net: AccessNet | None = None,
        scoped: Scope = Scope.NONE,
        quiet_fs: AccessFS = AccessFS.NONE,
        quiet_net: AccessNet = AccessNet.NONE,
        quiet_scoped: Scope = Scope.NONE,
        best_effort: bool = True,
    ) -> None:
        self._fd = -1
        self._closed = True
        self._enforced = False
        self._best_effort = best_effort
        self._abi = _syscall.abi_version()

        req_fs = fs_for_abi(LATEST_ABI) if handled_fs is None else AccessFS(handled_fs)
        req_net = (
            net_for_abi(LATEST_ABI) if handled_net is None else AccessNet(handled_net)
        )
        req_scoped = Scope(scoped)

        if best_effort:
            self._handled_fs = req_fs & fs_for_abi(self._abi)
            self._handled_net = req_net & net_for_abi(self._abi)
            self._scoped = req_scoped & scope_for_abi(self._abi)
        else:
            self._reject_unsupported(req_fs, fs_for_abi(self._abi), "filesystem")
            self._reject_unsupported(req_net, net_for_abi(self._abi), "network")
            self._reject_unsupported(req_scoped, scope_for_abi(self._abi), "scope")
            self._handled_fs = req_fs
            self._handled_net = req_net
            self._scoped = req_scoped

        quiet_fs, quiet_net, quiet_scoped = (
            AccessFS(quiet_fs),
            AccessNet(quiet_net),
            Scope(quiet_scoped),
        )
        if self._abi < 10 and (quiet_fs or quiet_net or quiet_scoped):
            if not best_effort:
                raise UnsupportedError("quiet rules require ABI 10")
            quiet_fs, quiet_net, quiet_scoped = (
                AccessFS.NONE,
                AccessNet.NONE,
                Scope.NONE,
            )
        if quiet_fs & ~self._handled_fs:
            raise ValueError("quiet_fs must be a subset of handled_fs")
        if quiet_net & ~self._handled_net:
            raise ValueError("quiet_net must be a subset of handled_net")
        if quiet_scoped & ~self._scoped:
            raise ValueError("quiet_scoped must be a subset of scoped")

        attr = _syscall.RulesetAttr(
            handled_access_fs=int(self._handled_fs),
            handled_access_net=int(self._handled_net),
            scoped=int(self._scoped),
            quiet_access_fs=int(quiet_fs),
            quiet_access_net=int(quiet_net),
            quiet_scoped=int(quiet_scoped),
        )
        fd = _syscall.create_ruleset(attr)
        try:
            os.set_inheritable(fd, False)
        except BaseException:
            os.close(fd)
            raise
        self._fd = fd
        self._closed = False

    @staticmethod
    def _reject_unsupported(requested: int, supported: int, kind: str) -> None:
        missing = requested & ~supported
        if missing:
            raise UnsupportedError(
                f"kernel does not support requested {kind} rights: {missing:#x}"
            )

    def _gate_quiet(self, quiet: bool) -> bool:
        if quiet and self._abi < 10:
            if not self._best_effort:
                raise UnsupportedError("quiet rules require ABI 10")
            return False
        return quiet

    @property
    def abi_version(self) -> int:
        """Landlock ABI version of the running kernel."""
        return self._abi

    @property
    def handled_fs(self) -> AccessFS:
        """Filesystem rights this ruleset denies by default."""
        return self._handled_fs

    @property
    def handled_net(self) -> AccessNet:
        """Network rights this ruleset denies by default."""
        return self._handled_net

    @property
    def scoped(self) -> Scope:
        """Scope isolation flags applied to the domain."""
        return self._scoped

    @property
    def enforced(self) -> bool:
        """Whether restrict() has been called successfully."""
        return self._enforced

    @property
    def closed(self) -> bool:
        """Whether the ruleset file descriptor has been closed."""
        return self._closed

    def fileno(self) -> int:
        """Return the underlying ruleset file descriptor."""
        if self._closed:
            raise ValueError("ruleset is closed")
        return self._fd

    def _check_mutable(self) -> None:
        if self._closed:
            raise RuntimeError("ruleset is closed")
        if self._enforced:
            raise RuntimeError("ruleset is already enforced")

    def allow_path(
        self, path: str | os.PathLike[str], access: AccessFS, *, quiet: bool = False
    ) -> AccessFS:
        """Grant filesystem access rights on a file hierarchy.

        The path can point to a file or a directory. A directory rule covers
        its whole hierarchy. Access is masked against the handled filesystem
        rights. Returns the rights actually granted, which is empty if none
        of the requested rights are handled and no rule was added.

        quiet marks the rule with LANDLOCK_ADD_RULE_QUIET, suppressing audit
        logs for accesses the ruleset declared quiet. Quiet requires ABI 10.
        On older kernels it is dropped in best-effort mode or rejected with
        UnsupportedError in strict mode. See "Extending a ruleset" in the
        kernel documentation.
        """
        self._check_mutable()
        quiet = self._gate_quiet(quiet)
        granted = AccessFS(access) & self._handled_fs
        if not granted:
            return AccessFS.NONE
        parent_fd = os.open(path, os.O_PATH | os.O_CLOEXEC)
        try:
            attr = _syscall.PathBeneathAttr(
                allowed_access=int(granted), parent_fd=parent_fd
            )
            flags = _syscall.ADD_RULE_QUIET if quiet else 0
            _syscall.add_path_beneath(self._fd, attr, flags)
        finally:
            os.close(parent_fd)
        return granted

    def allow_port(
        self, port: int, access: AccessNet, *, quiet: bool = False
    ) -> AccessNet:
        """Grant network access rights on a TCP or UDP port.

        Port 0 covers the ephemeral port range used by auto-bound sockets.
        Access is masked against the handled network rights. Returns the
        rights actually granted, which is empty if none of the requested
        rights are handled and no rule was added.

        A LandlockError with errno EAFNOSUPPORT means the kernel lacks
        TCP/IP support. The operation is impossible anyway and the error
        can safely be ignored. See "Extending a ruleset" in the kernel
        documentation.
        """
        self._check_mutable()
        port = operator.index(port)
        if not 0 <= port <= 65535:
            raise ValueError(f"port out of range: {port}")
        quiet = self._gate_quiet(quiet)
        granted = AccessNet(access) & self._handled_net
        if not granted:
            return AccessNet.NONE
        attr = _syscall.NetPortAttr(allowed_access=int(granted), port=port)
        flags = _syscall.ADD_RULE_QUIET if quiet else 0
        _syscall.add_net_port(self._fd, attr, flags)
        return granted

    def restrict(
        self, flags: RestrictFlag = RestrictFlag.NONE, *, no_new_privs: bool = True
    ) -> None:
        """Enforce the ruleset on the calling thread and its future children.

        Flags unsupported by the running kernel are dropped in best-effort
        mode, or rejected with UnsupportedError when best_effort is False.
        With no_new_privs, the thread is also prevented from gaining
        privileges through suid or file-capability binaries. On ABI 11 and
        newer this is set atomically with enforcement. On older kernels a
        prctl(PR_SET_NO_NEW_PRIVS) call is made first.

        Enforcement is irreversible and per-thread. Without the TSYNC flag
        (ABI 8), only the calling thread and its future children are
        restricted. Sibling threads keep their own policy. See "Enforcing
        a ruleset" in the kernel documentation.
        """
        if self._closed:
            raise RuntimeError("ruleset is closed")
        if self._enforced:
            raise RuntimeError("ruleset is already enforced")
        effective = RestrictFlag(flags)
        if self._best_effort:
            effective &= restrict_for_abi(self._abi)
        else:
            self._reject_unsupported(effective, restrict_for_abi(self._abi), "restrict")
        if no_new_privs:
            if self._abi >= 11:
                effective |= RestrictFlag.NO_NEW_PRIVS
            else:
                _syscall.set_no_new_privs()
        _syscall.restrict_self(self._fd, int(effective))
        self._enforced = True

    def close(self) -> None:
        """Close the ruleset file descriptor. Safe to call twice."""
        if not self._closed:
            self._closed = True
            os.close(self._fd)

    def __copy__(self) -> Ruleset:
        raise TypeError("Ruleset cannot be copied: it owns a kernel file descriptor")

    def __deepcopy__(self, memo: dict[int, object]) -> Ruleset:
        raise TypeError("Ruleset cannot be copied: it owns a kernel file descriptor")

    def __getstate__(self) -> None:
        raise TypeError("Ruleset cannot be pickled: it owns a kernel file descriptor")

    def __repr__(self) -> str:
        state = "closed" if self._closed else "enforced" if self._enforced else "open"
        abi = getattr(self, "_abi", "?")
        return f"{type(self).__name__}(fd={self._fd}, abi={abi}, {state})"

    def __enter__(self) -> Ruleset:
        if self._closed:
            raise RuntimeError("ruleset is closed")
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: types.TracebackType | None,
    ) -> None:
        self.close()

    def __del__(self) -> None:
        # __del__ must never raise
        with contextlib.suppress(Exception):
            self.close()

abi_version property

Landlock ABI version of the running kernel.

handled_fs property

Filesystem rights this ruleset denies by default.

handled_net property

Network rights this ruleset denies by default.

scoped property

Scope isolation flags applied to the domain.

enforced property

Whether restrict() has been called successfully.

closed property

Whether the ruleset file descriptor has been closed.

fileno()

Return the underlying ruleset file descriptor.

Source code in src/landlockpy/ruleset.py
178
179
180
181
182
def fileno(self) -> int:
    """Return the underlying ruleset file descriptor."""
    if self._closed:
        raise ValueError("ruleset is closed")
    return self._fd

allow_path(path, access, *, quiet=False)

Grant filesystem access rights on a file hierarchy.

The path can point to a file or a directory. A directory rule covers its whole hierarchy. Access is masked against the handled filesystem rights. Returns the rights actually granted, which is empty if none of the requested rights are handled and no rule was added.

quiet marks the rule with LANDLOCK_ADD_RULE_QUIET, suppressing audit logs for accesses the ruleset declared quiet. Quiet requires ABI 10. On older kernels it is dropped in best-effort mode or rejected with UnsupportedError in strict mode. See "Extending a ruleset" in the kernel documentation.

Source code in src/landlockpy/ruleset.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def allow_path(
    self, path: str | os.PathLike[str], access: AccessFS, *, quiet: bool = False
) -> AccessFS:
    """Grant filesystem access rights on a file hierarchy.

    The path can point to a file or a directory. A directory rule covers
    its whole hierarchy. Access is masked against the handled filesystem
    rights. Returns the rights actually granted, which is empty if none
    of the requested rights are handled and no rule was added.

    quiet marks the rule with LANDLOCK_ADD_RULE_QUIET, suppressing audit
    logs for accesses the ruleset declared quiet. Quiet requires ABI 10.
    On older kernels it is dropped in best-effort mode or rejected with
    UnsupportedError in strict mode. See "Extending a ruleset" in the
    kernel documentation.
    """
    self._check_mutable()
    quiet = self._gate_quiet(quiet)
    granted = AccessFS(access) & self._handled_fs
    if not granted:
        return AccessFS.NONE
    parent_fd = os.open(path, os.O_PATH | os.O_CLOEXEC)
    try:
        attr = _syscall.PathBeneathAttr(
            allowed_access=int(granted), parent_fd=parent_fd
        )
        flags = _syscall.ADD_RULE_QUIET if quiet else 0
        _syscall.add_path_beneath(self._fd, attr, flags)
    finally:
        os.close(parent_fd)
    return granted

allow_port(port, access, *, quiet=False)

Grant network access rights on a TCP or UDP port.

Port 0 covers the ephemeral port range used by auto-bound sockets. Access is masked against the handled network rights. Returns the rights actually granted, which is empty if none of the requested rights are handled and no rule was added.

A LandlockError with errno EAFNOSUPPORT means the kernel lacks TCP/IP support. The operation is impossible anyway and the error can safely be ignored. See "Extending a ruleset" in the kernel documentation.

Source code in src/landlockpy/ruleset.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def allow_port(
    self, port: int, access: AccessNet, *, quiet: bool = False
) -> AccessNet:
    """Grant network access rights on a TCP or UDP port.

    Port 0 covers the ephemeral port range used by auto-bound sockets.
    Access is masked against the handled network rights. Returns the
    rights actually granted, which is empty if none of the requested
    rights are handled and no rule was added.

    A LandlockError with errno EAFNOSUPPORT means the kernel lacks
    TCP/IP support. The operation is impossible anyway and the error
    can safely be ignored. See "Extending a ruleset" in the kernel
    documentation.
    """
    self._check_mutable()
    port = operator.index(port)
    if not 0 <= port <= 65535:
        raise ValueError(f"port out of range: {port}")
    quiet = self._gate_quiet(quiet)
    granted = AccessNet(access) & self._handled_net
    if not granted:
        return AccessNet.NONE
    attr = _syscall.NetPortAttr(allowed_access=int(granted), port=port)
    flags = _syscall.ADD_RULE_QUIET if quiet else 0
    _syscall.add_net_port(self._fd, attr, flags)
    return granted

restrict(flags=RestrictFlag.NONE, *, no_new_privs=True)

Enforce the ruleset on the calling thread and its future children.

Flags unsupported by the running kernel are dropped in best-effort mode, or rejected with UnsupportedError when best_effort is False. With no_new_privs, the thread is also prevented from gaining privileges through suid or file-capability binaries. On ABI 11 and newer this is set atomically with enforcement. On older kernels a prctl(PR_SET_NO_NEW_PRIVS) call is made first.

Enforcement is irreversible and per-thread. Without the TSYNC flag (ABI 8), only the calling thread and its future children are restricted. Sibling threads keep their own policy. See "Enforcing a ruleset" in the kernel documentation.

Source code in src/landlockpy/ruleset.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def restrict(
    self, flags: RestrictFlag = RestrictFlag.NONE, *, no_new_privs: bool = True
) -> None:
    """Enforce the ruleset on the calling thread and its future children.

    Flags unsupported by the running kernel are dropped in best-effort
    mode, or rejected with UnsupportedError when best_effort is False.
    With no_new_privs, the thread is also prevented from gaining
    privileges through suid or file-capability binaries. On ABI 11 and
    newer this is set atomically with enforcement. On older kernels a
    prctl(PR_SET_NO_NEW_PRIVS) call is made first.

    Enforcement is irreversible and per-thread. Without the TSYNC flag
    (ABI 8), only the calling thread and its future children are
    restricted. Sibling threads keep their own policy. See "Enforcing
    a ruleset" in the kernel documentation.
    """
    if self._closed:
        raise RuntimeError("ruleset is closed")
    if self._enforced:
        raise RuntimeError("ruleset is already enforced")
    effective = RestrictFlag(flags)
    if self._best_effort:
        effective &= restrict_for_abi(self._abi)
    else:
        self._reject_unsupported(effective, restrict_for_abi(self._abi), "restrict")
    if no_new_privs:
        if self._abi >= 11:
            effective |= RestrictFlag.NO_NEW_PRIVS
        else:
            _syscall.set_no_new_privs()
    _syscall.restrict_self(self._fd, int(effective))
    self._enforced = True

close()

Close the ruleset file descriptor. Safe to call twice.

Source code in src/landlockpy/ruleset.py
284
285
286
287
288
def close(self) -> None:
    """Close the ruleset file descriptor. Safe to call twice."""
    if not self._closed:
        self._closed = True
        os.close(self._fd)

landlockpy.mute_subdomain_logs(*, all_threads=False, no_new_privs=True)

Suppress audit logging for Landlock domains nested under this one.

Calls landlock_restrict_self with a ruleset file descriptor of -1, which updates the logging configuration without creating a new domain. Denied accesses originating from nested domains created afterwards by the caller or its descendants are not logged. Requires ABI 7.

With all_threads, the configuration is propagated to every thread of the current process, which requires ABI 8.

Like restrict(), the kernel requires the calling thread to already run with no_new_privs or hold CAP_SYS_ADMIN. With no_new_privs=True the attribute is set via prctl(PR_SET_NO_NEW_PRIVS) first. The LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS flag cannot be combined with a ruleset file descriptor of -1, so prctl is used on every ABI. See "Logging" and "Enforcing a ruleset" in the kernel documentation.

Source code in src/landlockpy/ruleset.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
def mute_subdomain_logs(
    *, all_threads: bool = False, no_new_privs: bool = True
) -> None:
    """Suppress audit logging for Landlock domains nested under this one.

    Calls landlock_restrict_self with a ruleset file descriptor of -1,
    which updates the logging configuration without creating a new domain.
    Denied accesses originating from nested domains created afterwards by
    the caller or its descendants are not logged. Requires ABI 7.

    With all_threads, the configuration is propagated to every thread of
    the current process, which requires ABI 8.

    Like restrict(), the kernel requires the calling thread to already run
    with no_new_privs or hold CAP_SYS_ADMIN. With no_new_privs=True the
    attribute is set via prctl(PR_SET_NO_NEW_PRIVS) first. The
    LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS flag cannot be combined with a
    ruleset file descriptor of -1, so prctl is used on every ABI. See
    "Logging" and "Enforcing a ruleset" in the kernel documentation.
    """
    abi = _syscall.abi_version()
    if abi < 7:
        raise UnsupportedError("subdomain log control requires ABI 7")
    flags = int(RestrictFlag.LOG_SUBDOMAINS_OFF)
    if all_threads:
        if abi < 8:
            raise UnsupportedError("TSYNC requires ABI 8")
        flags |= int(RestrictFlag.TSYNC)
    if no_new_privs:
        _syscall.set_no_new_privs()
    _syscall.restrict_self(-1, flags)

Access rights and flags

landlockpy.AccessFS

Bases: IntFlag

Filesystem access rights, mirroring LANDLOCK_ACCESS_FS_*.

Used in the handled_access_fs field of a ruleset and in the allowed_access field of path rules. See "Filesystem flags" in the kernel documentation.

Source code in src/landlockpy/flags.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class AccessFS(IntFlag):
    """Filesystem access rights, mirroring LANDLOCK_ACCESS_FS_*.

    Used in the handled_access_fs field of a ruleset and in the
    allowed_access field of path rules. See "Filesystem flags" in the
    kernel documentation.
    """

    NONE = 0
    EXECUTE = 1 << 0
    WRITE_FILE = 1 << 1
    READ_FILE = 1 << 2
    READ_DIR = 1 << 3
    REMOVE_DIR = 1 << 4
    REMOVE_FILE = 1 << 5
    MAKE_CHAR = 1 << 6
    MAKE_DIR = 1 << 7
    MAKE_REG = 1 << 8
    MAKE_SOCK = 1 << 9
    MAKE_FIFO = 1 << 10
    MAKE_BLOCK = 1 << 11
    MAKE_SYM = 1 << 12
    REFER = 1 << 13
    TRUNCATE = 1 << 14
    IOCTL_DEV = 1 << 15
    RESOLVE_UNIX = 1 << 16

landlockpy.AccessNet

Bases: IntFlag

Network access rights, mirroring LANDLOCK_ACCESS_NET_*.

Used in the handled_access_net field of a ruleset and in the allowed_access field of port rules. See "Network flags" in the kernel documentation.

Source code in src/landlockpy/flags.py
58
59
60
61
62
63
64
65
66
67
68
69
70
class AccessNet(IntFlag):
    """Network access rights, mirroring LANDLOCK_ACCESS_NET_*.

    Used in the handled_access_net field of a ruleset and in the
    allowed_access field of port rules. See "Network flags" in the
    kernel documentation.
    """

    NONE = 0
    BIND_TCP = 1 << 0
    CONNECT_TCP = 1 << 1
    BIND_UDP = 1 << 2
    CONNECT_SEND_UDP = 1 << 3

landlockpy.Scope

Bases: IntFlag

Scope flags, mirroring LANDLOCK_SCOPE_*.

Set on a ruleset to isolate the domain from resources outside it, such as abstract UNIX sockets or signals. See "Scope flags" in the kernel documentation.

Source code in src/landlockpy/flags.py
73
74
75
76
77
78
79
80
81
82
83
class Scope(IntFlag):
    """Scope flags, mirroring LANDLOCK_SCOPE_*.

    Set on a ruleset to isolate the domain from resources outside it,
    such as abstract UNIX sockets or signals. See "Scope flags" in the
    kernel documentation.
    """

    NONE = 0
    ABSTRACT_UNIX_SOCKET = 1 << 0
    SIGNAL = 1 << 1

landlockpy.RestrictFlag

Bases: IntFlag

Flags accepted by landlock_restrict_self, mirroring LANDLOCK_RESTRICT_SELF_*. See "Enforcing a ruleset" in the kernel documentation.

Source code in src/landlockpy/flags.py
86
87
88
89
90
91
92
93
94
95
96
97
class RestrictFlag(IntFlag):
    """Flags accepted by landlock_restrict_self, mirroring
    LANDLOCK_RESTRICT_SELF_*. See "Enforcing a ruleset" in the kernel
    documentation.
    """

    NONE = 0
    LOG_SAME_EXEC_OFF = 1 << 0
    LOG_NEW_EXEC_ON = 1 << 1
    LOG_SUBDOMAINS_OFF = 1 << 2
    TSYNC = 1 << 3
    NO_NEW_PRIVS = 1 << 4

Kernel probes

landlockpy.supported()

Return whether the running kernel supports Landlock.

Source code in src/landlockpy/__init__.py
82
83
84
def supported() -> bool:
    """Return whether the running kernel supports Landlock."""
    return abi_version() >= 1

landlockpy.abi_version()

Return the Landlock ABI version of the running kernel, or 0.

A return value of 0 means Landlock is unavailable: the platform is not Linux, the kernel is too old (ENOSYS), or Landlock is disabled at boot time (EOPNOTSUPP).

Source code in src/landlockpy/__init__.py
52
53
54
55
56
57
58
59
60
61
62
63
64
def abi_version() -> int:
    """Return the Landlock ABI version of the running kernel, or 0.

    A return value of 0 means Landlock is unavailable: the platform is not
    Linux, the kernel is too old (ENOSYS), or Landlock is disabled at boot
    time (EOPNOTSUPP).
    """
    try:
        return _syscall.abi_version()
    except LandlockError as exc:
        if exc.errno in (errno.ENOSYS, errno.EOPNOTSUPP):
            return 0
        raise

landlockpy.errata()

Return the errata bitmask for the current ABI version, or 0.

Bit N set means erratum N is fixed in the running kernel. Older kernels without the errata mechanism report 0. Most applications should not check errata. Best-effort enforcement is the safer default.

Source code in src/landlockpy/__init__.py
67
68
69
70
71
72
73
74
75
76
77
78
79
def errata() -> int:
    """Return the errata bitmask for the current ABI version, or 0.

    Bit N set means erratum N is fixed in the running kernel. Older kernels
    without the errata mechanism report 0. Most applications should not
    check errata. Best-effort enforcement is the safer default.
    """
    try:
        return _syscall.errata()
    except LandlockError as exc:
        if exc.errno in (errno.ENOSYS, errno.EOPNOTSUPP, errno.EINVAL):
            return 0
        raise

ABI helpers

landlockpy.fs_for_abi(abi)

Return the filesystem rights supported by the given ABI version.

Source code in src/landlockpy/flags.py
151
152
153
def fs_for_abi(abi: int) -> AccessFS:
    """Return the filesystem rights supported by the given ABI version."""
    return _for_abi(_FS_MIN_ABI, AccessFS, abi)

landlockpy.net_for_abi(abi)

Return the network rights supported by the given ABI version.

Source code in src/landlockpy/flags.py
156
157
158
def net_for_abi(abi: int) -> AccessNet:
    """Return the network rights supported by the given ABI version."""
    return _for_abi(_NET_MIN_ABI, AccessNet, abi)

landlockpy.scope_for_abi(abi)

Return the scope flags supported by the given ABI version.

Source code in src/landlockpy/flags.py
161
162
163
def scope_for_abi(abi: int) -> Scope:
    """Return the scope flags supported by the given ABI version."""
    return _for_abi(_SCOPE_MIN_ABI, Scope, abi)

landlockpy.restrict_for_abi(abi)

Return the restrict flags supported by the given ABI version.

Source code in src/landlockpy/flags.py
166
167
168
def restrict_for_abi(abi: int) -> RestrictFlag:
    """Return the restrict flags supported by the given ABI version."""
    return _for_abi(_RESTRICT_MIN_ABI, RestrictFlag, abi)

Errors

landlockpy.LandlockError

Bases: OSError

A Landlock syscall failed.

Source code in src/landlockpy/errors.py
5
6
class LandlockError(OSError):
    """A Landlock syscall failed."""

landlockpy.UnsupportedError

Bases: LandlockError

The running kernel does not support Landlock or the requested feature.

Source code in src/landlockpy/errors.py
 9
10
class UnsupportedError(LandlockError):
    """The running kernel does not support Landlock or the requested feature."""

Testing helpers

landlockpy.testing.probe(ruleset, fn)

Run fn in a forked child process restricted by ruleset.

The ruleset must be unenforced. It is enforced in the child only, so the caller's copy stays usable. The return value of fn is discarded. The child exits with os._exit, so atexit handlers, buffered I/O and threads do not run there.

Source code in src/landlockpy/testing.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def probe(ruleset: Ruleset, fn: Callable[[], object]) -> ProbeResult:
    """Run fn in a forked child process restricted by ruleset.

    The ruleset must be unenforced. It is enforced in the child only, so
    the caller's copy stays usable. The return value of fn is discarded.
    The child exits with os._exit, so atexit handlers, buffered I/O and
    threads do not run there.
    """
    if ruleset.closed:
        raise RuntimeError("ruleset is closed")
    if ruleset.enforced:
        raise RuntimeError("ruleset is already enforced")

    read_fd, write_fd = os.pipe()
    try:
        pid = os.fork()
    except BaseException:
        os.close(read_fd)
        os.close(write_fd)
        raise
    if pid == 0:
        os.close(read_fd)
        os._exit(_probe_child(ruleset, fn, write_fd))

    os.close(write_fd)
    try:
        _, status = os.waitpid(pid, 0)
        os.set_blocking(read_fd, False)
        try:
            detail = os.read(read_fd, 4096).decode(errors="replace")
        except BlockingIOError:
            detail = ""
    finally:
        os.close(read_fd)

    if os.WIFSIGNALED(status):
        return ProbeResult(ok=False, signal=os.WTERMSIG(status))
    code = os.WEXITSTATUS(status)
    if code == 0:
        return ProbeResult(ok=True)
    if code == 255:
        return ProbeResult(ok=False, exception=detail)
    return ProbeResult(ok=False, errno=code, exception=detail)

landlockpy.testing.probe_path(ruleset, path)

Probe whether the ruleset allows reading the file at path.

Convenience wrapper around probe() for the common case of checking whether a path remains readable under a policy.

Source code in src/landlockpy/testing.py
104
105
106
107
108
109
110
def probe_path(ruleset: Ruleset, path: str | os.PathLike[str]) -> ProbeResult:
    """Probe whether the ruleset allows reading the file at path.

    Convenience wrapper around probe() for the common case of checking
    whether a path remains readable under a policy.
    """
    return probe(ruleset, lambda: Path(path).read_bytes())

landlockpy.testing.ProbeResult dataclass

Outcome of a probe() run.

ok is True when fn completed under the ruleset. When fn or the enforcement raised an OSError, errno holds its errno value such as EACCES or EPERM. For failures that are not OSError, errno is 0 and exception holds a "ClassName: message" string from the child. signal is nonzero if the child was killed by a signal instead of exiting normally.

Source code in src/landlockpy/testing.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@dataclass(frozen=True)
class ProbeResult:
    """Outcome of a probe() run.

    ok is True when fn completed under the ruleset. When fn or the
    enforcement raised an OSError, errno holds its errno value such as
    EACCES or EPERM. For failures that are not OSError, errno is 0 and
    exception holds a "ClassName: message" string from the child. signal
    is nonzero if the child was killed by a signal instead of exiting
    normally.
    """

    ok: bool
    errno: int = 0
    signal: int = 0
    exception: str = ""