Skip to content

Improve wineserver discovery (fixes #208, alternative to #211)#212

Open
showier-drastic wants to merge 7 commits into
hartwork:mainfrom
showier-drastic:fix-wineserver-discovery
Open

Improve wineserver discovery (fixes #208, alternative to #211)#212
showier-drastic wants to merge 7 commits into
hartwork:mainfrom
showier-drastic:fix-wineserver-discovery

Conversation

@showier-drastic

@showier-drastic showier-drastic commented Jun 8, 2026

Copy link
Copy Markdown

Close #208. Rationales are detailed in the commit message. Co-authored by AI (with wine source code as reference), but verified by human, and also verified on Ubuntu 26.04.

sandwine wraps the sandboxed program with `wineserver -p0` / `wineserver -k`
for clean shutdown, invoking wineserver by bare name. That requires the
wineserver directory to be present on ${PATH} inside the sandbox.

To make this work, the sandbox ${PATH} was augmented with a single hardcoded
directory, "/usr/lib/wine". On Debian/Ubuntu, however, wineserver is installed
into a multiarch directory, e.g. "/usr/lib/x86_64-linux-gnu/wine/" on
Ubuntu 26.04. That directory is neither on the host ${PATH} nor equal to the
hardcoded path, so the bare `wineserver` call fails and clean shutdown breaks.

Replace the hardcoded path with find_wineserver(), which mirrors the resolution
order of Wine's own loader (dlls/ntdll/unix/loader.c:exec_wineserver):
${WINESERVER} -> ${PATH} -> the library directory next to the `wine` loader.
The library-directory case is made multiarch-aware via sysconfig's MULTIARCH,
so the correct location is discovered generically instead of being hardcoded
per distribution. The discovered directory still flows through the existing
mount-stack filter, so it is only added to ${PATH} if it exists in the sandbox.
This builds on the previous commit and replaces the ${PATH}-based mechanism
with a more robust one.

Rather than making the wineserver directory reachable by bare name on the
sandbox ${PATH}, resolve the wineserver binary on the host with
find_wineserver() and:

  - export it to the sandbox as ${WINESERVER},
  - bind-mount it read-only into the sandbox, and
  - invoke it by "$WINESERVER" in the clean-shutdown wrapper.

Why this is preferable:

  - Wine itself honors ${WINESERVER}, so both sandwine's own
    `wineserver -p0` / `wineserver -k` calls and Wine's internal server
    spawn use the exact same binary -- no divergence.
  - The binary is bind-mounted explicitly, so it is guaranteed to exist
    inside the sandbox even when it lives outside the default mount stack
    (e.g. a custom ${WINESERVER} pointing outside /usr). This does not
    rely on /usr happening to cover the install location, and the previous
    commit's ${PATH} append is therefore removed.
  - When wineserver cannot be found, sandwine now fails up front with a
    clear CommandNotFound("wineserver") instead of a cryptic
    "sh: wineserver: not found" from inside the sandbox.

The mount and ${WINESERVER} export are placed next to the existing
/opt/wine-* mounting logic so the bind mount is part of the mount stack
that is later filtered and emitted.
@showier-drastic

showier-drastic commented Jun 8, 2026

Copy link
Copy Markdown
Author

Interesting. Where is the wine binary in the CI image?

OK, maybe it's inside /usr/lib/wine. Guess we shouldn still add /usr/lib/wine (or whatever wineserver directory) to PATH?

@hartwork

hartwork commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Interesting. Where is the wine binary in the CI image?

OK, maybe it's inside /usr/lib/wine. Guess we shouldn still add /usr/lib/wine (or whatever wineserver directory) to PATH?

@showier-drastic I would need to debug it myself also. I'll finish review first.

(Note to self: Context was the failed CI run.)

@hartwork hartwork left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'm not sure yet this will work. Here's a few things I found and would like to clarify:

Comment thread sandwine/_main.py Outdated
Comment on lines +311 to +312
if env_wineserver and os.access(env_wineserver, os.X_OK) and os.path.isfile(env_wineserver):
return os.path.realpath(env_wineserver)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'll have to sleep over this, but I see a potential TOCTTOU vulnerablity here. Running os.path.realpath first and then checking access and file nature there change this picture (..). One more case like that below.

Comment thread sandwine/_main.py Outdated

def find_wineserver() -> str | None:
# Locate the wineserver binary the way Wine's own loader does
# (dlls/ntdll/unix/loader.c:exec_wineserver), so that distribution-specific

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The related code in Wine seems to have changed over time. Which version did you look at?

At https://gitlab.winehq.org/wine/wine/-/blob/master/dlls/ntdll/unix/loader.c?ref_type=heads#L515-518 I see order…

  1. [..]/wine
  2. ${WINESERVER}
  3. ${PATH}

…whereas in here I see:

  1. ${WINESERVER}
  2. ${PATH}
  3. [..]/wine
  4. [..]/bin

Would be important to me to better understand.

Comment thread sandwine/_main.py Outdated

multiarch = sysconfig.get_config_var("MULTIARCH") # e.g. "x86_64-linux-gnu" on Debian/Ubuntu
subdirs = [f"lib/{multiarch}/wine"] if multiarch else []
subdirs += ["lib/wine", "lib64/wine", "bin"] # plus BINDIR-style fallback

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Mixing 32bit and 64bit paths here seems like a candidate for trouble, in particular in this order which would be 32bit before 64bit on my 64bit box:

# file -L /usr/lib/libz.so /usr/lib64/libz.so | awk '{print $3, $1}'
32-bit /usr/lib/libz.so:
64-bit /usr/lib64/libz.so:

Comment thread sandwine/_main.py Outdated
Comment on lines +334 to +336
if candidate in seen:
continue
seen.add(candidate)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This mechanism seems to be needed only because subdirs is not checked for duplicates. What is the idea behind not dropping duplicates? E.g. something like prefixes = list(dict.fromkeys(prefixes)) # i.e. drop duplicates while keeping order upfront may work.

Comment thread sandwine/_main.py
wineserver_abs_path = find_wineserver()
if wineserver_abs_path is None:
raise CommandNotFound("wineserver")
env_tasks["WINESERVER"] = wineserver_abs_path

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

  • If we go forward with this, this new variable would need to be appended in the docs also here:

sandwine/README.md

Lines 146 to 153 in 9325f6e

### Environment Variables
- `${DISPLAY}`
- `${HOME}`
- `${HOSTNAME}` (with random 12-hex-digits value)
- `${PATH}` (with known-unavailable entries removed)
- `${TERM}`
- `${USER}`

  • If wine itself recognizes this variable, using a different name to not have tings collide in unexpected ways may be more robust. I'd need to sleep over that.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It is intentionally set to WINESERVER, so that wine can use exactly the same wine server to our code. I can add it to docs for sure.

Comment thread sandwine/_main.py Outdated
Comment thread sandwine/_main.py Outdated
if (wine_bin_abs_path := shutil.which("wine")) is not None:
# e.g. /usr/bin/wine -> /usr
prefixes.append(os.path.dirname(os.path.dirname(os.path.realpath(wine_bin_abs_path))))
prefixes += ["/usr", "/usr/local"]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'll need to sleep over bringing /usr/local in like this.

@hartwork hartwork changed the title Fix wineserver discovery Fix wineserver discovery (fixes #208, alternative to #211) Jun 9, 2026
@hartwork hartwork changed the title Fix wineserver discovery (fixes #208, alternative to #211) Improve wineserver discovery (fixes #208, alternative to #211) Jun 9, 2026
showier-drastic and others added 3 commits June 10, 2026 22:47
Co-authored-by: Sebastian Pipping <sebastian@pipping.org>
find_wineserver() validated the unresolved path with os.access()/
os.path.isfile() but returned os.path.realpath() of it. Those calls each
resolve symlinks independently, so a symlink swap in between could validate
one target while returning a different one to be bind-mounted into the
sandbox.

Add _resolve_executable_file(), which canonicalizes with realpath first and
then validates that resolved path, and use it for both the ${WINESERVER}
and library-directory cases.
find_wineserver() previously searched ${WINESERVER} -> ${PATH} -> bin_dir
and reverse-engineered bin_dir by guessing lib vs lib64, which on a 64-bit
multilib host (where /usr/lib is 32-bit and /usr/lib64 is 64-bit) could pick
a 32-bit wineserver. It also did not match the resolution *order* of wine
master's dlls/ntdll/unix/loader.c:exec_wineserver(), which for an installed
Wine is bin_dir -> ${WINESERVER} -> ${PATH} -> BINDIR.

Three changes:

  - Follow wine master's order: bin_dir, then ${WINESERVER}, then ${PATH},
    then a BINDIR (<prefix>/bin) fallback. The build-tree branches wine
    checks first do not apply to an installed Wine and are omitted. The order
    is version-sensitive, so the comment is pinned to master.

  - Stop guessing the bin_dir layout. Anchor on the 'wine' loader itself:
    os.path.realpath(shutil.which('wine')) follows symlinks (including
    /etc/alternatives/wine and winehq's /opt/wine-*/bin/wine) to the real
    loader, next to which wineserver is installed in essentially every
    packaging. The architecture-exact multiarch directory and a
    word-size-ordered lib/lib64 fallback remain only as backups (the latter
    now prefers the native word size, fixing the 32-bit-before-64-bit bug).

  - Keep the 'wine' loader reachable on the sandbox ${PATH}. wineserver is now
    invoked by its absolute ${WINESERVER} path, but 'wine' is still launched by
    bare name and lives next to wineserver (e.g. /usr/lib/wine/ on Ubuntu's
    wine32:i386, which ships no /usr/bin/wine). Re-add the discovered
    wineserver's directory to the candidate ${PATH} so the colocated 'wine'
    resolves again; the existing filter keeps it only if it exists in the
    sandbox mount stack.
@showier-drastic showier-drastic force-pushed the fix-wineserver-discovery branch from 171dcf7 to a7b7fae Compare June 10, 2026 15:51
@showier-drastic

Copy link
Copy Markdown
Author

Smoke test passed on my side for the latest commit: https://github.com/showier-drastic/sandwine/actions/runs/27288472359

However, it is still complex, and I am not totally satisfied. The main issue is that: wine prefers bin_dir over WINESERVER variable, and in most cases it will pick "bin_dir". This makes WINESERVER almost useless. Should we just remove it to reduce complexity?

@showier-drastic

Copy link
Copy Markdown
Author

If you have more issues, I would say please edit directly. I think I don't have sufficient knowledge to go further. Thanks!

@showier-drastic

Copy link
Copy Markdown
Author

Oh no, not my fault this time ;)

image

Comment thread sandwine/_main.py Outdated
if wine_loader is not None:
# e.g. /usr/bin/wine -> /usr
prefixes.append(os.path.dirname(os.path.dirname(wine_loader)))
prefixes += ["/usr", "/usr/local"]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@showier-drastic update: let's drop prefixes += ["/usr", "/usr/local"] and only use the parent's parent of wine_loader. That's bringing the right of the two in already if needed (e.g. on Debian) and does not add trouble where not. What do you think?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

PS: Looping over prefixes can also be fully resolved then, reducing complexity.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'm not sure about this, but I'll just trust you :)

Comment thread sandwine/_main.py Outdated
@hartwork

Copy link
Copy Markdown
Owner

Oh no, not my fault this time ;)
image

@showier-drastic restarted just now

showier-drastic and others added 2 commits June 11, 2026 00:13
Co-authored-by: Sebastian Pipping <sebastian@pipping.org>
Debian / Ubuntu systems will just find it through the wrapper.
Comment thread sandwine/_main.py
Comment on lines +350 to +353
prefixes = []
if wine_loader is not None:
# e.g. /usr/bin/wine -> /usr
prefixes.append(os.path.dirname(os.path.dirname(wine_loader)))

@hartwork hartwork Jun 10, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@showier-drastic can we resolve prefixes and the looping please?

Suggested change
prefixes = []
if wine_loader is not None:
# e.g. /usr/bin/wine -> /usr
prefixes.append(os.path.dirname(os.path.dirname(wine_loader)))
# e.g. /usr/bin/wine -> /usr
wine_prefix = os.path.dirname(os.path.dirname(wine_loader))

@showier-drastic

Copy link
Copy Markdown
Author

Oh no, removing /usr and /usr/local broke it. Could you please debug it more?

@showier-drastic

Copy link
Copy Markdown
Author

I've allowed "Allow edits and access to secrets by maintainers". You may directly push to my branch.

@hartwork

Copy link
Copy Markdown
Owner

If you have more issues, I would say please edit directly. I think I don't have sufficient knowledge to go further. Thanks!

@showier-drastic thanks but I believe that Git branches should only be written to by the author of that branch. I have created an alternative pull request #213 that also fixes the problem at arguably lower complexity. I'm good with you being in the review seat there or you you being the driver here — which do you prefer? Thanks!

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.

wineserver is not at path by default in Ubuntu 26.04

2 participants