push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,378 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (config.security) wrapperDir;
wrappers = lib.filterAttrs (name: value: value.enable) config.security.wrappers;
parentWrapperDir = dirOf wrapperDir;
# This is security-sensitive code, and glibc vulns happen from time to time.
# musl is security-focused and generally more minimal, so it's a better choice here.
# The dynamic linker is still a fairly complex piece of code, and the wrappers are
# quite small, so linking it statically is more appropriate.
securityWrapper =
sourceProg:
pkgs.pkgsStatic.callPackage ./wrapper.nix {
inherit sourceProg;
# glibc definitions of insecure environment variables
#
# We extract the single header file we need into its own derivation,
# so that we don't have to pull full glibc sources to build wrappers.
#
# They're taken from pkgs.glibc so that we don't have to keep as close
# an eye on glibc changes. Not every relevant variable is in this header,
# so we maintain a slightly stricter list in wrapper.c itself as well.
unsecvars = lib.overrideDerivation (pkgs.srcOnly pkgs.glibc) (
{ name, ... }:
{
name = "${name}-unsecvars";
installPhase = ''
mkdir $out
cp sysdeps/generic/unsecvars.h $out
'';
}
);
};
fileModeType =
let
# taken from the chmod(1) man page
symbolic = "[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+";
numeric = "[-+=]?[0-7]{0,4}";
mode = "((${symbolic})(,${symbolic})*)|(${numeric})";
in
lib.types.strMatching mode // { description = "file mode string"; };
wrapperType = lib.types.submodule (
{ name, config, ... }:
{
options.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable the wrapper.";
};
options.source = lib.mkOption {
type = lib.types.path;
description = "The absolute path to the program to be wrapped.";
};
options.program = lib.mkOption {
type = with lib.types; nullOr str;
default = name;
description = ''
The name of the wrapper program. Defaults to the attribute name.
'';
};
options.owner = lib.mkOption {
type = lib.types.str;
description = "The owner of the wrapper program.";
};
options.group = lib.mkOption {
type = lib.types.str;
description = "The group of the wrapper program.";
};
options.permissions = lib.mkOption {
type = fileModeType;
default = "u+rx,g+x,o+x";
example = "a+rx";
description = ''
The permissions of the wrapper program. The format is that of a
symbolic or numeric file mode understood by {command}`chmod`.
'';
};
options.capabilities = lib.mkOption {
type = lib.types.commas;
default = "";
description = ''
A comma-separated list of capability clauses to be given to the
wrapper program. The format for capability clauses is described in the
TEXTUAL REPRESENTATION section of the {manpage}`cap_from_text(3)`
manual page. For a list of capabilities supported by the system, check
the {manpage}`capabilities(7)` manual page.
::: {.note}
`cap_setpcap`, which is required for the wrapper
program to be able to raise caps into the Ambient set is NOT raised
to the Ambient set so that the real program cannot modify its own
capabilities!! This may be too restrictive for cases in which the
real program needs cap_setpcap but it at least leans on the side
security paranoid vs. too relaxed.
:::
'';
};
options.setuid = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to add the setuid bit the wrapper program.";
};
options.setgid = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to add the setgid bit the wrapper program.";
};
}
);
###### Activation script for the setcap wrappers
mkSetcapProgram =
{
program,
capabilities,
source,
owner,
group,
permissions,
...
}:
''
cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
# Prevent races
chmod 0000 "$wrapperDir/${program}"
chown ${owner}:${group} "$wrapperDir/${program}"
# Set desired capabilities on the file plus cap_setpcap so
# the wrapper program can elevate the capabilities set on
# its file into the Ambient set.
${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" "$wrapperDir/${program}"
# Set the executable bit
chmod ${permissions} "$wrapperDir/${program}"
'';
###### Activation script for the setuid wrappers
mkSetuidProgram =
{
program,
source,
owner,
group,
setuid,
setgid,
permissions,
...
}:
''
cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
# Prevent races
chmod 0000 "$wrapperDir/${program}"
chown ${owner}:${group} "$wrapperDir/${program}"
chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
'';
mkWrappedPrograms = builtins.map (
opts: if opts.capabilities != "" then mkSetcapProgram opts else mkSetuidProgram opts
) (lib.attrValues wrappers);
in
{
imports = [
(lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
(lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
];
###### interface
options = {
security.enableWrappers = lib.mkEnableOption "SUID/SGID wrappers" // {
default = true;
};
security.wrappers = lib.mkOption {
type = lib.types.attrsOf wrapperType;
default = { };
example = lib.literalExpression ''
{
# a setuid root program
doas =
{ setuid = true;
owner = "root";
group = "root";
source = "''${pkgs.doas}/bin/doas";
};
# a setgid program
locate =
{ setgid = true;
owner = "root";
group = "mlocate";
source = "''${pkgs.locate}/bin/locate";
};
# a program with the CAP_NET_RAW capability
ping =
{ owner = "root";
group = "root";
capabilities = "cap_net_raw+ep";
source = "''${pkgs.iputils.out}/bin/ping";
};
}
'';
description = ''
This option effectively allows adding setuid/setgid bits, capabilities,
changing file ownership and permissions of a program without directly
modifying it. This works by creating a wrapper program in a directory
(not configurable), which is then added to the shell `PATH`.
'';
};
security.wrapperDirSize = lib.mkOption {
default = "50%";
example = "10G";
type = lib.types.str;
description = ''
Size limit for the /run/wrappers tmpfs. Look at {manpage}`mount(8)`, tmpfs size option,
for the accepted syntax. WARNING: don't set to less than 64MB.
'';
};
security.wrapperDir = lib.mkOption {
type = lib.types.path;
default = "/run/wrappers/bin";
internal = true;
description = ''
This option defines the path to the wrapper programs. It
should not be overridden.
'';
};
};
###### implementation
config = lib.mkIf config.security.enableWrappers {
assertions = lib.mapAttrsToList (name: opts: {
assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
message = ''
The security.wrappers.${name} wrapper is not valid:
setuid/setgid and capabilities are mutually exclusive.
'';
}) wrappers;
security.wrappers =
let
mkSetuidRoot = source: {
setuid = true;
owner = "root";
group = "root";
inherit source;
};
in
{
# These are mount related wrappers that require the +s permission.
mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
};
# Make sure our wrapperDir exports to the PATH env variable when
# initializing the shell
environment.extraInit = ''
# Wrappers override other bin directories.
export PATH="${wrapperDir}:$PATH"
'';
security.apparmor.includes = lib.mapAttrs' (
wrapName: wrap:
lib.nameValuePair "nixos/security.wrappers/${wrapName}" ''
include "${
pkgs.apparmorRulesFromClosure { name = "security.wrappers.${wrapName}"; } [
(securityWrapper wrap.source)
]
}"
mrpx ${wrap.source},
''
) wrappers;
systemd.mounts = [
{
where = parentWrapperDir;
what = "tmpfs";
type = "tmpfs";
options = lib.concatStringsSep "," [
"nodev"
"mode=755"
"size=${config.security.wrapperDirSize}"
];
}
];
systemd.services.suid-sgid-wrappers = {
description = "Create SUID/SGID Wrappers";
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
after = [ "systemd-sysusers.service" ];
unitConfig.DefaultDependencies = false;
unitConfig.RequiresMountsFor = [
"/nix/store"
"/run/wrappers"
];
serviceConfig.RestrictSUIDSGID = false;
serviceConfig.Type = "oneshot";
script = ''
chmod 755 "${parentWrapperDir}"
# We want to place the tmpdirs for the wrappers to the parent dir.
wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
chmod a+rx "$wrapperDir"
${lib.concatStringsSep "\n" mkWrappedPrograms}
if [ -L ${wrapperDir} ]; then
# Atomically replace the symlink
# See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
old=$(readlink -f ${wrapperDir})
if [ -e "${wrapperDir}-tmp" ]; then
rm --force --recursive "${wrapperDir}-tmp"
fi
ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
rm --force --recursive "$old"
else
# For initial setup
ln --symbolic "$wrapperDir" "${wrapperDir}"
fi
'';
};
###### wrappers consistency checks
system.checks = lib.singleton (
pkgs.runCommand "ensure-all-wrappers-paths-exist"
{
preferLocalBuild = true;
}
''
# make sure we produce output
mkdir -p $out
echo -n "Checking that Nix store paths of all wrapped programs exist... "
declare -A wrappers
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: "wrappers['${n}']='${v.source}'") wrappers)}
for name in "''${!wrappers[@]}"; do
path="''${wrappers[$name]}"
if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
test -t 1 && echo -ne '\033[1;31m'
echo "FAIL"
echo "The path $path does not exist!"
echo 'Please, check the value of `security.wrappers."'$name'".source`.'
test -t 1 && echo -ne '\033[0m'
exit 1
fi
done
echo "OK"
''
);
};
}

