Skip to content

Instantly share code, notes, and snippets.

@HackingGate
Last active May 7, 2026 17:41
Show Gist options
  • Select an option

  • Save HackingGate/dc0da5c691e489080e71508814cae5e5 to your computer and use it in GitHub Desktop.

Select an option

Save HackingGate/dc0da5c691e489080e71508814cae5e5 to your computer and use it in GitHub Desktop.
Remove Wine/Proton host filesystem drive mappings and prevent new Z: mappings.
#!/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
@HackingGate
Copy link
Copy Markdown
Author

bash <(curl -fsSL https://gist.githubusercontent.com/HackingGate/dc0da5c691e489080e71508814cae5e5/raw/disable-wine-host-drives.sh)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment