Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/espanso: fix wayland problems due to missing capabilities #328890

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
./programs/ecryptfs.nix
./programs/environment.nix
./programs/envision.nix
./programs/espanso-capdacoverride
./programs/evince.nix
./programs/extra-container.nix
./programs/fcast-receiver.nix
Expand Down
57 changes: 57 additions & 0 deletions nixos/modules/programs/espanso-capdacoverride/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
config,
lib,
pkgs,
...
}:

let
cfg = config.programs.espanso.capdacoverride;
in
{
meta = {
maintainers = with lib.maintainers; [ pitkling ];
};

options = {
programs.espanso.capdacoverride = {
enable = (lib.mkEnableOption "espanso-wayland overlay with DAC_OVERRIDE capability") // {
description = ''
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 = lib.mkOption {
type = lib.types.package // {
check = package: lib.types.package.check package && (builtins.elem "wayland" package.buildFeatures);
description =
lib.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 = lib.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."espanso-wayland" = {
source = lib.getExe pkgs.espanso-wayland;
capabilities = "cap_dac_override+p";
owner = "root";
group = "root";
};
};
}
Original file line number Diff line number Diff line change
@@ -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";

# On Wayland, Espanso requires the DAC_OVERRIDE capability. One can create a wrapper binary with this
# capability using the `config.security.wrappers.<name>` 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.
Comment on lines +16 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also just patch the code to point to our correct path which IMO sounds a lot less troublesome than all of this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, that's an alternative I thought about in the beginning. My main motivation for going this route was that it seems lower maintenance wrt upstream changes. With the wrapper library, no matter which of the Espanso subcommands perform a fork (now or sometime in the future), it is always caught and redirected. Patching the code would require manual updates.

I should also mention that, ideally, such an automatic fork redirect or something similar would be handled internally by security.wrappers. The reason that's currently not working has to do with the way how Espanso yields the capabilities early and relies on file capabilities to regain them when required (see this comment).

wrapperLib = stdenv.mkDerivation {
name = "${pname}-${version}-wrapper-lib";

dontUnpack = true;

postPatch = ''
substitute ${./wrapper-lib.c} lib.c --subst-var-by to "${capDacOverrideWrapperDir}/espanso-wayland"
'';

buildPhase = ''
runHook preBuild
cc -fPIC -shared lib.c -o ${wrapperLibName}
pitkling marked this conversation as resolved.
Show resolved Hide resolved
runHook postBuild
'';

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need unstable here?

Copy link
Member Author

@pitkling pitkling Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we do. The wrapper library works by renaming the readlink symbol into readlink_wrapper. This is done via the --rename-dynamic-symbols flag of patchelf, which was added in release 0.18.0 (third bullet point in changelog). Stable NixOS currently has patchelf 0.15. (There is a brief explanation why patchelfUnstable is required at the top of espanso-capdacoverride.nix.)

];

postInstall =
''
echo readlink readlink_wrapper > readlink_name_map
patchelf \
--rename-dynamic-symbols readlink_name_map \
--add-needed ${wrapperLibName} \
"$out/bin/espanso"
''
+ previousAttrs.postInstall;
})
20 changes: 20 additions & 0 deletions nixos/modules/programs/espanso-capdacoverride/wrapper-lib.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include <stdio.h>
#include <string.h>
#include <unistd.h>

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);
}
}