Skip to content

fix: check managed systemd unit before is-enabled#38819

Merged
obviyus merged 1 commit intomainfrom
fix/systemd-unit-presence-before-is-enabled
Mar 7, 2026
Merged

fix: check managed systemd unit before is-enabled#38819
obviyus merged 1 commit intomainfrom
fix/systemd-unit-presence-before-is-enabled

Conversation

@obviyus
Copy link
Copy Markdown
Contributor

@obviyus obviyus commented Mar 7, 2026

Summary

Supersedes #38551.

Fresh installs were calling systemctl --user is-enabled before OpenClaw's managed user unit file existed. On some distros that comes back as a generic child-process failure, which made install fail even though the real answer was simply "service not installed yet".

This fixes the narrower root cause instead of treating every non-zero is-enabled exit as false:

  • check whether the managed unit file already exists under ~/.config/systemd/user/
  • if the unit file is missing, return false without calling systemctl
  • if the unit file exists, keep current behavior: disabled/not-found states still return false, real systemctl failures still throw

That keeps the fresh-install path working without weakening later error handling.

Test Plan

  • pnpm test --run src/daemon/systemd.test.ts src/cli/daemon-cli/install.integration.test.ts
  • manual fresh-VM validation from the reporter flow

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 7, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Untrusted PATH search when invoking systemctl enables binary hijacking under elevated execution

1. 🔵 Untrusted PATH search when invoking systemctl enables binary hijacking under elevated execution

Property Value
Severity Low
CWE CWE-426
Location src/daemon/systemd.ts:140-144

Description

The daemon utilities invoke systemctl by name (not absolute path) via execFile, which relies on PATH lookup.

  • execSystemctl() calls execFileUtf8("systemctl", ...).
  • execFile does not use a shell (good), but it does resolve the executable using the current process PATH.
  • In multiple flows (including isSystemdServiceEnabled), this can run while the process is invoked under sudo (the code explicitly detects SUDO_USER and changes behavior). If a privileged invocation occurs with an attacker-influenced PATH (e.g., sudo -E, misconfigured secure_path, or a service/unit that sets PATH to include a user-writable directory), an attacker can place a malicious systemctl earlier in PATH and gain code execution with the privileges of the OpenClaw process.

Vulnerable code:

async function execSystemctl(args: string[]): Promise<{ stdout: string; stderr: string; code: number }> {
  return await execFileUtf8("systemctl", args);
}

And used from:

const res = await execSystemctlUser(env, ["is-enabled", unitName]);

Recommendation

Mitigate executable-hijacking by avoiding PATH resolution for privileged command execution.

Recommended approaches (pick one):

  1. Use an absolute path to systemctl (and optionally verify it is owned by root and not writable):
const SYSTEMCTL_PATH = "/usr/bin/systemctl"; // or detect from a small allowlist
return await execFileUtf8(SYSTEMCTL_PATH, args, {
  env: {
    ...process.env,
    PATH: "/usr/sbin:/usr/bin:/sbin:/bin",
  },
});
  1. Provide a hardened environment to execFile (at minimum, set a safe PATH) and avoid inheriting potentially unsafe values when running under sudo/root.

Additionally, consider inserting -- before the unit name in calls like systemctl ... is-enabled -- <unit> to defensively prevent any option-like unit name from being parsed as a flag.


Analyzed PR: #38819 at commit 38f618c

Last updated on: 2026-03-07T12:17:58Z

@obviyus obviyus force-pushed the fix/systemd-unit-presence-before-is-enabled branch from 27bb16a to 38f618c Compare March 7, 2026 11:41
@obviyus obviyus merged commit 26c9796 into main Mar 7, 2026
4 checks passed
@obviyus obviyus deleted the fix/systemd-unit-presence-before-is-enabled branch March 7, 2026 11:41
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR fixes a fresh-install failure where systemctl --user is-enabled was called before OpenClaw's managed unit file existed, causing distro-dependent child-process errors to bubble up as install failures. The fix adds an existence check for the unit file via fs.access at the top of isSystemdServiceEnabled, returning false immediately on ENOENT (service not yet installed) without invoking systemctl, while still rethrowing any other filesystem errors.

Key changes:

  • src/daemon/systemd.ts: Early-return on ENOENT from fs.access(resolveSystemdUnitPath(env)) before calling systemctl is-enabled. Also fixes a subtle pre-existing inconsistency where resolveSystemdServiceName was passed args.env ?? {} instead of the fully-resolved env (args.env ?? process.env), ensuring the service name and unit file path always resolve from the same environment.
  • src/daemon/systemd.test.ts: Adds vi.restoreAllMocks() to beforeEach to properly clean up fs spies, introduces mockManagedUnitPresent() helper, and adds a dedicated test asserting that execFileMock is never called when the unit file is absent. All affected tests now mock fs.access and provide HOME env variable for proper path resolution.

Confidence Score: 4/5

  • Safe to merge—targeted, well-tested fix with correct error handling and no regressions to existing behavior.
  • The change is narrow in scope (one function, one new guard clause), correctly handles all error codes (ENOENT → silent false, everything else → rethrow), preserves all existing systemctl error-handling paths, and is backed by a new dedicated test plus updates to all affected existing tests. The subtle env-consistency fix is a clear improvement with no downside.
  • No files require special attention.

Last reviewed commit: 38f618c

@obviyus
Copy link
Copy Markdown
Contributor Author

obviyus commented Mar 7, 2026

Landed via temp rebase onto main.

  • Gate: pnpm tsgo && pnpm test --run src/daemon/systemd.test.ts src/cli/daemon-cli/install.integration.test.ts
  • Land commit: 38f618c34af5b263996d95cc6330f864db90e2f2
  • Merge commit: 26c9796

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27bb16a5c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +427 to +430
await fs.access(resolveSystemdUnitPath(env));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip unit-path precheck when running under sudo

execSystemctlUser explicitly supports sudo by targeting SUDO_USER via --machine <user>@ --user, but this new existence check uses resolveSystemdUnitPath(env) (derived from env.HOME) and returns false on ENOENT before systemctl is called. In a common sudo environment where HOME=/root and SUDO_USER is the real account, an already-enabled user service is now misreported as disabled, which can send install/status flows down the wrong path. The precheck should either resolve the managed unit path for the effective systemctl target user or be skipped when machine-scope sudo fallback is in play.

Useful? React with 👍 / 👎.

mcaxtr pushed a commit to mcaxtr/openclaw that referenced this pull request Mar 7, 2026
vincentkoc pushed a commit to BryanTegomoh/openclaw-fork that referenced this pull request Mar 8, 2026
Saitop pushed a commit to NomiciAI/openclaw that referenced this pull request Mar 8, 2026
GordonSH-oss pushed a commit to GordonSH-oss/openclaw that referenced this pull request Mar 9, 2026
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime maintainer Maintainer-authored PR size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant