Skip to content

Commit 44017ca

Browse files
committed
libstore: Use landlock with LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET for new enough kernels
This partially fixes the issue with cooperating processes being able to communicate via abstract sockets. The fix is partial, because processes outside the landlock domain of the sandboxed process can still connect to a socket created by the FOD. There's no equivalent way of restricting inbound connections. This closes the gap when there's no cooperating process on the host (i.e. 2 separate FODs). >= 6.12 kernel is widespread enough (NixOS 25.11 ships it by default) that we have no reason not to apply this hardening, even though it's incomplete. ca-fd-leak test exercises this exact code path and now the smuggling process fails with (on new enough kernels that have landlock support enabled): vm-test-run-ca-fd-leak> machine # sandbox setup: applied landlock sandboxing vm-test-run-ca-fd-leak> machine # building '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv'... vm-test-run-ca-fd-leak> machine # building derivation '/nix/store/s7brgi6pdr5f3n8yqlgmdlz8blb89njc-smuggled.drv': woken up vm-test-run-ca-fd-leak> machine # connect: Operation not permitted vm-test-run-ca-fd-leak> machine # sendmsg: Socket not connected
1 parent a760af8 commit 44017ca

3 files changed

Lines changed: 112 additions & 2 deletions

File tree

src/libstore/meson.build

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ foreach funcspec : check_funcs
7979
configdata_priv.set(define_name, define_value)
8080
endforeach
8181

82+
if host_machine.system() == 'linux'
83+
has_landlock = cxx.has_header('linux/landlock.h')
84+
configdata_priv.set('HAVE_LANDLOCK', has_landlock.to_int())
85+
endif
86+
8287
has_acl_support = cxx.has_header('sys/xattr.h') \
8388
and cxx.has_function('llistxattr') \
8489
and cxx.has_function('lremovexattr')

src/libstore/unix/build/linux-derivation-builder.cc

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#ifdef __linux__
22

3+
# include "store-config-private.hh"
4+
35
# include "nix/store/globals.hh"
46
# include "nix/store/personality.hh"
57
# include "nix/store/filetransfer.hh"
@@ -11,6 +13,8 @@
1113

1214
# include <algorithm>
1315
# include <string_view>
16+
# include <cstdint>
17+
1418
# include <sys/ioctl.h>
1519
# include <net/if.h>
1620
# include <netinet/ip.h>
@@ -19,11 +23,16 @@
1923
# include <sys/param.h>
2024
# include <sys/mount.h>
2125
# include <sys/syscall.h>
26+
# include <sys/prctl.h>
2227

2328
# if HAVE_SECCOMP
2429
# include <seccomp.h>
2530
# endif
2631

32+
# if HAVE_LANDLOCK
33+
# include <linux/landlock.h>
34+
# endif
35+
2736
# define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old))
2837

2938
namespace nix {
@@ -129,6 +138,77 @@ static void setupSeccomp(const LocalSettings & localSettings)
129138
# endif
130139
}
131140

