Skip to content

Commit b038772

Browse files
committed
Create Wine wrapper dir more robustly
Create Wine wrapper scripts under either `$XDG_RUNTIME_DIR` and `$XDG_CACHE_DIR`, depending on which location permits executables. `$XDG_RUNTIME_DIR` is preferred if available. Prior to this, we always created the directory for the Wine wrapper scripts under `$XDG_CACHE_DIR`. This can be problematic as sometimes the mount point has the `noexec` mount flag set, causing any Wine calls to crash later. Note that we also need to set sticky bits for files under `$XDG_RUNTIME_DIR` to avoid automatic cleanup per XDG spec. A quick search indicates no distro implements this cleanup yet, though. There also doesn't seem to be any harm unnecessarily setting this sticky bit for files residing elsewhere, so just set it everywhere for simplicity's sake. Fixes #307
1 parent 4cf64d3 commit b038772

File tree

4 files changed

+78
-38
lines changed

4 files changed

+78
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1010

1111
### Changed
1212
- `protontricks -c` and `protontricks-launch` now use the current working directory instead of the game's installation directory. `--cwd-app` can be used to restore old behavior. Scripts can also `$STEAM_APP_PATH` environment variable to determine the game's installation directory; this has been supported (albeit undocumented) since 1.8.0.
13+
- Protontricks will now prefer `XDG_RUNTIME_DIR` for storing Wine wrapper script and fall back to `XDG_CACHE_DIR` if it's not set
14+
15+
### Fixed
16+
- Fix Wine crash due to Protontricks not properly checking for execute permission
1317

1418
## [1.11.1] - 2024-02-20
1519
### Fixed

src/protontricks/data/scripts/bwrap_launcher.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ BLACKLISTED_ROOT_DIRS=(
3535
)
3636

3737
ADDITIONAL_MOUNT_DIRS=(
38-
/run/media "$PROTON_PATH" "$WINEPREFIX"
38+
/run/media "$PROTON_PATH" "$WINEPREFIX" "$PROTONTRICKS_PROXY_DIR"
3939
)
4040

4141
mount_dirs=()

src/protontricks/util.py

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"is_steam_deck", "get_legacy_runtime_library_paths",
1616
"get_host_library_paths", "RUNTIME_ROOT_GLOB_PATTERNS",
1717
"get_runtime_library_paths", "WINE_SCRIPT_TEMPLATE",
18-
"get_cache_dir", "create_wine_bin_dir", "run_command"
18+
"get_cache_dir", "get_script_dir", "create_wine_bin_dir", "run_command"
1919
)
2020

2121
logger = logging.getLogger("protontricks")
@@ -176,58 +176,75 @@ def find_runtime_app_root(runtime_app):
176176
).read_text(encoding="utf-8")
177177

178178

179-
def get_cache_dir():
179+
def is_dir_exec(path):
180180
"""
181-
Get Protontricks' cache directory, creating it first if it does not
182-
exist
181+
Check if the directory is under a mount that allows execute permission.
182+
183+
Checking for execute permission on the file itself does not suffice as
184+
the file system can forbid executables using 'noexec' mount flag.
183185
"""
184-
def is_dir_exec(path):
185-
"""
186-
Check if the directory is under a mount that allows execute permission
187-
"""
188-
path_ = path
189-
try:
190-
while not path_.is_mount():
191-
path_ = path_.parent
186+
path_ = path
187+
while not path_.is_mount():
188+
path_ = path_.parent
192189

193-
if str(path_) == "/":
194-
raise OSError("Did not find mount point!")
190+
if str(path_) == "/":
191+
raise OSError("Did not find mount point!")
195192

196-
# Parse mount point information
197-
mount_data = Path("/proc/mounts").read_text()
193+
# Parse mount point information
194+
mount_data = Path("/proc/mounts").read_text()
198195

199-
for line in mount_data.split("\n"):
200-
_, mount_path, _, mount_flags, *_ = line.split(" ")
196+
for line in mount_data.split("\n"):
197+
_, mount_path, _, mount_flags, *_ = line.split(" ")
201198

202-
if mount_path == str(path_):
203-
mount_flags = mount_flags.split(",")
199+
if mount_path == str(path_):
200+
mount_flags = mount_flags.split(",")
204201

205-
# If mount has 'noexec' flag, the shell executables won't work
206-
return "noexec" not in mount_flags
202+
# If mount has 'noexec' flag, the shell executables won't work
203+
return "noexec" not in mount_flags
207204

208-
logger.warning(f"Could not find mount point for {path}")
209-
return True
210-
except OSError as exc:
211-
print(f"shit {exc}")
205+
logger.warning(
206+
f"Could not find mount point for {path}. Assuming directory "
207+
"has execute permissions."
208+
)
209+
return True
212210

211+
212+
def get_cache_dir():
213+
"""
214+
Get Protontricks' cache directory, creating it first if it does not
215+
exist
216+
"""
217+
xdg_cache_dir = os.environ.get(
218+
"XDG_CACHE_HOME", os.path.expanduser("~/.cache")
219+
)
220+
base_path = Path(xdg_cache_dir) / "protontricks"
221+
os.makedirs(str(base_path), exist_ok=True)
222+
223+
return base_path
224+
225+
226+
def get_script_dir():
227+
"""
228+
Get Protontricks' Wine wrapper script directory, creating it first
229+
if it does not exist
230+
"""
213231
candidates = []
214232

215233
if os.environ.get("XDG_RUNTIME_DIR"):
216234
candidates.append(
217-
Path(os.environ["XDG_RUNTIME_DIR"]) / "protontricks"
235+
Path(os.environ["XDG_RUNTIME_DIR"]) / "protontricks" / "proton"
218236
)
219237

220-
xdg_cache_dir = os.environ.get(
221-
"XDG_CACHE_HOME", os.path.expanduser("~/.cache")
222-
)
223-
candidates.append(Path(xdg_cache_dir) / "protontricks")
238+
candidates.append(get_cache_dir() / "proton")
224239

225240
logger.debug(
226241
f"Candidates for Wine wrapper script directories: {candidates}"
227242
)
228243

229244
for candidate in candidates:
230245
if is_dir_exec(candidate):
246+
candidate.mkdir(exist_ok=True, parents=True)
247+
231248
logger.info(f"Using {candidate} as Wine wrapper directory")
232249
return candidate
233250

@@ -246,7 +263,7 @@ def create_wine_bin_dir(proton_app, use_bwrap=True):
246263
binaries = list((proton_app.proton_dist_path / "bin").iterdir())
247264

248265
# Create the base directory containing files for every Proton installation
249-
base_path = get_cache_dir() / "proton"
266+
base_path = get_script_dir()
250267
os.makedirs(str(base_path), exist_ok=True)
251268

252269
# Create a directory to hold the new executables for the specific
@@ -277,8 +294,18 @@ def create_wine_bin_dir(proton_app, use_bwrap=True):
277294
proxy_script_path.write_text(content, encoding="utf-8")
278295

279296
script_stat = proxy_script_path.stat()
280-
# Make the helper script executable
281-
proxy_script_path.chmod(script_stat.st_mode | stat.S_IEXEC)
297+
298+
# Make the helper script executable.
299+
#
300+
# Also set the sticky bit in case the file resides under
301+
# `XDG_RUNTIME_DIR`, as automatic cleanup might take place otherwise.
302+
# No distro seems to implement this at the moment, though. Sticky bit
303+
# on files doesn't seem to have any other effect on modern systems,
304+
# so it's no problem even if the proxy script resides under
305+
# `XDG_CACHE_DIR` instead.
306+
proxy_script_path.chmod(
307+
script_stat.st_mode | stat.S_IEXEC | stat.S_ISVTX
308+
)
282309

283310
# Create the wineserver keepalive batch script
284311
(bin_path / "wineserver-keepalive.bat").write_text(
@@ -292,11 +319,18 @@ def create_wine_bin_dir(proton_app, use_bwrap=True):
292319
)
293320
)
294321
keepalive_shell_script.chmod(
295-
keepalive_shell_script.stat().st_mode | stat.S_IEXEC
322+
keepalive_shell_script.stat().st_mode | stat.S_IEXEC | stat.S_ISVTX
296323
)
324+
297325
launcher_script = bin_path / "bwrap-launcher"
298-
launcher_script.write_text(BWRAP_LAUNCHER_SH_SCRIPT)
299-
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IEXEC)
326+
launcher_script_data = BWRAP_LAUNCHER_SH_SCRIPT
327+
launcher_script_data = launcher_script_data.replace(
328+
"@@script_path@@", str(proxy_script_path)
329+
)
330+
launcher_script.write_text(launcher_script_data)
331+
launcher_script.chmod(
332+
launcher_script.stat().st_mode | stat.S_IEXEC | stat.S_ISVTX
333+
)
300334

301335
return bin_path
302336

@@ -501,6 +535,7 @@ def run_command(
501535
# configuring the environment and Wine before launching the underlying
502536
# Wine binaries.
503537
wine_bin_dir = create_wine_bin_dir(proton_app)
538+
wine_environ["PROTONTRICKS_PROXY_DIR"] = str(wine_bin_dir)
504539
wine_environ["LEGACY_STEAM_RUNTIME_PATH"] = str(legacy_steam_runtime_path)
505540
wine_environ["PATH"] = os.pathsep.join(
506541
[str(wine_bin_dir), wine_environ["PATH"]]

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def env_vars(monkeypatch):
3030
"""
3131
monkeypatch.setenv("STEAM_RUNTIME", "")
3232
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
33+
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
3334

3435

3536
@pytest.fixture(scope="function", autouse=True)

0 commit comments

Comments
 (0)