From b6bbf2ed9b94e072cc17001d94fbd5e3a9e87345 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Mon, 26 Feb 2024 20:25:40 -0800 Subject: [PATCH 01/17] Potential first version of security.wrappers --- modules/module-list.nix | 1 + modules/security/wrappers/default.nix | 186 ++++++++++++++++++++++++++ modules/system/activation-scripts.nix | 1 + 3 files changed, 188 insertions(+) create mode 100644 modules/security/wrappers/default.nix diff --git a/modules/module-list.nix b/modules/module-list.nix index aa190c7d..cd3b9067 100644 --- a/modules/module-list.nix +++ b/modules/module-list.nix @@ -8,6 +8,7 @@ ./security/pki ./security/sandbox ./security/sudo.nix + ./security/wrappers ./system ./system/base.nix ./system/checks.nix diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix new file mode 100644 index 00000000..3add4de2 --- /dev/null +++ b/modules/security/wrappers/default.nix @@ -0,0 +1,186 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.security; + + parentWrapperDir = dirOf cfg.wrapperDir; + + wrapperType = lib.types.submodule ({ name, config, ... }: { + options = with lib; { + source = mkOption { + type = types.path; + description = mdDoc "The absolute path to the program to be wrapped."; + }; + program = mkOption { + type = with types; nullOr str; + default = name; + description = mdDoc "The name of the wrapper program. Defaults to the attribute name."; + }; + owner = mkOption { + type = types.str; + description = mdDoc "The owner of the wrapper program."; + }; + group = mkOption { + type = types.str; + description = mdDoc "The group of the wrapper program."; + }; + permissions = mkOption { + type = types.str; + default = "u+rx,g+x,o+x"; + description = mdDoc "The permissions to set on the wrapper."; + }; + setuid = mkOption { + type = types.bool; + default = false; + description = mdDoc "Whether to add the setuid bit to the wrapper program."; + }; + setgid = mkOption { + type = types.bool; + default = false; + description = mdDoc "Whether to add the setgid bit to the wrapper program."; + }; + codesign = mkOption { + type = types.bool; + default = false; + description = mdDoc "Whether to codesign the wrapper program."; + }; + }; + }); + + mkWrappedPrograms = + builtins.map + (opts: mkWrapper opts) + (builtins.attrValues cfg.wrappers); + + securityWrapper = sourceProg: pkgs.writers.writeBashBin "security-wrapper" '' + exec ${sourceProg} "$@" + ''; + + mkWrapper = + { program + , source + , owner + , group + , permissions + , setuid + , setgid + , codesign + , ... + }: + let + codesigned = if codesign + then '' + # codesign ${source} to "$wrapperDir/${program}" INSTEAD OF the next line + cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" + '' + else '' + cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" + ''; + in + '' + ${codesigned} + + # 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}" + ''; +in +{ + # probably not necessary since these options never existed in nix-darwin? + imports = [ + (lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead") + (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead") + ]; + + ###### interface + options.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 = lib.mdDoc '' + 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 under the + {option}`security.wrapperDir` directory, which is then added to + the shell `PATH`. + ''; + }; + wrapperDir = lib.mkOption { + type = lib.types.path; + default = "/run/wrappers/bin"; + internal = true; + description = lib.mdDoc '' + This option defines the path to the wrapper programs. It + should not be overridden. + ''; + }; + codesignIdentity = lib.mkOption { + type = lib.types.str; + default = "-"; + description = lib.mdDoc "Identity to use for codesigning."; + }; + }; + + ###### implementation + config = { + environment.extraInit = '' + # Wrappers override other bin directories. + export PATH="${cfg.wrapperDir}:$PATH" + ''; + + system.activationScripts.wrappers.text = '' + echo "setting up wrappers..." >&2 + if ! test -e /run/wrappers; then mkdir /run/wrappers; fi + + # We want to place the tmpdirs for the wrappers to the parent dir. + wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX) + chmod a+rx $wrapperDir + + ${builtins.concatStringsSep "\n" mkWrappedPrograms} + + if test -L ${cfg.wrapperDir}; then + # Atomically replace the symlink + # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/ + old=$(readlink -f ${cfg.wrapperDir}) + ln --symbolic --force --no-dereference $wrapperDir ${cfg.wrapperDir}-tmp + mv --no-target-directory ${cfg.wrapperDir}-tmp ${cfg.wrapperDir} + rm --force --recursive $old + else + # For initial setup + ln --symbolic $wrapperDir ${cfg.wrapperDir} + fi + ''; + }; +} diff --git a/modules/system/activation-scripts.nix b/modules/system/activation-scripts.nix index 5f8916cc..7b9f52aa 100644 --- a/modules/system/activation-scripts.nix +++ b/modules/system/activation-scripts.nix @@ -71,6 +71,7 @@ in ${cfg.activationScripts.keyboard.text} ${cfg.activationScripts.fonts.text} ${cfg.activationScripts.nvram.text} + ${cfg.activationScripts.wrappers.text} ${cfg.activationScripts.postActivation.text} From 30780a056e45381b0896f09534f05d615ff86e56 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Mon, 26 Feb 2024 20:42:45 -0800 Subject: [PATCH 02/17] Actually wrap with C program --- modules/security/wrappers/default.nix | 32 +++++++- modules/security/wrappers/wrapper.c | 102 ++++++++++++++++++++++++++ modules/security/wrappers/wrapper.nix | 20 +++++ 3 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 modules/security/wrappers/wrapper.c create mode 100644 modules/security/wrappers/wrapper.nix diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index 3add4de2..b923105c 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -52,9 +52,35 @@ let (opts: mkWrapper opts) (builtins.attrValues cfg.wrappers); - securityWrapper = sourceProg: pkgs.writers.writeBashBin "security-wrapper" '' - exec ${sourceProg} "$@" - ''; + # securityWrapper = sourceProg: pkgs.writers.writeBashBin "security-wrapper" '' + # exec ${sourceProg} "$@" + # ''; + # securityWrapper = sourceProg: pkgs.runCommand "security-wrapper" {} '' + # mkdir -p $out/bin + # cp ${sourceProg} $out/bin/security-wrapper + # # ln -s ${sourceProg} $out/bin/security-wrapper + # ''; + + 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 + ''; + }); + }; mkWrapper = { program diff --git a/modules/security/wrappers/wrapper.c b/modules/security/wrappers/wrapper.c new file mode 100644 index 00000000..2075b948 --- /dev/null +++ b/modules/security/wrappers/wrapper.c @@ -0,0 +1,102 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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"; + +static noreturn void assert_failure(const char *assertion) { + fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion); + fflush(stderr); + abort(); +} + +// 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) { + ASSERT(argc >= 1); + + // argv[0] goes into a lot of places, to a far greater degree than other elements + // of argv. glibc has had buffer overflows relating to argv[0], eg CVE-2023-6246. + // Since we expect the wrappers to be invoked from either $PATH or /run/wrappers/bin, + // there should be no reason to pass any particularly large values here, so we can + // be strict for strictness' sake. + ASSERT(strlen(argv[0]) < 512); + + 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); + } + + execve(SOURCE_PROG, argv, environ); + + fprintf(stderr, "%s: cannot run `%s': %s\n", + argv[0], SOURCE_PROG, strerror(errno)); + + return 1; +} diff --git a/modules/security/wrappers/wrapper.nix b/modules/security/wrappers/wrapper.nix new file mode 100644 index 00000000..ca4b27bf --- /dev/null +++ b/modules/security/wrappers/wrapper.nix @@ -0,0 +1,20 @@ +{ stdenv, unsecvars, linuxHeaders, sourceProg, debug ? false }: +# For testing: +# $ nix-build -E 'with import {}; 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 + ''; +} From 69006ab74cf78a2ae1876d86ac4de3704e278978 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:20:32 -0800 Subject: [PATCH 03/17] Trim C wrapper to work on Darwin --- modules/security/wrappers/default.nix | 16 +++++------ modules/security/wrappers/wrapper.c | 38 +++++++++++++-------------- modules/security/wrappers/wrapper.nix | 5 ++-- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index b923105c..9fc3b74d 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -72,14 +72,14 @@ let # 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 - ''; - }); + # unsecvars = lib.overrideDerivation (pkgs.srcOnly pkgs.glibc) + # ({ name, ... }: { + # name = "${name}-unsecvars"; + # installPhase = '' + # mkdir $out + # cp sysdeps/generic/unsecvars.h $out + # ''; + # }); }; mkWrapper = diff --git a/modules/security/wrappers/wrapper.c b/modules/security/wrappers/wrapper.c index 2075b948..5700cf10 100644 --- a/modules/security/wrappers/wrapper.c +++ b/modules/security/wrappers/wrapper.c @@ -3,21 +3,21 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include // imported from glibc -#include "unsecvars.h" +// #include "unsecvars.h" #ifndef SOURCE_PROG #error SOURCE_PROG should be defined via preprocessor commandline @@ -86,12 +86,12 @@ int main(int argc, char **argv) { // // 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); - } + // for (char *unsec = UNSECURE_ENVVARS_TUNABLES UNSECURE_ENVVARS; *unsec; unsec = strchr(unsec, 0) + 1) { + // if (debug) { + // fprintf(stderr, "unsetting %s\n", unsec); + // } + // unsetenv(unsec); + // } execve(SOURCE_PROG, argv, environ); diff --git a/modules/security/wrappers/wrapper.nix b/modules/security/wrappers/wrapper.nix index ca4b27bf..8c795e74 100644 --- a/modules/security/wrappers/wrapper.nix +++ b/modules/security/wrappers/wrapper.nix @@ -1,9 +1,8 @@ -{ stdenv, unsecvars, linuxHeaders, sourceProg, debug ? false }: +{ stdenv, sourceProg, debug ? false }: # For testing: # $ nix-build -E 'with import {}; 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}"'' @@ -15,6 +14,6 @@ stdenv.mkDerivation { dontStrip = debug; installPhase = '' mkdir -p $out/bin - $CC $CFLAGS ${./wrapper.c} -I${unsecvars} -o $out/bin/security-wrapper + $CC $CFLAGS ${./wrapper.c} -o $out/bin/security-wrapper ''; } From 6d5c8de9a8e07d4bc7a91576b5fcdfbfb7c5aea9 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:23:27 -0800 Subject: [PATCH 04/17] include libs --- modules/security/wrappers/wrapper.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/security/wrappers/wrapper.c b/modules/security/wrappers/wrapper.c index 5700cf10..af06e85a 100644 --- a/modules/security/wrappers/wrapper.c +++ b/modules/security/wrappers/wrapper.c @@ -3,7 +3,8 @@ #include #include #include -// #include +#include +#include // #include // #include // #include From 1be75fe7e857dd83aea79685fcc56f47c6d28bb1 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:30:50 -0800 Subject: [PATCH 05/17] unset env vars --- modules/security/wrappers/wrapper.c | 37 ++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/modules/security/wrappers/wrapper.c b/modules/security/wrappers/wrapper.c index af06e85a..b6e4a8b3 100644 --- a/modules/security/wrappers/wrapper.c +++ b/modules/security/wrappers/wrapper.c @@ -61,6 +61,31 @@ static noreturn void assert_failure(const char *assertion) { "MALLOC_ARENA_MAX\0" \ "MALLOC_ARENA_TEST\0" +#define UNSECURE_ENVVARS \ + "GCONV_PATH\0" \ + "GETCONF_DIR\0" \ + "HOSTALIASES\0" \ + "LD_AUDIT\0" \ + "LD_DEBUG\0" \ + "LD_DEBUG_OUTPUT\0" \ + "LD_DYNAMIC_WEAK\0" \ + "LD_HWCAP_MASK\0" \ + "LD_LIBRARY_PATH\0" \ + "LD_ORIGIN_PATH\0" \ + "LD_PRELOAD\0" \ + "LD_PROFILE\0" \ + "LD_SHOW_AUXV\0" \ + "LD_USE_LOAD_BIAS\0" \ + "LOCALDOMAIN\0" \ + "LOCPATH\0" \ + "MALLOC_TRACE\0" \ + "NIS_PATH\0" \ + "NLSPATH\0" \ + "RESOLV_HOST_CONF\0" \ + "RES_OPTIONS\0" \ + "TMPDIR\0" \ + // GLIBC_TUNABLES_ENVVAR \ + int main(int argc, char **argv) { ASSERT(argc >= 1); @@ -87,12 +112,12 @@ int main(int argc, char **argv) { // // 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); - // } + for (char *unsec = UNSECURE_ENVVARS_TUNABLES UNSECURE_ENVVARS; *unsec; unsec = strchr(unsec, 0) + 1) { + if (debug) { + fprintf(stderr, "unsetting %s\n", unsec); + } + unsetenv(unsec); + } execve(SOURCE_PROG, argv, environ); From 8f14fefb2df09c3172b36caa98823e6225a5cd9e Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:14:11 -0800 Subject: [PATCH 06/17] Update examples --- modules/security/wrappers/default.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index 9fc3b74d..ec0640a2 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -132,7 +132,7 @@ in doas = { setuid = true; owner = "root"; - group = "root"; + group = "wheel"; source = "''${pkgs.doas}/bin/doas"; }; @@ -146,11 +146,11 @@ in }; - # a program with the CAP_NET_RAW capability + # a codesigned program ping = { owner = "root"; - group = "root"; - capabilities = "cap_net_raw+ep"; + group = "wheel"; + codesign = true; source = "''${pkgs.iputils.out}/bin/ping"; }; } From f15674cb3e1c1a6eb1462f90e03c8844254ebd1b Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:05:42 -0800 Subject: [PATCH 07/17] Remove codesigning support; leave stubs in place --- modules/security/wrappers/default.nix | 49 ++++++++------------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index ec0640a2..a80a4aca 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -39,11 +39,11 @@ let default = false; description = mdDoc "Whether to add the setgid bit to the wrapper program."; }; - codesign = mkOption { - type = types.bool; - default = false; - description = mdDoc "Whether to codesign the wrapper program."; - }; + # codesign = mkOption { + # type = types.bool; + # default = false; + # description = mdDoc "Whether to codesign the wrapper program."; + # }; }; }); @@ -52,15 +52,6 @@ let (opts: mkWrapper opts) (builtins.attrValues cfg.wrappers); - # securityWrapper = sourceProg: pkgs.writers.writeBashBin "security-wrapper" '' - # exec ${sourceProg} "$@" - # ''; - # securityWrapper = sourceProg: pkgs.runCommand "security-wrapper" {} '' - # mkdir -p $out/bin - # cp ${sourceProg} $out/bin/security-wrapper - # # ln -s ${sourceProg} $out/bin/security-wrapper - # ''; - securityWrapper = sourceProg : pkgs.pkgsStatic.callPackage ./wrapper.nix { inherit sourceProg; @@ -90,7 +81,7 @@ let , permissions , setuid , setgid - , codesign + , codesign ? false , ... }: let @@ -144,23 +135,13 @@ in group = "mlocate"; source = "''${pkgs.locate}/bin/locate"; }; - - - # a codesigned program - ping = - { owner = "root"; - group = "wheel"; - codesign = true; - source = "''${pkgs.iputils.out}/bin/ping"; - }; } ''; description = lib.mdDoc '' - 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 under the - {option}`security.wrapperDir` directory, which is then added to - the shell `PATH`. + This option effectively allows adding setuid/setgid bits and/or changing + file ownership and permissions without directly modifying it. This works + by creating a wrapper program under the {option}`security.wrapperDir` + directory, which is then added to the shell `PATH`. ''; }; wrapperDir = lib.mkOption { @@ -172,11 +153,11 @@ in should not be overridden. ''; }; - codesignIdentity = lib.mkOption { - type = lib.types.str; - default = "-"; - description = lib.mdDoc "Identity to use for codesigning."; - }; + # codesignIdentity = lib.mkOption { + # type = lib.types.str; + # default = "-"; + # description = lib.mdDoc "Identity to use for codesigning."; + # }; }; ###### implementation From 2702ccab6bc79e271c8f613f5b251407d111ddd1 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:40:52 -0700 Subject: [PATCH 08/17] Create wrapper directory at boot Without this change, the wrapper directory would be removed on reboot and would not be recreated until the next time the activation script ran --- modules/services/activate-system/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/services/activate-system/default.nix b/modules/services/activate-system/default.nix index 6a982fe8..b70484f3 100644 --- a/modules/services/activate-system/default.nix +++ b/modules/services/activate-system/default.nix @@ -26,6 +26,7 @@ ${config.system.activationScripts.etcChecks.text} ${config.system.activationScripts.etc.text} ${config.system.activationScripts.keyboard.text} + ${config.system.activationScripts.wrappers.text} ''; serviceConfig.RunAtLoad = true; serviceConfig.KeepAlive.SuccessfulExit = false; From ef8b45c7c00ce50487c9ac86c8263b469e3a35af Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:16:37 -0700 Subject: [PATCH 09/17] Fix tests --- tests/environment-path.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/environment-path.nix b/tests/environment-path.nix index 0bb9a055..3a60dcbb 100644 --- a/tests/environment-path.nix +++ b/tests/environment-path.nix @@ -20,6 +20,7 @@ with lib; env_path=$(bash -c 'source ${config.system.build.setEnvironment}; echo $PATH') test "$env_path" = "${builtins.concatStringsSep ":" [ + "/run/wrappers/bin" "beforePath" "myPath" "beforeProfile/bin" From 30ec0282676f8e96edb7ad7d4b8d16aec9fbf709 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:22:13 -0700 Subject: [PATCH 10/17] Replace Karabiner-specific /run/wrappers/bin implementation --- modules/services/karabiner-elements/default.nix | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/modules/services/karabiner-elements/default.nix b/modules/services/karabiner-elements/default.nix index 8be2ddff..51cac68e 100644 --- a/modules/services/karabiner-elements/default.nix +++ b/modules/services/karabiner-elements/default.nix @@ -86,16 +86,11 @@ in serviceConfig.RunAtLoad = true; }; - # We need this to run every reboot as /run gets nuked so we can't put this - # inside the preActivation script as it only gets run on darwin-rebuild switch. - launchd.daemons.setsuid_karabiner_session_monitor = { - script = '' - rm -rf /run/wrappers - mkdir -p /run/wrappers/bin - install -m4555 "${cfg.package}/Library/Application Support/org.pqrs/Karabiner-Elements/bin/karabiner_session_monitor" /run/wrappers/bin - ''; - serviceConfig.RunAtLoad = true; - serviceConfig.KeepAlive.SuccessfulExit = false; + security.wrappers.karabiner_session_monitor = { + setuid = true; + owner = "root"; + group = "wheel"; + source = "${pkgs.karabiner-elements}/Library/Application Support/org.pqrs/Karabiner-Elements/bin/karabiner_session_monitor"; }; launchd.user.agents.karabiner_session_monitor = { From ee07696c1cc6733487434d65b909651327647888 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:43:13 -0700 Subject: [PATCH 11/17] Remove mdDoc as it has been deprecated --- modules/security/wrappers/default.nix | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index a80a4aca..ccd81212 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -9,40 +9,40 @@ let options = with lib; { source = mkOption { type = types.path; - description = mdDoc "The absolute path to the program to be wrapped."; + description = "The absolute path to the program to be wrapped."; }; program = mkOption { type = with types; nullOr str; default = name; - description = mdDoc "The name of the wrapper program. Defaults to the attribute name."; + description = "The name of the wrapper program. Defaults to the attribute name."; }; owner = mkOption { type = types.str; - description = mdDoc "The owner of the wrapper program."; + description = "The owner of the wrapper program."; }; group = mkOption { type = types.str; - description = mdDoc "The group of the wrapper program."; + description = "The group of the wrapper program."; }; permissions = mkOption { type = types.str; default = "u+rx,g+x,o+x"; - description = mdDoc "The permissions to set on the wrapper."; + description = "The permissions to set on the wrapper."; }; setuid = mkOption { type = types.bool; default = false; - description = mdDoc "Whether to add the setuid bit to the wrapper program."; + description = "Whether to add the setuid bit to the wrapper program."; }; setgid = mkOption { type = types.bool; default = false; - description = mdDoc "Whether to add the setgid bit to the wrapper program."; + description = "Whether to add the setgid bit to the wrapper program."; }; # codesign = mkOption { # type = types.bool; # default = false; - # description = mdDoc "Whether to codesign the wrapper program."; + # description = "Whether to codesign the wrapper program."; # }; }; }); @@ -137,7 +137,7 @@ in }; } ''; - description = lib.mdDoc '' + description = '' This option effectively allows adding setuid/setgid bits and/or changing file ownership and permissions without directly modifying it. This works by creating a wrapper program under the {option}`security.wrapperDir` @@ -148,7 +148,7 @@ in type = lib.types.path; default = "/run/wrappers/bin"; internal = true; - description = lib.mdDoc '' + description = '' This option defines the path to the wrapper programs. It should not be overridden. ''; @@ -156,7 +156,7 @@ in # codesignIdentity = lib.mkOption { # type = lib.types.str; # default = "-"; - # description = lib.mdDoc "Identity to use for codesigning."; + # description = "Identity to use for codesigning."; # }; }; From 78d5089516a677920f662e3d398a6f3304a8c66d Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:20:18 -0700 Subject: [PATCH 12/17] Require services.activate-system.enable for security.wrappers `/run` is wiped on boot, and services.activate-system restores it. Technically a user could use security.wrappers without services.activate-system so long as they were willing to rerun the activation script after every reboot, but some services (such as Karabiner) add programs to security.wrappers and expect that they exist on or shortly after boot --- modules/security/wrappers/default.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index ccd81212..abd7fd23 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -111,6 +111,10 @@ in (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead") ]; + meta.maintainers = [ + lib.maintainers.samasaur or "samasaur" + ]; + ###### interface options.security = { wrappers = lib.mkOption { @@ -162,6 +166,10 @@ in ###### implementation config = { + assertions = [ + { assertion = cfg.wrappers != {} -> config.services.activate-system.enable; message = "security.wrappers requires services.activate-system because `/run` is wiped on boot"; } + ]; + environment.extraInit = '' # Wrappers override other bin directories. export PATH="${cfg.wrapperDir}:$PATH" From fd173ed9aeba87128e15c809018f543f09e1003d Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:30:54 -0800 Subject: [PATCH 13/17] Fix shellcheck errors --- modules/security/wrappers/default.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index abd7fd23..5476ec32 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -181,7 +181,7 @@ in # We want to place the tmpdirs for the wrappers to the parent dir. wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX) - chmod a+rx $wrapperDir + chmod a+rx "$wrapperDir" ${builtins.concatStringsSep "\n" mkWrappedPrograms} @@ -189,12 +189,12 @@ in # Atomically replace the symlink # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/ old=$(readlink -f ${cfg.wrapperDir}) - ln --symbolic --force --no-dereference $wrapperDir ${cfg.wrapperDir}-tmp + ln --symbolic --force --no-dereference "$wrapperDir" ${cfg.wrapperDir}-tmp mv --no-target-directory ${cfg.wrapperDir}-tmp ${cfg.wrapperDir} - rm --force --recursive $old + rm --force --recursive "$old" else # For initial setup - ln --symbolic $wrapperDir ${cfg.wrapperDir} + ln --symbolic "$wrapperDir" ${cfg.wrapperDir} fi ''; }; From 5ce52286fe16eff5c0580aa5001853633fca330f Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:06:18 -0800 Subject: [PATCH 14/17] security.wrappers: unset Darwin-specific env vars --- modules/security/wrappers/default.nix | 17 ------ modules/security/wrappers/wrapper.c | 85 ++++++++++++++++----------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index 5476ec32..3c866e0a 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -54,23 +54,6 @@ let 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 - # ''; - # }); }; mkWrapper = diff --git a/modules/security/wrappers/wrapper.c b/modules/security/wrappers/wrapper.c index b6e4a8b3..6f19c0ce 100644 --- a/modules/security/wrappers/wrapper.c +++ b/modules/security/wrappers/wrapper.c @@ -1,24 +1,9 @@ -#define _GNU_SOURCE #include #include #include #include #include #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include - -// imported from glibc -// #include "unsecvars.h" #ifndef SOURCE_PROG #error SOURCE_PROG should be defined via preprocessor commandline @@ -38,6 +23,10 @@ static noreturn void assert_failure(const char *assertion) { abort(); } +// The following comment and list of env vars are taken from the NixOS wrapper. +// Most of them likely have no effect on Darwin, but I have chosen to continue +// unsetting them just in case. + // 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. @@ -51,21 +40,15 @@ static noreturn void assert_failure(const char *assertion) { // 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" -#define UNSECURE_ENVVARS \ +#define GLIBC_UNSECURE_ENVVARS \ "GCONV_PATH\0" \ "GETCONF_DIR\0" \ + "GLIBC_TUNABLES\0" \ "HOSTALIASES\0" \ "LD_AUDIT\0" \ + "LD_BIND_NOT\0" \ + "LD_BIND_NOW\0" \ "LD_DEBUG\0" \ "LD_DEBUG_OUTPUT\0" \ "LD_DYNAMIC_WEAK\0" \ @@ -75,16 +58,50 @@ static noreturn void assert_failure(const char *assertion) { "LD_PRELOAD\0" \ "LD_PROFILE\0" \ "LD_SHOW_AUXV\0" \ - "LD_USE_LOAD_BIAS\0" \ + "LD_VERBOSE\0" \ + "LD_WARN\0" \ "LOCALDOMAIN\0" \ "LOCPATH\0" \ + "MALLOC_ARENA_MAX\0" \ + "MALLOC_ARENA_TEST\0" \ + "MALLOC_CHECK\0" \ + "MALLOC_MMAP_MAX_\0" \ + "MALLOC_MMAP_THRESHOLD_\0" \ + "MALLOC_PERTURB_\0" \ + "MALLOC_TOP_PAD_\0" \ "MALLOC_TRACE\0" \ + "MALLOC_TRIM_THRESHOLD_\0" \ "NIS_PATH\0" \ "NLSPATH\0" \ "RESOLV_HOST_CONF\0" \ "RES_OPTIONS\0" \ "TMPDIR\0" \ - // GLIBC_TUNABLES_ENVVAR \ + "TZDIR\0" + +// These are the variables that dyld refers to to load libraries. +// Taken from `man dyld` on macOS 14.6.1. +// macOS appears to ignore these variables for setuid binaries, +// but again we err on the side of caution. +#define DYLD_UNSECURE_ENVVARS \ + "DYLD_FRAMEWORK_PATH\0" \ + "DYLD_FALLBACK_FRAMEWORK_PATH\0" \ + "DYLD_VERSIONED_FRAMEWORK_PATH\0" \ + "DYLD_LIBRARY_PATH\0" \ + "DYLD_FALLBACK_LIBRARY_PATH\0" \ + "DYLD_VERSIONED_LIBRARY_PATH\0" \ + "DYLD_IMAGE_SUFFIX\0" \ + "DYLD_INSERT_LIBRARIES\0" \ + "DYLD_PRINT_TO_FILE\0" \ + "DYLD_PRINT_LIBRARIES\0" \ + "DYLD_PRINT_LOADERS\0" \ + "DYLD_PRINT_SEARCHING\0" \ + "DYLD_PRINT_APIS\0" \ + "DYLD_PRINT_BINDINGS\0" \ + "DYLD_PRINT_INITIALIZERS\0" \ + "DYLD_PRINT_SEGMENTS\0" \ + "DYLD_PRINT_ENV\0" \ + "DYLD_SHARED_REGION\0" \ + "DYLD_SHARED_CACHE_DIR\0" \ int main(int argc, char **argv) { ASSERT(argc >= 1); @@ -100,19 +117,19 @@ int main(int argc, char **argv) { // Drop insecure environment variables explicitly // - // glibc does this automatically in SUID binaries, but we'd like to cover this: + // dyld does this automatically in SUID binaries (for everything in DYLD_ENVVARS), + // 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. + // a) for variables that dyld does not unset (such as TMPDIR) + // b) in binaries with entitlements, but don't run with an altered effective + // UID/GID, and thus don't have env vars stripped automatically // // 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, + // If we don't explicitly unset them, it's quite easy to just set DYLD_INSERT_LIBRARIES, // 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) { + for (char *unsec = GLIBC_UNSECURE_ENVVARS DYLD_UNSECURE_ENVVARS; *unsec; unsec = strchr(unsec, 0) + 1) { if (debug) { fprintf(stderr, "unsetting %s\n", unsec); } From 5048c6a95e5971698d10f616be56910802bc1930 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:09:38 -0800 Subject: [PATCH 15/17] security.wrappers: update comment (glibc -> dyld) --- modules/security/wrappers/wrapper.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/security/wrappers/wrapper.c b/modules/security/wrappers/wrapper.c index 6f19c0ce..685410d2 100644 --- a/modules/security/wrappers/wrapper.c +++ b/modules/security/wrappers/wrapper.c @@ -125,7 +125,7 @@ int main(int argc, char **argv) { // UID/GID, and thus don't have env vars stripped automatically // // We're using musl, which doesn't drop environment variables in secure mode, - // and we'd also like glibc-specific variables to be covered. + // and we'd also like dyld-specific variables to be covered. // // If we don't explicitly unset them, it's quite easy to just set DYLD_INSERT_LIBRARIES, // have it passed through to the wrapped program, and gain privileges. From 4fc8bc106aba9fed827349cdc3fa42028e970f98 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Mon, 18 Nov 2024 00:29:20 -0800 Subject: [PATCH 16/17] security.wrappers: remove assertion on services.activate-system.enable The option was removed since it is now always on. --- modules/security/wrappers/default.nix | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index 3c866e0a..64021a3f 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -149,10 +149,6 @@ in ###### implementation config = { - assertions = [ - { assertion = cfg.wrappers != {} -> config.services.activate-system.enable; message = "security.wrappers requires services.activate-system because `/run` is wiped on boot"; } - ]; - environment.extraInit = '' # Wrappers override other bin directories. export PATH="${cfg.wrapperDir}:$PATH" From 5776f9bf3d88a61d20203279dac0421b99d02093 Mon Sep 17 00:00:00 2001 From: Sam <30577766+Samasaur1@users.noreply.github.com> Date: Mon, 20 Jan 2025 01:10:15 -0800 Subject: [PATCH 17/17] security.wrappers: copy source programs instead of wrapping in binaries Copying the wrapper code from NixOS and hoping that I managed to catch all the relevant dyld environment variables was not particularly confidence-inducing, so this commit removes the wrappers entirely and simply copies the source programs before modifying permissions. This means that the given source programs must safely handle being setuid/setgid binaries and being located in `/run/wrappers/bin`. There's now only `default.nix` in `modules/security/wrappers`, so I could have removed the directory, but I have left it for now in anticipation of other files potentially ending up there. --- modules/security/wrappers/default.nix | 8 +- modules/security/wrappers/wrapper.c | 145 -------------------------- modules/security/wrappers/wrapper.nix | 19 ---- 3 files changed, 2 insertions(+), 170 deletions(-) delete mode 100644 modules/security/wrappers/wrapper.c delete mode 100644 modules/security/wrappers/wrapper.nix diff --git a/modules/security/wrappers/default.nix b/modules/security/wrappers/default.nix index 64021a3f..178c7793 100644 --- a/modules/security/wrappers/default.nix +++ b/modules/security/wrappers/default.nix @@ -52,10 +52,6 @@ let (opts: mkWrapper opts) (builtins.attrValues cfg.wrappers); - securityWrapper = sourceProg : pkgs.pkgsStatic.callPackage ./wrapper.nix { - inherit sourceProg; - }; - mkWrapper = { program , source @@ -71,10 +67,10 @@ let codesigned = if codesign then '' # codesign ${source} to "$wrapperDir/${program}" INSTEAD OF the next line - cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" + cp ${source} "$wrapperDir/${program}" '' else '' - cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" + cp ${source} "$wrapperDir/${program}" ''; in '' diff --git a/modules/security/wrappers/wrapper.c b/modules/security/wrappers/wrapper.c deleted file mode 100644 index 685410d2..00000000 --- a/modules/security/wrappers/wrapper.c +++ /dev/null @@ -1,145 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#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"; - -static noreturn void assert_failure(const char *assertion) { - fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion); - fflush(stderr); - abort(); -} - -// The following comment and list of env vars are taken from the NixOS wrapper. -// Most of them likely have no effect on Darwin, but I have chosen to continue -// unsetting them just in case. - -// 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 GLIBC_UNSECURE_ENVVARS \ - "GCONV_PATH\0" \ - "GETCONF_DIR\0" \ - "GLIBC_TUNABLES\0" \ - "HOSTALIASES\0" \ - "LD_AUDIT\0" \ - "LD_BIND_NOT\0" \ - "LD_BIND_NOW\0" \ - "LD_DEBUG\0" \ - "LD_DEBUG_OUTPUT\0" \ - "LD_DYNAMIC_WEAK\0" \ - "LD_HWCAP_MASK\0" \ - "LD_LIBRARY_PATH\0" \ - "LD_ORIGIN_PATH\0" \ - "LD_PRELOAD\0" \ - "LD_PROFILE\0" \ - "LD_SHOW_AUXV\0" \ - "LD_VERBOSE\0" \ - "LD_WARN\0" \ - "LOCALDOMAIN\0" \ - "LOCPATH\0" \ - "MALLOC_ARENA_MAX\0" \ - "MALLOC_ARENA_TEST\0" \ - "MALLOC_CHECK\0" \ - "MALLOC_MMAP_MAX_\0" \ - "MALLOC_MMAP_THRESHOLD_\0" \ - "MALLOC_PERTURB_\0" \ - "MALLOC_TOP_PAD_\0" \ - "MALLOC_TRACE\0" \ - "MALLOC_TRIM_THRESHOLD_\0" \ - "NIS_PATH\0" \ - "NLSPATH\0" \ - "RESOLV_HOST_CONF\0" \ - "RES_OPTIONS\0" \ - "TMPDIR\0" \ - "TZDIR\0" - -// These are the variables that dyld refers to to load libraries. -// Taken from `man dyld` on macOS 14.6.1. -// macOS appears to ignore these variables for setuid binaries, -// but again we err on the side of caution. -#define DYLD_UNSECURE_ENVVARS \ - "DYLD_FRAMEWORK_PATH\0" \ - "DYLD_FALLBACK_FRAMEWORK_PATH\0" \ - "DYLD_VERSIONED_FRAMEWORK_PATH\0" \ - "DYLD_LIBRARY_PATH\0" \ - "DYLD_FALLBACK_LIBRARY_PATH\0" \ - "DYLD_VERSIONED_LIBRARY_PATH\0" \ - "DYLD_IMAGE_SUFFIX\0" \ - "DYLD_INSERT_LIBRARIES\0" \ - "DYLD_PRINT_TO_FILE\0" \ - "DYLD_PRINT_LIBRARIES\0" \ - "DYLD_PRINT_LOADERS\0" \ - "DYLD_PRINT_SEARCHING\0" \ - "DYLD_PRINT_APIS\0" \ - "DYLD_PRINT_BINDINGS\0" \ - "DYLD_PRINT_INITIALIZERS\0" \ - "DYLD_PRINT_SEGMENTS\0" \ - "DYLD_PRINT_ENV\0" \ - "DYLD_SHARED_REGION\0" \ - "DYLD_SHARED_CACHE_DIR\0" \ - -int main(int argc, char **argv) { - ASSERT(argc >= 1); - - // argv[0] goes into a lot of places, to a far greater degree than other elements - // of argv. glibc has had buffer overflows relating to argv[0], eg CVE-2023-6246. - // Since we expect the wrappers to be invoked from either $PATH or /run/wrappers/bin, - // there should be no reason to pass any particularly large values here, so we can - // be strict for strictness' sake. - ASSERT(strlen(argv[0]) < 512); - - int debug = getenv(wrapper_debug) != NULL; - - // Drop insecure environment variables explicitly - // - // dyld does this automatically in SUID binaries (for everything in DYLD_ENVVARS), - // but we'd like to cover this: - // - // a) for variables that dyld does not unset (such as TMPDIR) - // b) in binaries with entitlements, but don't run with an altered effective - // UID/GID, and thus don't have env vars stripped automatically - // - // We're using musl, which doesn't drop environment variables in secure mode, - // and we'd also like dyld-specific variables to be covered. - // - // If we don't explicitly unset them, it's quite easy to just set DYLD_INSERT_LIBRARIES, - // have it passed through to the wrapped program, and gain privileges. - for (char *unsec = GLIBC_UNSECURE_ENVVARS DYLD_UNSECURE_ENVVARS; *unsec; unsec = strchr(unsec, 0) + 1) { - if (debug) { - fprintf(stderr, "unsetting %s\n", unsec); - } - unsetenv(unsec); - } - - execve(SOURCE_PROG, argv, environ); - - fprintf(stderr, "%s: cannot run `%s': %s\n", - argv[0], SOURCE_PROG, strerror(errno)); - - return 1; -} diff --git a/modules/security/wrappers/wrapper.nix b/modules/security/wrappers/wrapper.nix deleted file mode 100644 index 8c795e74..00000000 --- a/modules/security/wrappers/wrapper.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ stdenv, sourceProg, debug ? false }: -# For testing: -# $ nix-build -E 'with import {}; pkgs.callPackage ./wrapper.nix { sourceProg = "${pkgs.hello}/bin/hello"; debug = true; }' -stdenv.mkDerivation { - name = "security-wrapper-${baseNameOf sourceProg}"; - 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} -o $out/bin/security-wrapper - ''; -}