diff --git a/scripts/docker/setup.sh b/scripts/docker/setup.sh index 5b8ca2001fed..3f35fc0adce8 100755 --- a/scripts/docker/setup.sh +++ b/scripts/docker/setup.sh @@ -792,13 +792,19 @@ echo "==> Fixing data-directory permissions" # Use -xdev to restrict chown to the config-dir mount only — without it, # the recursive chown would cross into the workspace bind mount and rewrite # ownership of all user project files on Linux hosts. +# Run a no-dereference chown from each entry's directory. This keeps ownership +# repair for sockets/FIFOs while preventing a swapped symlink leaf from +# redirecting the root operation outside the mounted tree. # After fixing the config dir, only the OpenClaw metadata subdirectory # (.openclaw/) inside the workspace gets chowned, not the user's project files. run_prestart_gateway --user root --entrypoint sh openclaw-gateway -c \ - 'find /home/node/.openclaw -xdev -exec chown node:node {} +; \ - chown node:node /home/node/.config; \ - find /home/node/.config/openclaw -xdev -exec chown node:node {} +; \ - [ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true' + 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH; \ + /usr/bin/find -P /home/node/.openclaw -xdev -execdir /usr/bin/chown -h node:node {} +; \ + /usr/bin/chown -h node:node /home/node/.config; \ + /usr/bin/find -P /home/node/.config/openclaw -xdev -execdir /usr/bin/chown -h node:node {} +; \ + if [ -d /home/node/.openclaw/workspace/.openclaw ] && [ ! -L /home/node/.openclaw/workspace/.openclaw ]; then \ + /usr/bin/find -P /home/node/.openclaw/workspace/.openclaw -xdev -execdir /usr/bin/chown -h node:node {} +; \ + fi || true' echo "" if [[ -n "$SKIP_ONBOARDING" ]]; then diff --git a/src/docker-setup.e2e.test.ts b/src/docker-setup.e2e.test.ts index 23525db7439c..20d66ac652e3 100644 --- a/src/docker-setup.e2e.test.ts +++ b/src/docker-setup.e2e.test.ts @@ -179,6 +179,10 @@ const prestartContainerEnvFlags = [ "-e OPENCLAW_WORKSPACE_DIR=/home/node/.openclaw/workspace", ].join(" "); +const noFollowOwnershipRepair = (root: string) => + `/usr/bin/find -P ${root} -xdev -execdir /usr/bin/chown -h node:node {} +`; +const prestartSafePath = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; + function createEnv( sandbox: DockerSetupSandbox, overrides: Record = {}, @@ -847,11 +851,24 @@ describe("scripts/docker/setup.sh", () => { // Verify that a root-user chown step runs before setup. const log = await readDockerLog(activeSandbox); const chownIdx = log.indexOf("--user root"); + const safePathIdx = log.indexOf(`${prestartSafePath}; export PATH`); + const stateRepairIdx = log.indexOf(noFollowOwnershipRepair("/home/node/.openclaw")); const onboardIdx = log.indexOf("onboard"); expect(chownIdx).toBeGreaterThanOrEqual(0); + expect(safePathIdx).toBeGreaterThan(chownIdx); + expect(stateRepairIdx).toBeGreaterThan(safePathIdx); expect(onboardIdx).toBeGreaterThan(chownIdx); expect(log).toContain("run --rm --no-deps --user root --entrypoint sh openclaw-gateway -c"); - expect(log).toContain("chown node:node /home/node/.config"); + expect(log).toContain("/usr/bin/chown -h node:node /home/node/.config"); + expect(log).toContain(noFollowOwnershipRepair("/home/node/.openclaw")); + expect(log).toContain(noFollowOwnershipRepair("/home/node/.config/openclaw")); + expect(log).toContain("[ ! -L /home/node/.openclaw/workspace/.openclaw ]"); + expect(log).toContain(noFollowOwnershipRepair("/home/node/.openclaw/workspace/.openclaw")); + expect(log).toContain("fi || true"); + expect(log).not.toContain("-type d -o -type f"); + expect(log).not.toContain("-exec chown"); + expect(log).not.toContain(" chown node:node"); + expect(log).not.toContain("chown -R node:node /home/node/.openclaw/workspace/.openclaw"); }); it("precreates auth profile secret key dir outside the mounted state dir", async () => { @@ -872,7 +889,7 @@ describe("scripts/docker/setup.sh", () => { expect(secretDir.startsWith(`${configDir}/`)).toBe(false); const log = await readDockerLog(activeSandbox); - expect(log).toContain("find /home/node/.config/openclaw -xdev"); + expect(log).toContain(noFollowOwnershipRepair("/home/node/.config/openclaw")); }); it("reuses existing config token when OPENCLAW_GATEWAY_TOKEN is unset", async () => {