From 3ec1cd9a0703fbd55d865b7fd2b07d08374f0355 Mon Sep 17 00:00:00 2001 From: lignus <64274485+linnnus@users.noreply.github.com> Date: Sat, 9 Aug 2025 18:22:08 +0200 Subject: [PATCH] launchd+targets/darwin: Escape XML in plists (#7356) This patch updates all usage of toPlist such that it escapes any strings in the final output. The motication for this change is to avoid confusion when end-users of home-manager's APIs are not aware that the option values they set end up being passed un-escaped to XML files. BREAKING CHANGE: Consumers doing manual escaping will now be doubly escaped. Co-authored-by: Linnnus --- modules/launchd/default.nix | 2 +- modules/launchd/launchd.nix | 12 ++++++++++++ modules/misc/news/2025/07/2025-07-01_22-15-34.nix | 13 +++++++++++++ modules/targets/darwin/keybindings.nix | 4 +++- modules/targets/darwin/user-defaults/default.nix | 3 ++- tests/modules/launchd/agents.nix | 1 + tests/modules/launchd/expected-agent.plist | 2 ++ .../services/macos-remap-keys/basic-agent.plist | 2 +- 8 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 modules/misc/news/2025/07/2025-07-01_22-15-34.nix diff --git a/modules/launchd/default.nix b/modules/launchd/default.nix index 8cb500eda..baebdfdce 100644 --- a/modules/launchd/default.nix +++ b/modules/launchd/default.nix @@ -43,7 +43,7 @@ let }; }; - toAgent = config: pkgs.writeText "${config.Label}.plist" (toPlist { } config); + toAgent = config: pkgs.writeText "${config.Label}.plist" (toPlist { escape = true; } config); agentPlists = lib.mapAttrs' (n: v: lib.nameValuePair "${v.config.Label}.plist" (toAgent v.config)) ( lib.filterAttrs (n: v: v.enable) cfg.agents diff --git a/modules/launchd/launchd.nix b/modules/launchd/launchd.nix index fee128ebb..a212efd33 100644 --- a/modules/launchd/launchd.nix +++ b/modules/launchd/launchd.nix @@ -155,6 +155,18 @@ in This key maps to the second argument of `execvp(3)`. This key is required in the absence of the Program key. Please note: many people are confused by this key. Please read `execvp(3)` very carefully! ''; + # TODO: Remove this some time after 25.01. + apply = + value: + if value != null then + map ( + item: + lib.warnIf (lib.hasInfix "&" item) + "A value for `ProgramArguments` contains the literal string `&`. This is no longer necessary and will lead to double-escaping, as home-manager now automatically escapes special characters." + item + ) value + else + value; }; EnableGlobbing = mkOption { diff --git a/modules/misc/news/2025/07/2025-07-01_22-15-34.nix b/modules/misc/news/2025/07/2025-07-01_22-15-34.nix new file mode 100644 index 000000000..9c6fba5e2 --- /dev/null +++ b/modules/misc/news/2025/07/2025-07-01_22-15-34.nix @@ -0,0 +1,13 @@ +{ pkgs, ... }: +{ + time = "2025-07-01T20:15:34+00:00"; + condition = pkgs.stdenv.hostPlatform.isDarwin; + message = '' + XML characters are escaped for 'targets.darwin.keybindings' and 'launchd.agents.'. + + Special characters used in strings passed to 'targets.darwin.keybindings' + and 'launchd.agents.' are now escaped before being included in the + generated plist files. If you were doing manual escaping you will need to + stop to avoid double escaping. + ''; +} diff --git a/modules/targets/darwin/keybindings.nix b/modules/targets/darwin/keybindings.nix index 7b0f3b93b..eba3cef73 100644 --- a/modules/targets/darwin/keybindings.nix +++ b/modules/targets/darwin/keybindings.nix @@ -8,7 +8,9 @@ let cfg = config.targets.darwin; homeDir = config.home.homeDirectory; - confFile = pkgs.writeText "DefaultKeybinding.dict" (lib.generators.toPlist { } cfg.keybindings); + confFile = pkgs.writeText "DefaultKeybinding.dict" ( + lib.generators.toPlist { escape = true; } cfg.keybindings + ); in { options.targets.darwin.keybindings = lib.mkOption { diff --git a/modules/targets/darwin/user-defaults/default.nix b/modules/targets/darwin/user-defaults/default.nix index 4d83f9f1c..153f0d164 100644 --- a/modules/targets/darwin/user-defaults/default.nix +++ b/modules/targets/darwin/user-defaults/default.nix @@ -11,7 +11,8 @@ let mkActivationCmds = isLocal: settings: let - toDefaultsFile = domain: attrs: pkgs.writeText "${domain}.plist" (lib.generators.toPlist { } attrs); + toDefaultsFile = + domain: attrs: pkgs.writeText "${domain}.plist" (lib.generators.toPlist { escape = true; } attrs); cliFlags = lib.optionalString isLocal "-currentHost"; diff --git a/tests/modules/launchd/agents.nix b/tests/modules/launchd/agents.nix index 8c2d49073..76e6fa65e 100644 --- a/tests/modules/launchd/agents.nix +++ b/tests/modules/launchd/agents.nix @@ -14,6 +14,7 @@ }; ProcessType = "Background"; UnrecognizedByHomeManager = "should make it to the resulting plist"; + "\"Special\" characters" = ""; }; }; diff --git a/tests/modules/launchd/expected-agent.plist b/tests/modules/launchd/expected-agent.plist index 890b99339..4db6dcc1e 100644 --- a/tests/modules/launchd/expected-agent.plist +++ b/tests/modules/launchd/expected-agent.plist @@ -2,6 +2,8 @@ + "Special" characters + <should be escaped> KeepAlive Crashed diff --git a/tests/modules/services/macos-remap-keys/basic-agent.plist b/tests/modules/services/macos-remap-keys/basic-agent.plist index b59f97d99..fa3da3cb8 100644 --- a/tests/modules/services/macos-remap-keys/basic-agent.plist +++ b/tests/modules/services/macos-remap-keys/basic-agent.plist @@ -14,7 +14,7 @@ /usr/bin/hidutil property --set - { "UserKeyMapping": [ { "HIDKeyboardModifierMappingSrc": 0x700000039, "HIDKeyboardModifierMappingDst": 0x70000002A } ] } + { "UserKeyMapping": [ { "HIDKeyboardModifierMappingSrc": 0x700000039, "HIDKeyboardModifierMappingDst": 0x70000002A } ] } RunAtLoad