diff mbox series

[[RFC] 1/2] utils: Add landlock_restrict_network function

Message ID 20260612-landlock-v1-1-77891f63ed7f@est.tech
State New
Headers show
Series [[RFC] 1/2] utils: Add landlock_restrict_network function | expand

Commit Message

David Nyström June 12, 2026, 11:38 a.m. UTC
Add landlock_restrict_network() which blocks TCP bind/connect using
Landlock LSM (ABI v4+, kernel 6.7+). Designed to stack with the
existing disable_network() namespace isolation, covering the case
where disable_network() is skipped for non-local UIDs.

Gracefully returns False on older kernels (ABI < 4).

Signed-off-by: David Nyström <david.nystrom@est.tech>
---
 lib/bb/utils.py | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

Comments

Alexander Kanavin June 13, 2026, 11:52 a.m. UTC | #1
On Fri, 12 Jun 2026 at 14:01, David Nyström via lists.openembedded.org
<david.nystrom=est.tech@lists.openembedded.org> wrote:
> +def landlock_restrict_network():
> +    """Block TCP bind/connect using Landlock LSM (ABI v4+, kernel 6.7+).
> +    Gracefully skipped on older kernels. Stacks with disable_network()."""
> +
> +    NR_CREATE = 444  # landlock_create_ruleset
> +    NR_SELF   = 446  # landlock_restrict_self
> +    NET_TCP   = 0x3  # BIND_TCP | CONNECT_TCP
> +
> +    libc = ctypes.CDLL('libc.so.6')
> +
> +    abi = libc.syscall(NR_CREATE, 0, 0, 1)
> +    if abi < 4:
> +        return False
> +
> +    attr = struct.pack("QQ", 0, NET_TCP)
> +    buf = ctypes.create_string_buffer(attr)
> +    fd = libc.syscall(NR_CREATE, buf, len(attr), 0)
> +    if fd < 0:
> +        return False
> +
> +    libc.prctl(38, 1, 0, 0, 0)  # PR_SET_NO_NEW_PRIVS
> +    r = libc.syscall(NR_SELF, fd, 0)
> +    os.close(fd)
> +    return r == 0

Far too many magic numbers. I would really want to do this with an API.

This also needs some kind of test, e.g. that the function indeed has
the desired effect.

Alex
Richard Purdie June 13, 2026, 2:26 p.m. UTC | #2
On Sat, 2026-06-13 at 13:52 +0200, Alexander Kanavin via
lists.openembedded.org wrote:
> On Fri, 12 Jun 2026 at 14:01, David Nyström via
> lists.openembedded.org
> <david.nystrom=est.tech@lists.openembedded.org> wrote:
> > +def landlock_restrict_network():
> > +    """Block TCP bind/connect using Landlock LSM (ABI v4+, kernel
> > 6.7+).
> > +    Gracefully skipped on older kernels. Stacks with
> > disable_network()."""
> > +
> > +    NR_CREATE = 444  # landlock_create_ruleset
> > +    NR_SELF   = 446  # landlock_restrict_self
> > +    NET_TCP   = 0x3  # BIND_TCP | CONNECT_TCP
> > +
> > +    libc = ctypes.CDLL('libc.so.6')
> > +
> > +    abi = libc.syscall(NR_CREATE, 0, 0, 1)
> > +    if abi < 4:
> > +        return False
> > +
> > +    attr = struct.pack("QQ", 0, NET_TCP)
> > +    buf = ctypes.create_string_buffer(attr)
> > +    fd = libc.syscall(NR_CREATE, buf, len(attr), 0)
> > +    if fd < 0:
> > +        return False
> > +
> > +    libc.prctl(38, 1, 0, 0, 0)  # PR_SET_NO_NEW_PRIVS
> > +    r = libc.syscall(NR_SELF, fd, 0)
> > +    os.close(fd)
> > +    return r == 0
> 
> Far too many magic numbers. I would really want to do this with an
> API.
> 
> This also needs some kind of test, e.g. that the function indeed has
> the desired effect.

Unfortunately, to use tech like this, we do end up needing to do
something like that and utils.py already has quite a bit of it. The
plus side is that the kernel is really good about maintaining these
APIs so the numbers are unlikely to change.

I wouldn't take something like this unless there was a really good case
for using it. Network isolation in more builds probably is a strong
enough use case...

Cheers,