141+
# if HAVE_LANDLOCK && defined(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET)
142+
143+
# define DO_LANDLOCK 1
144+
145+
/* We are using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET on best-effort basis. There are no glibc wrappers for now. */
146+
147+
static int landlockCreateRuleset(const ::landlock_ruleset_attr * attr, std::size_t size, std::uint32_t flags)
148+
{
149+
return ::syscall(__NR_landlock_create_ruleset, attr, size, flags);
150+
}
151+
152+
static int landlockRestrictSelf(Descriptor rulesetFd, std::uint32_t flags)
153+
{
154+
return ::syscall(__NR_landlock_restrict_self, rulesetFd, flags);
155+
}
156+
157+
static int getLandlockAbiVersion()
158+
{
159+
int abiVersion = landlockCreateRuleset(nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION);
160+
return abiVersion;
161+
}
162+
163+
static void setupLandlock()
164+
{
165+
bool landlockSupportsScopeAbstractUnixSocket = []() {
166+
int abiVersion = getLandlockAbiVersion();
167+
if (abiVersion >= 6)
168+
/* All good, we can use LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET. See
169+
https://docs.kernel.org/userspace-api/landlock.html#abstract-unix-socket-abi-6 */
170+
return true;
171+
172+
if (abiVersion == -1) {
173+
debug("landlock is not available");
174+
return false;
175+
}
176+
177+
debug("landlock version %d does not support LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", abiVersion);
178+
return false;
179+
}();
180+
181+
/* Bail out early if landlock is not enabled or LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET wouldn't work.
182+
TODO: Consider adding more landlock rules for filesystem access as defense-in-depth on top. */
183+
if (!landlockSupportsScopeAbstractUnixSocket)
184+
return;
185+
186+
::landlock_ruleset_attr attr = {
187+
/* This prevents multiple FODs from communicating with each other
188+
via abstract sockets. Note that cooperating processes outside the
189+
sandbox can still connect to an abstract socket created by the FOD. To
190+
mitigate that issue entirely we'd still need network namespaces. */
191+
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
192+
};
193+
194+
/* This better not fail - if the kernel reports a new enough ABI version we
195+
should treat any errors as fatal from now on. */
196+
AutoCloseFD rulesetFd = landlockCreateRuleset(&attr, sizeof(attr), 0);
197+
if (!rulesetFd)
198+
throw SysError("failed to create a landlock ruleset");
199+
200+
if (landlockRestrictSelf(rulesetFd.get(), 0) == -1)
201+
throw SysError("failed to apply landlock");
202+
203+
debug("applied landlock sandboxing");
204+
}
205+
206+
# else
207+
208+
# define DO_LANDLOCK 0
209+
210+
# endif
211+
132212
static void doBind(const std::filesystem::path & source, const std::filesystem::path & target, bool optional = false)
133213
{
134214
debug("bind mounting %1% to %2%", PathFmt(source), PathFmt(target));
@@ -169,8 +249,27 @@ struct LinuxDerivationBuilder : virtual DerivationBuilderImpl
169249
{
170250
auto & localSettings = store.config->getLocalSettings();
171251

252+
/* Set the NO_NEW_PRIVS before doing seccomp/landlock setup.
253+
landlock_restrict_self requires either NO_NEW_PRIVS or CAP_SYS_ADMIN.
254+
With user namespaces we do get CAP_SYS_ADMIN. */
255+
if (!localSettings.allowNewPrivileges)
256+
if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1)
257+
throw SysError("failed to set PR_SET_NO_NEW_PRIVS");
258+
172259
setupSeccomp(localSettings);
173260

261+
# if DO_LANDLOCK
262+
try {
263+
setupLandlock();
264+
} catch (SysError & e) {
265+
if (e.errNo != EPERM)
266+
throw;
267+
/* If allowNewPrivileges is true and we don't have CAP_SYS_ADMIN
268+
this code path might be hit. */
269+
warn("setting up landlock: %s", e.message());
270+
}
271+
# endif
272+
174273
linux::setPersonality({
175274
.system = drv.platform,
176275
.impersonateLinux26 = localSettings.impersonateLinux26,
@@ -765,4 +864,6 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu
765864

766865
} // namespace nix
767866

867+
# undef DO_LANDLOCK
868+
768869
#endif

tests/nixos/ca-fd-leak/default.nix

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ in
7878
7979
# Build the smuggled derivation.
8080
# This will connect to the smuggler server and send it the file descriptor
81-
machine.succeed(r"""
81+
sender_output = machine.succeed(r"""
8282
nix-build -E '
8383
builtins.derivation {
8484
name = "smuggled";
@@ -89,9 +89,13 @@ in
8989
outputHash = builtins.hashString "sha256" "hello, world\n";
9090
builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
9191
args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ];
92-
}'
92+
}' 2>&1
9393
""".strip())
9494
95+
# Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET prevents a sandboxed process
96+
# from connecting to an abstract socket created in an unrelated landlock domain.
97+
# There's no such flag for preventing inbound connections.
98+
assert "connect: Operation not permitted" in sender_output
9599
96100
# Tell the smuggler server that we're done
97101
machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")

0 commit comments

Comments
 (0)