Last active
May 31, 2026 12:28
-
-
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 | |
| # WINE_PROTON_ROOTS="/opt/proton:/games/HeroicTools/proton" ./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")}" | |
| prefix="${WINEPREFIX:-$HOME/.wine}" | |
| stripper="$HOME/.local/bin/wine-strip-host-drives" | |
| wrapper_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) | |
| resolve_real_cmd() { | |
| local name=$1 | |
| local dir candidate resolved_dir | |
| if [ -n "${WINE_NO_HOST_DRIVES_REAL_DIR:-}" ]; then | |
| candidate="${WINE_NO_HOST_DRIVES_REAL_DIR%/}/$name" | |
| if [ -x "$candidate" ]; then | |
| printf '%s\n' "$candidate" | |
| return 0 | |
| fi | |
| fi | |
| local search_path="${PATH:-}:/usr/bin:/usr/local/bin:/usr/sbin:/bin" | |
| local old_ifs=$IFS | |
| IFS=: | |
| for dir in $search_path; do | |
| IFS=$old_ifs | |
| [ -n "$dir" ] || dir=. | |
| resolved_dir=$(CDPATH= cd -- "$dir" 2>/dev/null && pwd -P) || continue | |
| [ "$resolved_dir" = "$wrapper_dir" ] && continue | |
| candidate="$resolved_dir/$name" | |
| if [ -x "$candidate" ]; then | |
| printf '%s\n' "$candidate" | |
| return 0 | |
| fi | |
| IFS=: | |
| done | |
| IFS=$old_ifs | |
| return 1 | |
| } | |
| real=$(resolve_real_cmd "$cmd" || true) | |
| if [ -z "$real" ] && [ "$cmd" = "wine64" ]; then | |
| real=$(resolve_real_cmd wine || true) | |
| fi | |
| if [ ! -x "$real" ]; then | |
| echo "missing real Wine command for: $cmd" >&2 | |
| exit 127 | |
| fi | |
| real_dir=$(dirname -- "$real") | |
| 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 | |
| local extra_roots=() | |
| local proton_roots=( | |
| "$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" \ | |
| "$game_root/HeroicTools/proton" \ | |
| "$game_root/Steam/compatibilitytools.d" | |
| ) | |
| if [ -n "${WINE_PROTON_ROOTS:-}" ]; then | |
| IFS=: read -r -a extra_roots <<< "$WINE_PROTON_ROOTS" | |
| proton_roots+=("${extra_roots[@]}") | |
| fi | |
| for root in "${proton_roots[@]}"; do | |
| [ -n "$root" ] || continue | |
| [ -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 re | |
| import sys | |
| z_drive_block = re.compile( | |
| r'(?m)^(?P<indent>[ \t]*)if not file_exists\(self\.prefix_dir \+ "/dosdevices/z:", follow_symlinks=False\):\n' | |
| r'(?:(?P=indent)[ \t]+os\.makedirs\(f"\{self\.prefix_dir\}/dosdevices", exist_ok=True\)\n)?' | |
| r'(?P=indent)[ \t]+os\.symlink\("/", self\.prefix_dir \+ "/dosdevices/z:"\)\n' | |
| ) | |
| def replace_z_drive_block(match): | |
| return f'{match.group("indent")}# Do not expose the host root as Z: in newly created prefixes.\n' | |
| 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 = z_drive_block.sub(replace_z_drive_block, updated) | |
| 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: | |
| backup = path.with_name(path.name + ".no-host-drives.bak") | |
| if not backup.exists(): | |
| backup.write_text(text) | |
| print(f"backed up {path} to {backup}") | |
| 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)