Last active
May 7, 2026 17:41
-
-
Save HackingGate/dc0da5c691e489080e71508814cae5e5 to your computer and use it in GitHub Desktop.
Remove Wine/Proton host filesystem drive mappings and prevent new Z: mappings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Remove Wine/Proton host filesystem drive mappings and prevent new Z: mappings. | |
| # | |
| # Usage: | |
| # ./disable-wine-host-drives.sh | |
| # ./disable-wine-host-drives.sh --home /home/someuser | |
| # | |
| # Optional: | |
| # DRY_RUN=1 ./disable-wine-host-drives.sh | |
| # WINE_GAME_ROOT=/games ./disable-wine-host-drives.sh | |
| set -euo pipefail | |
| target_home="${TARGET_HOME:-$HOME}" | |
| while [ "$#" -gt 0 ]; do | |
| case "$1" in | |
| --home) | |
| target_home="${2:?missing value for --home}" | |
| shift 2 | |
| ;; | |
| -h|--help) | |
| sed -n '1,12p' "$0" | |
| exit 0 | |
| ;; | |
| *) | |
| echo "unknown argument: $1" >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| done | |
| dry_run="${DRY_RUN:-0}" | |
| bin_dir="$target_home/.local/bin" | |
| game_root="${WINE_GAME_ROOT:-/games}" | |
| run() { | |
| if [ "$dry_run" = 1 ]; then | |
| printf 'DRY_RUN:' | |
| printf ' %q' "$@" | |
| printf '\n' | |
| else | |
| "$@" | |
| fi | |
| } | |
| write_file() { | |
| local path=$1 | |
| local tmp | |
| tmp=$(mktemp) | |
| cat > "$tmp" | |
| if [ "$dry_run" = 1 ]; then | |
| echo "DRY_RUN: write $path" | |
| rm -f "$tmp" | |
| else | |
| install -m 755 "$tmp" "$path" | |
| rm -f "$tmp" | |
| fi | |
| } | |
| is_allowed_target() { | |
| local target=$1 | |
| local resolved_game_root | |
| local resolved_target | |
| resolved_game_root=$(readlink -f "$game_root" 2>/dev/null || printf '%s\n' "$game_root") | |
| resolved_target=$(readlink -f "$target" 2>/dev/null || printf '%s\n' "$target") | |
| case "$resolved_target" in | |
| "$resolved_game_root"|"$resolved_game_root"/*) return 0 ;; | |
| esac | |
| return 1 | |
| } | |
| strip_host_drives() { | |
| local roots=("$@") | |
| local root | |
| [ "$#" -gt 0 ] || roots=("$target_home") | |
| for root in "${roots[@]}"; do | |
| [ -e "$root" ] || continue | |
| find "$root" -name dosdevices -type d -print0 2>/dev/null | | |
| while IFS= read -r -d '' dir; do | |
| for link in "$dir"/*; do | |
| [ -e "$link" ] || [ -L "$link" ] || continue | |
| [ -L "$link" ] || continue | |
| name=$(basename "$link") | |
| target=$(readlink "$link") | |
| case "$name" in | |
| c:|lpt*|com*) continue ;; | |
| esac | |
| case "$target" in | |
| /dev/sr*|/dev/lp*|/dev/ttyS*) continue ;; | |
| esac | |
| case "$target" in | |
| /*) target_path=$target ;; | |
| *) target_path=$dir/$target ;; | |
| esac | |
| if [ ! -e "$target_path" ]; then | |
| echo "remove $link -> $target (broken)" | |
| if [ "$dry_run" != 1 ]; then | |
| rm -- "$link" | |
| fi | |
| continue | |
| fi | |
| is_allowed_target "$target_path" && continue | |
| echo "remove $link -> $target" | |
| if [ "$dry_run" != 1 ]; then | |
| rm -- "$link" | |
| fi | |
| done | |
| done || true | |
| done | |
| return 0 | |
| } | |
| install_wrappers() { | |
| run install -d -m 755 "$bin_dir" | |
| write_file "$bin_dir/wine-strip-host-drives" <<'EOF' | |
| #!/usr/bin/env bash | |
| # Remove Wine/Proton dosdevices symlinks that expose the host filesystem or raw block devices. | |
| set -euo pipefail | |
| game_root="${WINE_GAME_ROOT:-/games}" | |
| is_allowed() { | |
| local target=$1 | |
| local resolved_game_root | |
| local resolved_target | |
| resolved_game_root=$(readlink -f "$game_root" 2>/dev/null || printf '%s\n' "$game_root") | |
| resolved_target=$(readlink -f "$target" 2>/dev/null || printf '%s\n' "$target") | |
| case "$resolved_target" in | |
| "$resolved_game_root"|"$resolved_game_root"/*) return 0 ;; | |
| esac | |
| return 1 | |
| } | |
| roots=("$@") | |
| [ "$#" -gt 0 ] || roots=("$HOME") | |
| for root in "${roots[@]}"; do | |
| [ -e "$root" ] || continue | |
| find "$root" -name dosdevices -type d -print0 2>/dev/null | | |
| while IFS= read -r -d '' dir; do | |
| for link in "$dir"/*; do | |
| [ -e "$link" ] || [ -L "$link" ] || continue | |
| [ -L "$link" ] || continue | |
| name=$(basename "$link") | |
| target=$(readlink "$link") | |
| case "$name" in | |
| c:|lpt*|com*) continue ;; | |
| esac | |
| case "$target" in | |
| /dev/sr*|/dev/lp*|/dev/ttyS*) continue ;; | |
| esac | |
| case "$target" in | |
| /*) target_path=$target ;; | |
| *) target_path=$dir/$target ;; | |
| esac | |
| if [ ! -e "$target_path" ]; then | |
| echo "remove $link -> $target (broken)" | |
| if [ "${DRY_RUN:-0}" != 1 ]; then | |
| rm -- "$link" | |
| fi | |
| continue | |
| fi | |
| is_allowed "$target_path" && continue | |
| echo "remove $link -> $target" | |
| if [ "${DRY_RUN:-0}" != 1 ]; then | |
| rm -- "$link" | |
| fi | |
| done | |
| done || true | |
| done | |
| exit 0 | |
| EOF | |
| write_file "$bin_dir/wine-no-host-drives-wrapper" <<'EOF' | |
| #!/usr/bin/env bash | |
| # Run Wine through the system binary, but keep host filesystem drives out of prefixes. | |
| set -u | |
| cmd="${WINE_NO_HOST_DRIVES_CMD:-$(basename "$0")}" | |
| real_dir="${WINE_NO_HOST_DRIVES_REAL_DIR:-/usr/sbin}" | |
| real="$real_dir/$cmd" | |
| prefix="${WINEPREFIX:-$HOME/.wine}" | |
| stripper="$HOME/.local/bin/wine-strip-host-drives" | |
| if [ ! -x "$real" ] && [ "$cmd" = "wine64" ]; then | |
| real="$real_dir/wine" | |
| fi | |
| if [ ! -x "$real" ]; then | |
| echo "missing real Wine command: $real" >&2 | |
| exit 127 | |
| fi | |
| strip_prefix() { | |
| [ -x "$stripper" ] || return 0 | |
| [ -d "$prefix/dosdevices" ] || return 0 | |
| "$stripper" "$prefix" >/dev/null || true | |
| } | |
| case "$cmd" in | |
| wine|wine64|winecfg) | |
| if [ ! -d "$prefix/dosdevices" ] && [ -x "$real_dir/wineboot" ]; then | |
| WINEPREFIX="$prefix" "$real_dir/wineboot" -u >/dev/null 2>&1 || true | |
| fi | |
| strip_prefix | |
| ;; | |
| esac | |
| "$real" "$@" | |
| status=$? | |
| strip_prefix | |
| exit "$status" | |
| EOF | |
| local cmd | |
| for cmd in wine wine64 wineboot winecfg wineserver; do | |
| write_file "$bin_dir/$cmd" <<EOF | |
| #!/usr/bin/env bash | |
| WINE_NO_HOST_DRIVES_CMD=$cmd exec "\$HOME/.local/bin/wine-no-host-drives-wrapper" "\$@" | |
| EOF | |
| done | |
| } | |
| patch_proton_files() { | |
| command -v python3 >/dev/null || { | |
| echo "python3 is required to patch Proton launcher files" >&2 | |
| return 1 | |
| } | |
| local candidates=() | |
| local root | |
| for root in \ | |
| "$target_home/.local/share/Steam/compatibilitytools.d" \ | |
| "$target_home/.local/share/Steam/steamapps/common" \ | |
| "$target_home/.local/share/umu/compatibilitytools" \ | |
| "$target_home/.config/heroic/tools/proton" | |
| do | |
| [ -d "$root" ] || continue | |
| while IFS= read -r -d '' file; do | |
| candidates+=("$file") | |
| done < <(find "$root" -type f \( -name proton -o -name proton_3.7_tracked_files \) -print0 2>/dev/null) | |
| done | |
| [ "${#candidates[@]}" -gt 0 ] || return 0 | |
| if [ "$dry_run" = 1 ]; then | |
| printf 'DRY_RUN: patch Proton files:\n' | |
| printf ' %s\n' "${candidates[@]}" | |
| return 0 | |
| fi | |
| python3 - "${candidates[@]}" <<'PY' | |
| from pathlib import Path | |
| import sys | |
| old = ''' if not file_exists(self.prefix_dir + "/dosdevices/z:", follow_symlinks=False): | |
| os.symlink("/", self.prefix_dir + "/dosdevices/z:") | |
| ''' | |
| new = ''' # Do not expose the host root as Z: in newly created prefixes. | |
| ''' | |
| strip_func = ''' | |
| def remove_host_dosdevices_for_launcher(): | |
| """Remove Wine/Proton drive mappings that expose host filesystems.""" | |
| stripper = os.path.expanduser("~/.local/bin/wine-strip-host-drives") | |
| if file_exists(stripper, follow_symlinks=True) and os.access(stripper, os.X_OK): | |
| subprocess.run( | |
| [stripper, g_compatdata.prefix_dir], | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| check=False, | |
| ) | |
| ''' | |
| strip_func_anchor = "\ndef comma_escaped(s):\n" | |
| launch_anchor = ''' #determine mode | |
| rc = 0 | |
| ''' | |
| launch_replacement = ''' remove_host_dosdevices_for_launcher() | |
| #determine mode | |
| rc = 0 | |
| ''' | |
| exit_anchor = ''' sys.exit(rc) | |
| ''' | |
| exit_replacement = ''' remove_host_dosdevices_for_launcher() | |
| sys.exit(rc) | |
| ''' | |
| for name in sys.argv[1:]: | |
| path = Path(name) | |
| try: | |
| text = path.read_text() | |
| except UnicodeDecodeError: | |
| continue | |
| updated = text | |
| if path.name == "proton": | |
| updated = updated.replace(old, new) | |
| if "def remove_host_dosdevices_for_launcher():" not in updated: | |
| updated = updated.replace(strip_func_anchor, "\n" + strip_func + "def comma_escaped(s):\n") | |
| if " remove_host_dosdevices_for_launcher()\n\n #determine mode" not in updated: | |
| updated = updated.replace(launch_anchor, launch_replacement) | |
| if " remove_host_dosdevices_for_launcher()\n sys.exit(rc)" not in updated: | |
| updated = updated.replace(exit_anchor, exit_replacement, 1) | |
| elif path.name == "proton_3.7_tracked_files": | |
| lines = [line for line in updated.splitlines() if line != "./dosdevices/z:"] | |
| updated = "\n".join(lines) + ("\n" if lines else "") | |
| if updated != text: | |
| path.write_text(updated) | |
| print(f"patched {path}") | |
| PY | |
| } | |
| cleanup_prefixes() { | |
| strip_host_drives "$target_home" "$game_root" | |
| } | |
| main() { | |
| install_wrappers | |
| patch_proton_files | |
| cleanup_prefixes | |
| } | |
| main |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
bash <(curl -fsSL https://gist.githubusercontent.com/HackingGate/dc0da5c691e489080e71508814cae5e5/raw/disable-wine-host-drives.sh)