View File

@@ -0,0 +1,221 @@
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdnoreturn.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <linux/capability.h>
#include <sys/prctl.h>
#include <limits.h>
#include <stdint.h>
#include <syscall.h>
#include <byteswap.h>
// imported from glibc
#include "unsecvars.h"
#ifndef SOURCE_PROG
#error SOURCE_PROG should be defined via preprocessor commandline
#endif
// aborts when false, printing the failed expression
#define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr))
extern char **environ;
// Wrapper debug variable name
static char *wrapper_debug = "WRAPPER_DEBUG";
#define CAP_SETPCAP 8
#if __BYTE_ORDER == __BIG_ENDIAN
#define LE32_TO_H(x) bswap_32(x)
#else
#define LE32_TO_H(x) (x)
#endif
static noreturn void assert_failure(const char *assertion) {
fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion);
fflush(stderr);
abort();
}
int get_last_cap(unsigned *last_cap) {
FILE* file = fopen("/proc/sys/kernel/cap_last_cap", "r");
if (file == NULL) {
int saved_errno = errno;
fprintf(stderr, "failed to open /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
return -saved_errno;
}
int res = fscanf(file, "%u", last_cap);
if (res == EOF) {
int saved_errno = errno;
fprintf(stderr, "could not read number from /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
return -saved_errno;
}
fclose(file);
return 0;
}
// Given the path to this program, fetch its configured capability set
// (as set by `setcap ... /path/to/file`) and raise those capabilities
// into the Ambient set.
static int make_caps_ambient(const char *self_path) {
struct vfs_ns_cap_data data = {};
int r = getxattr(self_path, "security.capability", &data, sizeof(data));
if (r < 0) {
if (errno == ENODATA) {
// no capabilities set
return 0;
}
fprintf(stderr, "cannot get capabilities for %s: %s", self_path, strerror(errno));
return 1;
}
size_t size;
uint32_t version = LE32_TO_H(data.magic_etc) & VFS_CAP_REVISION_MASK;
switch (version) {
case VFS_CAP_REVISION_1:
size = VFS_CAP_U32_1;
break;
case VFS_CAP_REVISION_2:
case VFS_CAP_REVISION_3:
size = VFS_CAP_U32_3;
break;
default:
fprintf(stderr, "BUG! Unsupported capability version 0x%x on %s. Report to NixOS bugtracker\n", version, self_path);
return 1;
}
const struct __user_cap_header_struct header = {
.version = _LINUX_CAPABILITY_VERSION_3,
.pid = getpid(),
};
struct __user_cap_data_struct user_data[2] = {};
for (size_t i = 0; i < size; i++) {
// merge inheritable & permitted into one
user_data[i].permitted = user_data[i].inheritable =
LE32_TO_H(data.data[i].inheritable) | LE32_TO_H(data.data[i].permitted);
}
if (syscall(SYS_capset, &header, &user_data) < 0) {
fprintf(stderr, "failed to inherit capabilities: %s", strerror(errno));
return 1;
}
unsigned last_cap;
r = get_last_cap(&last_cap);
if (r < 0) {
return 1;
}
uint64_t set = user_data[0].permitted | (uint64_t)user_data[1].permitted << 32;
for (unsigned cap = 0; cap < last_cap; cap++) {
if (!(set & (1ULL << cap))) {
continue;
}
// Check for the cap_setpcap capability, we set this on the
// wrapper so it can elevate the capabilities to the Ambient
// set but we do not want to propagate it down into the
// wrapped program.
//
// TODO: what happens if that's the behavior you want
// though???? I'm preferring a strict vs. loose policy here.
if (cap == CAP_SETPCAP) {
if(getenv(wrapper_debug)) {
fprintf(stderr, "cap_setpcap in set, skipping it\n");
}
continue;
}
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, (unsigned long) cap, 0, 0)) {
fprintf(stderr, "cannot raise the capability %d into the ambient set: %s\n", cap, strerror(errno));
return 1;
}
if (getenv(wrapper_debug)) {
fprintf(stderr, "raised %d into the ambient capability set\n", cap);
}
}
return 0;
}
// These are environment variable aliases for glibc tunables.
// This list shouldn't grow further, since this is a legacy mechanism.
// Any future tunables are expected to only be accessible through GLIBC_TUNABLES.
//
// They are not included in the glibc-provided UNSECURE_ENVVARS list,
// since any SUID executable ignores them. This wrapper also serves
// executables that are merely granted ambient capabilities, rather than
// being SUID, and hence don't run in secure mode. We'd like them to
// defend those in depth as well, so we clear these explicitly.
//
// Except for MALLOC_CHECK_ (which is marked SXID_ERASE), these are all
// marked SXID_IGNORE (ignored in secure mode), so even the glibc version
// of this wrapper would leave them intact.
#define UNSECURE_ENVVARS_TUNABLES \
"MALLOC_CHECK_\0" \
"MALLOC_TOP_PAD_\0" \
"MALLOC_PERTURB_\0" \
"MALLOC_MMAP_THRESHOLD_\0" \
"MALLOC_TRIM_THRESHOLD_\0" \
"MALLOC_MMAP_MAX_\0" \
"MALLOC_ARENA_MAX\0" \
"MALLOC_ARENA_TEST\0"
int main(int argc, char **argv) {
int debug = getenv(wrapper_debug) != NULL;
// Drop insecure environment variables explicitly
//
// glibc does this automatically in SUID binaries, but we'd like to cover this:
//
// a) before it gets to glibc
// b) in binaries that are only granted ambient capabilities by the wrapper,
// but don't run with an altered effective UID/GID, nor directly gain
// capabilities themselves, and thus don't run in secure mode.
//
// We're using musl, which doesn't drop environment variables in secure mode,
// and we'd also like glibc-specific variables to be covered.
//
// If we don't explicitly unset them, it's quite easy to just set LD_PRELOAD,
// have it passed through to the wrapped program, and gain privileges.
for (char *unsec = UNSECURE_ENVVARS_TUNABLES UNSECURE_ENVVARS; *unsec; unsec = strchr(unsec, 0) + 1) {
if (debug) {
fprintf(stderr, "unsetting %s\n", unsec);
}
unsetenv(unsec);
}
// Read the capabilities set on the wrapper and raise them in to
// the ambient set so the program we're wrapping receives the
// capabilities too!
if (make_caps_ambient("/proc/self/exe") != 0) {
return 1;
}
char *replacement_argv[2] = {SOURCE_PROG, NULL};
char *old_argv0;
// Replace untrusted or missing argv[0] by the wrapped program path.
// This mitigates vulnerabilities caused by incorrect handling in privileged code.
if (argv[0]) {
old_argv0 = argv[0];
argv[0] = SOURCE_PROG;
} else {
old_argv0 = "«nullptr»";
argv = replacement_argv;
}
execve(SOURCE_PROG, argv, environ);
fprintf(stderr, "%s: cannot run `%s': %s\n",
old_argv0, SOURCE_PROG, strerror(errno));
return 1;
}

View File

@@ -0,0 +1,35 @@
{
stdenv,
unsecvars,
linuxHeaders,
sourceProg,
debug ? false,
}:
# For testing:
# $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { sourceProg = "${pkgs.hello}/bin/hello"; debug = true; }'
stdenv.mkDerivation {
name = "security-wrapper-${baseNameOf sourceProg}";
buildInputs = [ linuxHeaders ];
dontUnpack = true;
CFLAGS = [
''-DSOURCE_PROG="${sourceProg}"''
]
++ (
if debug then
[
"-Werror"
"-Og"
"-g"
]
else
[
"-Wall"
"-O2"
]
);
dontStrip = debug;
installPhase = ''
mkdir -p $out/bin
$CC $CFLAGS ${./wrapper.c} -I${unsecvars} -o $out/bin/security-wrapper
'';
}