fix: check managed systemd unit before is-enabled#38819
Conversation
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🔵 Untrusted PATH search when invoking
|
| 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()callsexecFileUtf8("systemctl", ...).execFiledoes not use a shell (good), but it does resolve the executable using the current processPATH.- In multiple flows (including
isSystemdServiceEnabled), this can run while the process is invoked undersudo(the code explicitly detectsSUDO_USERand changes behavior). If a privileged invocation occurs with an attacker-influencedPATH(e.g.,sudo -E, misconfiguredsecure_path, or a service/unit that setsPATHto include a user-writable directory), an attacker can place a malicioussystemctlearlier inPATHand 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):
- 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",
},
});- Provide a hardened environment to
execFile(at minimum, set a safePATH) 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
27bb16a to
38f618c
Compare
Greptile SummaryThis PR fixes a fresh-install failure where Key changes:
Confidence Score: 4/5
Last reviewed commit: 38f618c |
|
Landed via temp rebase onto main.
|
There was a problem hiding this comment.
💡 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".
| await fs.access(resolveSystemdUnitPath(env)); | ||
| } catch (error) { | ||
| if ((error as NodeJS.ErrnoException).code === "ENOENT") { | ||
| return false; |
There was a problem hiding this comment.
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 👍 / 👎.
(cherry picked from commit 26c9796)
(cherry picked from commit 26c9796)
Summary
Supersedes #38551.
Fresh installs were calling
systemctl --user is-enabledbefore 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-enabledexit asfalse:~/.config/systemd/user/falsewithout callingsystemctlfalse, realsystemctlfailures still throwThat 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