Richard
Paul Barker June 15, 2026, 8:28 a.m. UTC | #3
On Fri, 2026-06-12 at 13:38 +0200, David Nyström wrote:
> Add landlock_restrict_network() which blocks TCP bind/connect using
> Landlock LSM (ABI v4+, kernel 6.7+). Designed to stack with the
> existing disable_network() namespace isolation, covering the case
> where disable_network() is skipped for non-local UIDs.
> 
> Gracefully returns False on older kernels (ABI < 4).
> 
> Signed-off-by: David Nyström <david.nystrom@est.tech>

Hi David,

I think adding this is a good idea, but the code needs a few changes to
ensure it is maintainable.

> ---
>  lib/bb/utils.py | 26 ++++++++++++++++++++++++++
>  1 file changed, 26 insertions(+)
> 
> diff --git a/lib/bb/utils.py b/lib/bb/utils.py
> index 181082c95..1347c29d0 100644
> --- a/lib/bb/utils.py
> +++ b/lib/bb/utils.py
> @@ -2054,6 +2054,32 @@ def disable_network(uid=None, gid=None):
>      with open("/proc/self/gid_map", "w") as f:
>          f.write("%s %s 1" % (gid, gid))
>  
> +def landlock_restrict_network():
> +    """Block TCP bind/connect using Landlock LSM (ABI v4+, kernel 6.7+).
> +    Gracefully skipped on older kernels. Stacks with disable_network()."""
> +
> +    NR_CREATE = 444  # landlock_create_ruleset
> +    NR_SELF   = 446  # landlock_restrict_self
> +    NET_TCP   = 0x3  # BIND_TCP | CONNECT_TCP

We should base these on the names used in the Linux kernel so it's easy
to search for things and compare with example C code in the docs. So,

    NR_landlock_create_ruleset = 444
    NR_landlock_add_rule = 445

    LANDLOCK_ACCESS_NET_BIND_TCP = 0x1
    LANDLOCK_ACCESS_NET_CONNECT_TCP = 0x2

    LANDLOCK_CREATE_RULESET_VERSION = 1

> +
> +    libc = ctypes.CDLL('libc.so.6')
> +
> +    abi = libc.syscall(NR_CREATE, 0, 0, 1)
> +    if abi < 4:
> +        return False

    # Check that landlock is enabled and supports network access
    # restriction (added in ABI version 4)
    abi = libc.syscall(NR_landlock_create_ruleset,
                       0, 0,
                       LANDLOCK_CREATE_RULESET_VERSION)
    if abi < 4:
        logger.debug("System doesn't support disabling network via landlock")
        return False

That's a little more verbose, but much clearer.

> +
> +    attr = struct.pack("QQ", 0, NET_TCP)
> +    buf = ctypes.create_string_buffer(attr)
> +    fd = libc.syscall(NR_CREATE, buf, len(attr), 0)
> +    if fd < 0:
> +        return False

We probably also want a logger.debug() call to log the failure here as
well.

> +
> +    libc.prctl(38, 1, 0, 0, 0)  # PR_SET_NO_NEW_PRIVS

The commit message only describes use of landlock, not no_new_privs. We
need constants for this call as well.

> +    r = libc.syscall(NR_SELF, fd, 0)
> +    os.close(fd)
> +    return r == 0
> +
> +
>  def export_proxies(d):
>      from bb.fetch2 import get_fetcher_environment
>      """ export common proxies variables from datastore to environment """

Thanks,
David Nyström July 17, 2026, 6:11 p.m. UTC | #4
On Mon, 15 Jun 2026, Paul Barker wrote:

> On Fri, 2026-06-12 at 13:38 +0200, David Nyström wrote:
>> Add landlock_restrict_network() which blocks TCP bind/connect using
>> Landlock LSM (ABI v4+, kernel 6.7+). Designed to stack with the
>> existing disable_network() namespace isolation, covering the case
>> where disable_network() is skipped for non-local UIDs.
>>
>> Gracefully returns False on older kernels (ABI < 4).
>>
>> Signed-off-by: David Nyström <david.nystrom@est.tech>
>
> Hi David,
>
> I think adding this is a good idea, but the code needs a few changes to
> ensure it is maintainable.

Thanks for the review, and good comments.
My comments below.

>> ---
>>  lib/bb/utils.py | 26 ++++++++++++++++++++++++++
>>  1 file changed, 26 insertions(+)
>>
>> diff --git a/lib/bb/utils.py b/lib/bb/utils.py
>> index 181082c95..1347c29d0 100644
>> --- a/lib/bb/utils.py
>> +++ b/lib/bb/utils.py
>> @@ -2054,6 +2054,32 @@ def disable_network(uid=None, gid=None):
>>      with open("/proc/self/gid_map", "w") as f:
>>          f.write("%s %s 1" % (gid, gid))
>>
>> +def landlock_restrict_network():
>> +    """Block TCP bind/connect using Landlock LSM (ABI v4+, kernel 6.7+).
>> +    Gracefully skipped on older kernels. Stacks with disable_network()."""
>> +
>> +    NR_CREATE = 444  # landlock_create_ruleset
>> +    NR_SELF   = 446  # landlock_restrict_self
>> +    NET_TCP   = 0x3  # BIND_TCP | CONNECT_TCP
>
> We should base these on the names used in the Linux kernel so it's easy
> to search for things and compare with example C code in the docs. So,
>
>    NR_landlock_create_ruleset = 444
>    NR_landlock_add_rule = 445
>
>    LANDLOCK_ACCESS_NET_BIND_TCP = 0x1
>    LANDLOCK_ACCESS_NET_CONNECT_TCP = 0x2
>
>    LANDLOCK_CREATE_RULESET_VERSION = 1

+1.

>> +
>> +    libc = ctypes.CDLL('libc.so.6')
>> +
>> +    abi = libc.syscall(NR_CREATE, 0, 0, 1)
>> +    if abi < 4:
>> +        return False
>
>    # Check that landlock is enabled and supports network access
>    # restriction (added in ABI version 4)
>    abi = libc.syscall(NR_landlock_create_ruleset,
>                       0, 0,
>                       LANDLOCK_CREATE_RULESET_VERSION)
>    if abi < 4:
>        logger.debug("System doesn't support disabling network via landlock")
>        return False
>
> That's a litte more verbose, but much clearer.

Good point.

>> +
>> +    attr = struct.pack("QQ", 0, NET_TCP)
>> +    buf = ctypes.create_string_buffer(attr)
>> +    fd = libc.syscall(NR_CREATE, buf, len(attr), 0)
>> +    if fd < 0:
>> +        return False
>
> We probably also want a logger.debug() call to log the failure here as
> well.
>
>> +
>> +    libc.prctl(38, 1, 0, 0, 0)  # PR_SET_NO_NEW_PRIVS
>
> The commit message only describes use of landlock, not no_new_privs. We
> need constants for this call as well.

Also a very good point, setuid/gid and setcap:ed binaries will 
silently run without privs, which should be documented also in the commit message.
This is a landlock requirement for unpriv use, and the main source of 
potential sideeffects I would expect from this approach.
Since there is no way do disable it, without setting the 
"network" flag on a failing task, I'd really want to gain more 
confidence about the potential sideeffects of no_new_privs on 
various edge cases before I send a non-RFC patch.
Testing is ongoing with your comments addressed, but I will not be able to 
finish before my vacation starts(tomorrow). If considered urgent,
feel free to use idea only.

>> +    r = libc.syscall(NR_SELF, fd, 0)
>> +    os.close(fd)
>> +    return r == 0
>> +
>> +
>>  def export_proxies(d):
>>      from bb.fetch2 import get_fetcher_environment
>>      """ export common proxies variables from datastore to environment """
>
> Thanks,
>
> -- 
> Paul Barker
>
>
>
diff mbox series

Patch

diff --git a/lib/bb/utils.py b/lib/bb/utils.py
index 181082c95..1347c29d0 100644
--- a/lib/bb/utils.py
+++ b/lib/bb/utils.py
@@ -2054,6 +2054,32 @@  def disable_network(uid=None, gid=None):
     with open("/proc/self/gid_map", "w") as f:
         f.write("%s %s 1" % (gid, gid))
 
+def landlock_restrict_network():
+    """Block TCP bind/connect using Landlock LSM (ABI v4+, kernel 6.7+).
+    Gracefully skipped on older kernels. Stacks with disable_network()."""
+
+    NR_CREATE = 444  # landlock_create_ruleset
+    NR_SELF   = 446  # landlock_restrict_self
+    NET_TCP   = 0x3  # BIND_TCP | CONNECT_TCP
+
+    libc = ctypes.CDLL('libc.so.6')
+
+    abi = libc.syscall(NR_CREATE, 0, 0, 1)
+    if abi < 4:
+        return False
+
+    attr = struct.pack("QQ", 0, NET_TCP)
+    buf = ctypes.create_string_buffer(attr)
+    fd = libc.syscall(NR_CREATE, buf, len(attr), 0)
+    if fd < 0:
+        return False
+
+    libc.prctl(38, 1, 0, 0, 0)  # PR_SET_NO_NEW_PRIVS
+    r = libc.syscall(NR_SELF, fd, 0)
+    os.close(fd)
+    return r == 0
+
+
 def export_proxies(d):
     from bb.fetch2 import get_fetcher_environment
     """ export common proxies variables from datastore to environment """