diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7b753937e8082..09088202503de 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -184,6 +184,7 @@ ./programs/ecryptfs.nix ./programs/environment.nix ./programs/envision.nix + ./programs/espanso-capdacoverride ./programs/evince.nix ./programs/extra-container.nix ./programs/fcast-receiver.nix diff --git a/nixos/modules/programs/espanso-capdacoverride/default.nix b/nixos/modules/programs/espanso-capdacoverride/default.nix new file mode 100644 index 0000000000000..866c47ed131f7 --- /dev/null +++ b/nixos/modules/programs/espanso-capdacoverride/default.nix @@ -0,0 +1,60 @@ +{ + config, + lib, + pkgs, + ... +}: + +with lib; +{ + meta = { + maintainers = with maintainers; [ pitkling ]; + }; + + options = { + programs.espanso.capdacoverride = { + enable = (mkEnableOption "espanso-wayland overlay with DAC_OVERRIDE capability") // { + default = false; + extraDescription = '' + Creates an espanso binary with the DAC_OVERRIDE capability (via `security.wrappers`) and overlays `pkgs.espanso-wayland` such that self-forks call the capability-enabled binary. + Required for `pkgs.espanso-wayland` to work correctly if not run with root privileges. + ''; + }; + + package = mkOption { + type = types.package // { + check = package: types.package.check package && (builtins.elem "wayland" package.buildFeatures); + description = + types.package.description + + " for espanso with wayland support (`package.builtFeatures` must contain `\"wayland\"`)"; + }; + default = pkgs._espanso-wayland-orig; + defaultText = "pkgs.espanso-wayland (before applying the overlay)"; + description = "The espanso-wayland package used as the base to generate the capability-enabled package."; + }; + }; + }; + + config = + let + cfg = config.programs.espanso.capdacoverride; + in + mkIf cfg.enable { + nixpkgs.overlays = [ + (final: prev: { + _espanso-wayland-orig = prev.espanso-wayland; + espanso-wayland = pkgs.callPackage ./espanso-capdacoverride.nix { + capDacOverrideWrapperDir = "${config.security.wrapperDir}"; + espanso = cfg.package; + }; + }) + ]; + + security.wrappers."${pkgs.espanso-wayland.meta.mainProgram}" = { + source = "${getExe pkgs.espanso-wayland}"; + capabilities = "cap_dac_override+p"; + owner = "root"; + group = "root"; + }; + }; +} diff --git a/nixos/modules/programs/espanso-capdacoverride/espanso-capdacoverride.nix b/nixos/modules/programs/espanso-capdacoverride/espanso-capdacoverride.nix new file mode 100644 index 0000000000000..48822ac9f2e83 --- /dev/null +++ b/nixos/modules/programs/espanso-capdacoverride/espanso-capdacoverride.nix @@ -0,0 +1,60 @@ +{ + autoPatchelfHook, + capDacOverrideWrapperDir, + espanso, + patchelfUnstable, # have to use patchelfUnstable to support --rename-dynamic-symbols + stdenv, +}: +let + inherit (espanso) version; + pname = "${espanso.pname}-capdacoverride"; + + wrapperLibName = "wrapper-lib.so"; + wrapperLibSource = "wrapper-lib.c"; + + # On Wayland, Espanso requires the DAC_OVERRIDE capability. One can create a wrapper binary with this + # capability using the `config.security.wrappers.` framework. However, this is not enough: the + # capability is required by a worker process of Espanso created by forking `/proc/self/exe`, which points + # to the executable **without** the DAC_OVERRIDE capability. Thus, we inject a wrapper library into Espanso + # that redirects requests to `/proc/self/exe` to the binary with the proper capabilities. + wrapperLib = stdenv.mkDerivation { + name = "${pname}-${version}-wrapper-lib"; + + src = builtins.path { + name = "${pname}-${version}-wrapper-lib-source"; + path = ./.; + filter = path: type: baseNameOf path == wrapperLibSource; + }; + + postPatch = '' + substitute ${wrapperLibSource} lib.c --subst-var-by to "${capDacOverrideWrapperDir}/espanso" + cc -fPIC -shared lib.c -o ${wrapperLibName} + ''; + + installPhase = '' + runHook preInstall + install -D -t $out/lib ${wrapperLibName} + runHook postInstall + ''; + }; +in +espanso.overrideAttrs (previousAttrs: { + inherit pname; + + buildInputs = previousAttrs.buildInputs ++ [ wrapperLib ]; + + nativeBuildInputs = previousAttrs.nativeBuildInputs ++ [ + autoPatchelfHook + patchelfUnstable + ]; + + postInstall = + '' + echo readlink readlink_wrapper > readlink_name_map + patchelf \ + --rename-dynamic-symbols readlink_name_map \ + --add-needed ${wrapperLibName} \ + "$out/bin/espanso" + '' + + previousAttrs.postInstall; +}) diff --git a/nixos/modules/programs/espanso-capdacoverride/wrapper-lib.c b/nixos/modules/programs/espanso-capdacoverride/wrapper-lib.c new file mode 100644 index 0000000000000..fe3d2ccf31267 --- /dev/null +++ b/nixos/modules/programs/espanso-capdacoverride/wrapper-lib.c @@ -0,0 +1,20 @@ +#include +#include +#include + +static const char from[] = "/proc/self/exe"; +static const char to[] = "@to@"; + +ssize_t readlink_wrapper(const char *restrict path, char *restrict buf, size_t bufsize) { + if (strcmp(path, from) == 0) { + printf("readlink_wrapper.c: Resolving readlink call to '%s' to '%s'\n", from, to); + size_t to_length = strlen(to); + if (to_length > bufsize) { + to_length = bufsize; + } + memcpy(buf, to, to_length); + return to_length; + } else { + return readlink(path, buf, bufsize); + } +}