Skip to content

Run LTspice as non-root wineuser, support --user=arbitrary uid - #9

Merged
aanas-sayed merged 9 commits into
mainfrom
feat/non-root-wineuser
May 9, 2026
Merged

Run LTspice as non-root wineuser, support --user=arbitrary uid#9
aanas-sayed merged 9 commits into
mainfrom
feat/non-root-wineuser

Conversation

@aanas-sayed

@aanas-sayed aanas-sayed commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary

Move LTspice + Wine off root inside the container so consumers can drop --cap-add=DAC_OVERRIDE and use --user=$(id -u):$(id -g) like a normal sandboxed simulator.

  • Image now runs as wineuser (uid 1000) by default. Works under docker run --user=$(id -u):$(id -g) --cap-drop=ALL for any host uid.
  • Wine prefix is shipped as a uid-agnostic template at /opt/wineprefix-template; entrypoint cp -as it to /tmp/wine-prefix on each container start so the copy is owned by the running uid (satisfies Wine's prefix-owner check without CAP_CHOWN).
  • LTspice's 1.7 GB install lives at /opt/ltspice with a symlink left in the prefix template — keeps the per-run copy small (~150 MB).
  • test.sh runs the hardened invocation (--user=$(id -u):$(id -g) --cap-drop=ALL) and verifies the produced .raw file is owned by the host uid.
  • README documents the sandboxed invocation; docs/wineprefix.md explains the prefix split.
  • New manual workflow macos-latest.yml publishes the Wine-9 image. Each run pushes two tags: an immutable devel-YYYYMMDD-wine-X.Y audit tag and a rolling macos-latest pointer (overrideable per run).

CI green on amd64-linux and arm64-linux. Wine version pinning per tag (latest = Wine latest stable, macos-latest = Wine 9) unchanged.

Test plan

  • CI: build + amd64-linux test + arm64-linux test all pass on this branch
  • .raw file produced under hardened invocation is owned by the host uid (asserted by test.sh)

Move WINEPREFIX from /root/.wine to /home/wineuser/.wine and run the
container as uid 1000 by default. The prefix tree is chmod a+rwX'd at
build time so callers can also pass --user=$(id -u):$(id -g) and drop
--cap-add=DAC_OVERRIDE on native Linux.

test.sh now runs the simulation twice — once with the image's default
user and once hardened (--user + --cap-drop=ALL) — and asserts the
output .raw file is owned by the host uid.
The previous "default user" variant ran the container without --user,
relying on the image's built-in uid 1000. On native Linux that fails
when the host uid differs (GH Actions runner is uid 1001) because the
in-container user can't write to the bind mount without DAC_OVERRIDE.

The hardened variant (--user=$(id -u):$(id -g) + --cap-drop=ALL) is
the supported consumption pattern and is the only one worth testing —
keeping the default variant required either chmod'ing the test dir or
testing a configuration nobody will use.
Wine refuses to use a prefix not owned by the running uid (hard exit:
"is not owned by you"). Without CAP_CHOWN we cannot fix the owner at
runtime, so chmod-only does not satisfy the check.

Restructure: the build-time prefix becomes a uid-agnostic template at
/opt/wineprefix-template, and LTspice's 1.7 GB install is moved to
/opt/ltspice with a relative symlink left behind so the registry
entries written by msiexec still resolve. The entrypoint cp -a's the
template into $WINEPREFIX (default /tmp/wine-prefix) on every fresh
container start; the copy inherits the current uid, satisfying Wine.
LOGNAME/USER are pinned to "wineuser" so getpwuid()-based lookups for
arbitrary uids without /etc/passwd entries do not surprise Wine.
cp -a --no-preserve=ownership preserves mode bits, so a 0444/0555
template chmod'd a+rX produced a per-uid prefix copy that the running
uid could not write to. Wine then hit "I/O error : Permission denied"
trying to update userdef.reg / registry timestamps. Switch the
template chmod to a+rwX so the copy is writable regardless of uid.
Three additional write paths were unreachable for arbitrary --user uids:
- /opt/ltspice was a+rX; LTspice writes a config XML into its install
  dir, which fails when the install is read-only.
- /home/wineuser was 0755 owned by uid 1000; the container's default
  CWD (and any temp files written there) was unwritable for any other
  uid.
- Build-time Xvfb left /tmp/.X11-unix and /tmp/.X99-lock in the image
  with uid-1000 ownership, blocking runtime Xvfb when running as a
  different uid. Clean those at end of build.
Document the new defaults: container runs as wineuser (uid 1000)
instead of root, supports --user=$(id -u):$(id -g) for any host uid,
and works with --cap-drop=ALL. Add a "sandboxed invocation" example
for the canonical hardened Linux flow, and troubleshooting notes for
the bind-mount permission case and the per-run prefix-copy cost.
@aanas-sayed
aanas-sayed merged commit 9c6ffef into main May 9, 2026
10 checks passed
@aanas-sayed
aanas-sayed deleted the feat/non-root-wineuser branch May 9, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant