diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 63095c936728..91ef22883b4f 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -22,6 +22,8 @@ - [LibreChat](https://www.librechat.ai/), open-source self-hostable ChatGPT clone with Agents and RAG APIs. Available as [services.librechat](#opt-services.librechat.enable). +- [nohang](https://github.com/hakavlad/nohang), a daemon for Linux that prevents out of memory (OOM) situations from affecting system responsiveness. Available as [services.nohang](#opt-services.nohang.enable) + - [DankMaterialShell](https://danklinux.com), a complete desktop shell for Wayland compositors built with Quickshell. Available as [programs.dms-shell](#opt-programs.dms-shell.enable). - [dms-greeter](https://danklinux.com), a modern display manager greeter for DankMaterialShell that works with greetd and supports multiple Wayland compositors. Available as [services.displayManager.dms-greeter](#opt-services.displayManager.dms-greeter.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index ed876450bac2..da1a3e66a159 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1530,6 +1530,7 @@ ./services/system/localtimed.nix ./services/system/nix-daemon-firewall.nix ./services/system/nix-daemon.nix + ./services/system/nohang.nix ./services/system/nscd.nix ./services/system/nvme-rs.nix ./services/system/saslauthd.nix diff --git a/nixos/modules/services/system/nohang.nix b/nixos/modules/services/system/nohang.nix new file mode 100644 index 000000000000..abc8d00f90e7 --- /dev/null +++ b/nixos/modules/services/system/nohang.nix @@ -0,0 +1,113 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.nohang; + + inherit (lib) + literalExpression + mkEnableOption + mkIf + mkOption + mkPackageOption + types + ; +in +{ + meta = { + maintainers = with lib.maintainers; [ Dev380 ]; + }; + + options.services.nohang = { + enable = mkEnableOption "nohang, a daemon that keeps system responsiveness when Linux is out of memory"; + + package = mkPackageOption pkgs "nohang" { }; + + configPath = mkOption { + type = types.either (types.enum [ + "basic" + "desktop" + ]) types.path; + default = "desktop"; + example = literalExpression "./my-nohang-config.conf"; + description = '' + Configuration file to use with nohang. The default and desktop example configurations in the nohang repository + can be used by setting this to "basic" or "desktop" (which is the default). Otherwise, you can set it to the path + of a custom configuration file. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.nohang = { + description = "Sophisticated low memory handler"; + documentation = [ + "man:nohang(8)" + "https://github.com/hakavlad/nohang" + ]; + after = [ "sysinit.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = + "${lib.getExe cfg.package} --monitor --config " + + ( + if cfg.configPath == "basic" then + "${cfg.package}/etc/nohang/nohang.conf" + else if cfg.configPath == "desktop" then + "${cfg.package}/etc/nohang/nohang-desktop.conf" + else + cfg.configPath + ); + Slice = "hostcritical.slice"; + SyslogIdentifier = + if cfg.configPath == "basic" then + "nohang" + else if cfg.configPath == "desktop" then + "nohang-desktop" + else + "nohang-custom-config"; + KillMode = "mixed"; + Restart = "always"; + RestartSec = 0; + + CPUSchedulingResetOnFork = true; + RestrictRealtime = "yes"; + + TasksMax = 25; + MemoryMax = "100M"; + MemorySwapMax = "100M"; + + UMask = 27; + ProtectSystem = "strict"; + ReadWritePaths = "/var/log"; + InaccessiblePaths = "/home /root"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + ProtectHostname = true; + MemoryDenyWriteExecute = "yes"; + RestrictNamespaces = "yes"; + LockPersonality = "yes"; + PrivateTmp = true; + DeviceAllow = "/dev/kmsg rw"; + DevicePolicy = "closed"; + + CapabilityBoundingSet = [ + "CAP_KILL" + "CAP_IPC_LOCK" + "CAP_SYS_PTRACE" + "CAP_DAC_READ_SEARCH" + "CAP_DAC_OVERRIDE" + "CAP_AUDIT_WRITE" + "CAP_SETUID" + "CAP_SETGID" + "CAP_SYS_RESOURCE" + "CAP_SYSLOG" + ]; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0cc6942f10db..36cd4a2ab0ff 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1105,6 +1105,7 @@ in nixpkgs = pkgs.callPackage ../modules/misc/nixpkgs/test.nix { inherit evalMinimalConfig; }; nixseparatedebuginfod2 = runTest ./nixseparatedebuginfod2.nix; node-red = runTest ./node-red.nix; + nohang = runTest ./nohang.nix; nomad = runTest ./nomad.nix; nominatim = runTest ./nominatim.nix; non-default-filesystems = handleTest ./non-default-filesystems.nix { }; diff --git a/nixos/tests/nohang.nix b/nixos/tests/nohang.nix new file mode 100644 index 000000000000..8b7c4ec0ca15 --- /dev/null +++ b/nixos/tests/nohang.nix @@ -0,0 +1,56 @@ +# The following was modified from ./earlyoom.nix +{ lib, pkgs, ... }: +{ + name = "nohang"; + meta = { + maintainers = with lib.maintainers; [ + Dev380 + ]; + }; + + nodes.machine = + { pkgs, ... }: + { + # Limit VM resource usage. + virtualisation.memorySize = 1024; + + services.nohang.enable = true; + # disable other oom killers just in case + systemd.oomd.enable = false; + + systemd.services.testbloat = { + description = "Create a lot of memory pressure"; + serviceConfig = { + ExecStart = "${lib.getExe' pkgs.coreutils "tail"} /dev/zero"; + }; + }; + }; + + # SIGTERM may be given so tail /dev/zero may or may not succeed + # The output will have have something like "Sending SIGTERM to /nix/store/87fc" + # with the truncated path so we'll check for that in the test + testScript = '' + machine.wait_for_unit("nohang.service") + + with subtest("nohang should kill the bad service"): + machine.execute("systemctl start --wait testbloat.service") + signal_type = None + match machine.get_unit_info("testbloat.service")["Result"]: + case "signal": + signal_type = "SIGKILL" + case "success": + signal_type = "SIGTERM" + output = machine.succeed('journalctl -u nohang.service -b0') + + if not f'[ OK ] Sending {signal_type}' in output: + raise Exception(f"'[ OK ] Sending {signal_type}' not in output") + if not 'The victim' in output: + raise Exception("'The victim' not in output") + if not 'Memory status after implementing a corrective action:' in output: + raise Exception("'Memory status after implementing a corrective action:' not in output") + if not 'FINISHING implement_corrective_action()' in output: + raise Exception("'FINISHING implement_corrective_action()' not in output") + if not f"{signal_type} to {'${pkgs.coreutils}'[:len('/nix/store/1234')]}: 1" in output: + raise Exception(f"'{signal_type} to {'${pkgs.coreutils}'[:len('/nix/store/1234')]}: 1' not in output") + ''; +} diff --git a/pkgs/by-name/no/nohang/package.nix b/pkgs/by-name/no/nohang/package.nix index 08c6a2e99ccf..846a644bd6ab 100644 --- a/pkgs/by-name/no/nohang/package.nix +++ b/pkgs/by-name/no/nohang/package.nix @@ -1,6 +1,7 @@ { lib, stdenv, + nixosTests, fetchFromGitHub, python3, sudo, @@ -39,6 +40,10 @@ stdenv.mkDerivation (finalAttrs: { "SYSTEMDUNITDIR=/lib/systemd/system" ]; + passthru.tests = { + inherit (nixosTests) nohang; + }; + meta = { homepage = "https://github.com/hakavlad/nohang"; description = "Sophisticated low memory handler for Linux";