Skip to content

Instantly share code, notes, and snippets.

@thenets
Created August 8, 2026 06:32
Show Gist options
  • Select an option

  • Save thenets/d765245ec24dee2f837c0a3ff3876087 to your computer and use it in GitHub Desktop.

Select an option

Save thenets/d765245ec24dee2f837c0a3ff3876087 to your computer and use it in GitHub Desktop.
SteamOS installer fix: NVMe sanitize fails with 0x4286 Access Denied (repair_device.sh)

SteamOS installer fails with NVMe 0x4286 Access Denied

Installing SteamOS on a desktop PC from the Steam Deck recovery image, the very first step of the installer dies:

(A)(USB SanDisk 3.2Gen1)(root@steamdeck deck)# /home/deck/tools/repair_device.sh all

Warning!

This action irrevocably clears *all* user data from /dev/nvme0n1
Pausing five seconds in case you didn't mean to do this...
Ok, let's go. Sanitizing /dev/nvme0n1:
NVMe status: Access Denied: Access to the namespace and/or LBA range is denied due to lack of access rights(0x4286)
(1)(A)(USB SanDisk 3.2Gen1)(root@steamdeck deck)# id
uid=0(root) gid=0(root) groups=0(root)

You are already uid=0. Adding more privilege will not help.

What is actually happening

0x4286 is not a Linux permission error. It is a status code returned by the SSD controller itself, in reply to the NVMe Sanitize command that repair_device.sh issues as its first action:

nvme sanitize -a 2 "${DISK}"

The drive is refusing the command. There are two distinct reasons a drive does that, and they need different responses:

  1. The drive does not implement Sanitize at all. This is the common case, and it affects a lot of retail and OEM NVMe drives (the WD Blue SN550 is the one in the upstream bug report). The installer never checks whether the command is supported before issuing it.

  2. The namespace is locked or write-protected. TCG OPAL / BitLocker eDrive hardware encryption from a previous Windows install, a vendor namespace write-protect bit, or a self-encrypting drive that was never unlocked. Here the drive has Sanitize, but will not let you touch the media.

The controller capability field SANICAP tells you which one you have:

nvme id-ctrl /dev/nvme0 | grep -i sanicap

All-zero means case 1. Non-zero means case 2.

The fix for case 1: skip sanitize

Sanitize is a secure-erase operation. It exists so that old data is cryptographically unrecoverable, which matters when you are disposing of a drive. It is not what makes the install work: repair_device.sh repartitions and reformats the disk immediately afterwards regardless.

So for a fresh install on a drive you are keeping, sanitize can be replaced with a plain wipe that destroys the things the installer actually cares about:

  • wipefs -a removes filesystem, RAID and partition-table signatures
  • zeroing the first 100 MiB kills the protective MBR and the primary GPT
  • zeroing the last 100 MiB kills the backup GPT, which is what causes "the partition table was restored" surprises if you skip it

That is what steamos-sanitize-fix.sh does.

The fix for case 2: unlock first

If SANICAP is non-zero, or if the plain wipe also fails with I/O errors, the drive is locked and no wipe of any kind will succeed. You have to clear the lock before installing anything:

  • PSID revert using the PSID printed on the drive's physical label (sedutil-cli --yesIreallywanttoERASEALLmydatausingthePSID <PSID> /dev/nvme0n1). This erases the drive and resets it to factory state.
  • or clear the hardware-encryption / "secure erase" state from your BIOS setup, if the board exposes it.

Then re-run.

Usage

chmod +x steamos-sanitize-fix.sh

./steamos-sanitize-fix.sh diagnose        # read-only. Do this first.
./steamos-sanitize-fix.sh patch           # write the patched installer only
./steamos-sanitize-fix.sh run             # patch, wipe, run the installer

The target device defaults to /dev/nvme0n1; pass another as an argument. Everything must run as root, from the recovery image's terminal.

diagnose changes nothing. It reports SANICAP, the sanitize log, namespace write-protect flags, and the SED lock state, so you know which of the two cases above you are in before you erase anything.

run irreversibly destroys everything on the target device. It asks you to type ERASE to confirm, unless you pass --yes.

Options:

Flag Effect
--yes skip the interactive confirmation
--try-format attempt nvme format -n 1 -s 1 -r before falling back to the plain wipe. Off by default: on some drives format hangs or runs for many minutes, and the install does not need it
--script PATH path to repair_device.sh (auto-detected, default /home/deck/tools/repair_device.sh)

How the patch is applied

The script does not edit sanitize_all() in place, and it does not touch the original file at all. It writes a patched copy to /tmp/repair_device_fixed.sh with a second definition of sanitize_all() appended immediately after the original one closes.

In bash, a later function definition replaces an earlier one. So the patcher only has to find where the original body ends, never to parse or rewrite it. That is considerably harder to get wrong than a sed expression editing braces, and it survives cosmetic changes to the original function.

Two checks run before the patched script is allowed near a disk:

  • the injected marker must be present in the output, so a patch that silently failed to apply cannot fall through to the original sanitize
  • bash -n must pass on the result

There is also a guard that refuses to target the disk holding the running root filesystem or the live installer medium, since pointing this at your USB stick mid-install would be unrecoverable.

Verification status

The patcher was tested against a mock repair_device.sh carrying the real sanitize_all() function (same DISK variable, same brace style), with the disk tools stubbed out. Verified: the override lands after the original, the original is left intact, the override is what executes at runtime, the tail offset computes correctly, and the installer proceeds past the sanitize step.

Not verified: a real install on real hardware. If the repair_device.sh on your recovery image differs enough that sanitize_all() cannot be found, the script aborts with a clear message rather than producing a broken installer.

References

#!/usr/bin/env bash
#
# steamos-sanitize-fix.sh
#
# Works around the SteamOS installer failing at the very first step with:
#
# Ok, let's go. Sanitizing /dev/nvme0n1:
# NVMe status: Access Denied: Access to the namespace and/or LBA range is
# denied due to lack of access rights(0x4286)
#
# That status comes from the SSD controller, not from Linux, so being root
# changes nothing. Either the drive does not implement the NVMe Sanitize
# command at all (very common on retail/OEM drives), or the namespace is
# locked or write-protected (OPAL / BitLocker eDrive / vendor write protect).
#
# Sanitize is only a secure-erase nicety for a fresh install: repair_device.sh
# repartitions and reformats the disk immediately afterwards. So this script
# replaces sanitize_all() with a plain wipe (wipefs + zeroing the first and
# last 100 MiB, which is what actually kills the primary and backup GPT and
# any stale LVM/RAID signatures) and then runs the stock installer.
#
# Usage:
# ./steamos-sanitize-fix.sh diagnose [device] # read-only, safe, do this first
# ./steamos-sanitize-fix.sh patch [device] # write the patched installer only
# ./steamos-sanitize-fix.sh run [device] # patch, wipe, run the installer
#
# device defaults to /dev/nvme0n1
#
# Options:
# --yes skip the interactive confirmation in "run"
# --try-format before falling back, attempt `nvme format --ses=1`
# (off by default: on some drives format hangs or takes
# many minutes, and it is not needed for the install)
# --script PATH path to repair_device.sh
# (default: auto-detected, /home/deck/tools/repair_device.sh)
#
# "run" DESTROYS EVERYTHING on the target device. That is the point of it.
set -euo pipefail
ASSUME_YES=0
TRY_FORMAT=0
REPAIR_SCRIPT=""
ACTION=""
DEV=""
PATCHED=/tmp/repair_device_fixed.sh
die() { printf '\n[FATAL] %s\n' "$*" >&2; exit 1; }
info() { printf '[*] %s\n' "$*"; }
warn() { printf '[!] %s\n' "$*" >&2; }
ok() { printf '[+] %s\n' "$*"; }
usage() {
# Print the header comment block, stopping at the first non-comment line,
# so the help text cannot drift out of sync with a hardcoded line range.
awk 'NR > 2 { if ($0 !~ /^#/) exit; sub(/^# ?/, ""); print }' "$0"
exit "${1:-0}"
}
# ---------------------------------------------------------------- arguments --
while [ $# -gt 0 ]; do
case "$1" in
diagnose|patch|run) ACTION="$1"; shift ;;
--yes|-y) ASSUME_YES=1; shift ;;
--try-format) TRY_FORMAT=1; shift ;;
--script) REPAIR_SCRIPT="${2:-}"; shift 2 ;;
-h|--help) usage 0 ;;
/dev/*) DEV="$1"; shift ;;
*) warn "unknown argument: $1"; usage 1 ;;
esac
done
[ -n "$ACTION" ] || usage 1
DEV="${DEV:-/dev/nvme0n1}"
# The controller device: /dev/nvme0n1 -> /dev/nvme0. Needed because id-ctrl,
# sanitize-log and format take the controller, not the namespace.
CTRL="$(printf '%s' "$DEV" | sed 's/n[0-9]\+$//')"
# ------------------------------------------------------------ sanity checks --
need_root() {
[ "$(id -u)" -eq 0 ] || die "run this as root (you are uid $(id -u))"
}
need_tools() {
local missing=()
for t in "$@"; do
command -v "$t" >/dev/null 2>&1 || missing+=("$t")
done
[ ${#missing[@]} -eq 0 ] || die "missing required tools: ${missing[*]}"
}
check_device() {
[ -b "$DEV" ] || die "$DEV is not a block device"
# Refuse to touch the disk we booted from. On the recovery image the live
# root is the USB stick, and pointing this at it would be unrecoverable
# mid-run. Compare parent disks, not partitions.
local root_src root_disk target_disk
root_src="$(findmnt -n -o SOURCE / 2>/dev/null || true)"
if [ -b "$root_src" ]; then
root_disk="$(lsblk -ndo PKNAME "$root_src" 2>/dev/null || true)"
target_disk="$(basename "$DEV")"
if [ -n "$root_disk" ] && [ "$root_disk" = "$target_disk" ]; then
die "$DEV holds the running root filesystem. Wrong device."
fi
fi
# Same guard for the live medium of an archiso-style image.
local live
live="$(findmnt -n -o SOURCE /run/archiso/bootmnt 2>/dev/null || true)"
if [ -n "$live" ]; then
local live_disk
live_disk="$(lsblk -ndo PKNAME "$live" 2>/dev/null || true)"
if [ -n "$live_disk" ] && [ "$live_disk" = "$(basename "$DEV")" ]; then
die "$DEV is the live installer medium. Wrong device."
fi
fi
}
find_repair_script() {
if [ -n "$REPAIR_SCRIPT" ]; then
[ -r "$REPAIR_SCRIPT" ] || die "cannot read $REPAIR_SCRIPT"
return
fi
local c
for c in /home/deck/tools/repair_device.sh \
/home/deck/repair_device.sh \
/usr/share/steamos/repair_device.sh \
./repair_device.sh; do
if [ -r "$c" ]; then REPAIR_SCRIPT="$c"; return; fi
done
die "repair_device.sh not found, pass it with --script PATH"
}
# ------------------------------------------------------------- diagnostics --
diagnose() {
need_tools lsblk
check_device
printf '\n=== target ===\n'
lsblk -o NAME,SIZE,MODEL,SERIAL,RO,TYPE,MOUNTPOINTS "$DEV" || true
if ! command -v nvme >/dev/null 2>&1; then
warn "nvme-cli not installed, skipping controller queries"
return 0
fi
printf '\n=== does the drive implement Sanitize at all? ===\n'
# SANICAP is a controller capability field. All-zero means the Sanitize
# command is simply not supported, and no amount of unlocking will help:
# skipping it is the only fix.
local sanicap
sanicap="$(nvme id-ctrl "$CTRL" 2>/dev/null | awk '/^sanicap/ {print $3}')" || true
if [ -z "$sanicap" ]; then
warn "could not read sanicap from $CTRL"
else
printf 'sanicap : %s\n' "$sanicap"
case "$sanicap" in
0|0x0|0x00000000)
ok "Sanitize is UNSUPPORTED by this drive. Skipping it is correct and safe."
;;
*)
warn "Sanitize is advertised as supported, yet it returned Access Denied."
warn "That points at a locked or write-protected namespace (OPAL / eDrive)."
warn "If the plain wipe below also fails with I/O errors, the lock has to"
warn "come off first: PSID revert from the drive label, or clear hardware"
warn "encryption in the BIOS, then re-run."
;;
esac
fi
printf '\n=== sanitize log ===\n'
nvme sanitize-log "$CTRL" 2>&1 | head -20 || true
printf '\n=== namespace write protection ===\n'
nvme id-ns "$DEV" -H 2>/dev/null | grep -i -E 'protect|nsattr' || \
printf '(nothing reported)\n'
printf '\n=== security / format capability ===\n'
nvme id-ctrl "$CTRL" 2>/dev/null | grep -i -E '^(oacs|fna|nn)' || true
printf '\n=== self-encrypting drive lock state ===\n'
if command -v sedutil-cli >/dev/null 2>&1; then
sedutil-cli --scan 2>&1 | head -10 || true
else
printf '(sedutil-cli not installed; if the wipe fails with I/O errors,\n'
printf ' an OPAL lock is the likely cause and needs a PSID revert)\n'
fi
printf '\n'
ok "diagnosis done. Nothing was modified."
}
# -------------------------------------------------------------- the patcher --
# Insert an overriding definition of sanitize_all() immediately after the
# original one. Redefining a bash function later in the file wins, so this
# never has to understand or rewrite the original body: it only has to find
# where that body ends. That is far less fragile than editing braces in place.
write_patched_script() {
find_repair_script
info "source installer : $REPAIR_SCRIPT"
grep -qE '^[[:space:]]*sanitize_all[[:space:]]*\(\)' "$REPAIR_SCRIPT" || \
die "no sanitize_all() in $REPAIR_SCRIPT; this installer differs from the one this script targets"
awk '
BEGIN { state = 0; depth = 0; opened = 0 }
# state 0: hunting for the function definition
state == 0 && $0 ~ /^[[:space:]]*sanitize_all[[:space:]]*\(\)/ {
state = 1
}
# state 1: inside the function, tracking brace depth so we can find
# the line that closes it. Both brace styles work: the opening "{" may
# sit on the definition line or on the line after it.
state == 1 {
print
n = gsub(/\{/, "{"); depth += n
if (n > 0) { opened = 1 }
n = gsub(/\}/, "}"); depth -= n
if (depth <= 0 && opened) {
state = 2
print ""
print "# ---- injected by steamos-sanitize-fix.sh ----------------------------"
print "# The stock sanitize_all() calls `nvme sanitize`, which this drive"
print "# refuses with 0x4286 Access Denied. This override does the only part"
print "# a fresh install actually needs: destroy every filesystem and"
print "# partition-table signature on the disk."
print "sanitize_all()"
print "{"
print " local dev=\"${1:-$DISK}\""
print " echo \"NVMe sanitize disabled by steamos-sanitize-fix.sh\""
print " echo \"Performing a plain wipe of $dev instead.\""
print " wipefs -a \"$dev\" || true"
print " blkdiscard -f \"$dev\" 2>/dev/null || true"
print " dd if=/dev/zero of=\"$dev\" bs=1M count=100 conv=fsync status=none"
print " local end_mib"
print " end_mib=$(( $(blockdev --getsize64 \"$dev\") / 1048576 - 100 ))"
print " if [ \"$end_mib\" -gt 0 ]; then"
print " dd if=/dev/zero of=\"$dev\" bs=1M count=100 seek=\"$end_mib\" conv=fsync status=none"
print " fi"
print " sync"
print " partprobe \"$dev\" 2>/dev/null || blockdev --rereadpt \"$dev\" 2>/dev/null || true"
print " echo \"Wipe complete.\""
print " return 0"
print "}"
print "# ---- end injection ---------------------------------------------------"
print ""
}
next
}
{ print }
' "$REPAIR_SCRIPT" > "$PATCHED"
# An override that never got inserted would silently run the original
# sanitize and fail again, so prove the injection landed.
grep -q 'injected by steamos-sanitize-fix.sh' "$PATCHED" || \
die "patch did not apply; inspect $REPAIR_SCRIPT by hand"
# And prove the result is still valid bash before handing it a disk.
bash -n "$PATCHED" || die "patched script fails syntax check, refusing to run it"
chmod +x "$PATCHED"
ok "patched installer written to $PATCHED (original untouched)"
info "review it with: grep -n -A28 'injected by' $PATCHED"
}
# ---------------------------------------------------------- the manual wipe --
manual_wipe() {
need_tools wipefs dd blockdev sync
if [ "$TRY_FORMAT" -eq 1 ] && command -v nvme >/dev/null 2>&1; then
info "attempting nvme format -s 1 (non-fatal if it fails)"
# --force only exists on newer nvme-cli, so retry without it rather
# than mistaking an unknown-option error for a locked drive.
nvme format "$DEV" -n 1 -s 1 -r --force 2>&1 ||
nvme format "$DEV" -n 1 -s 1 -r 2>&1 ||
warn "nvme format refused too, continuing with the plain wipe"
fi
info "wipefs -a $DEV"
wipefs -a "$DEV" || warn "wipefs reported an error, continuing"
info "blkdiscard (best effort)"
blkdiscard -f "$DEV" 2>/dev/null || info "blkdiscard unsupported or refused, fine"
info "zeroing the first 100 MiB"
dd if=/dev/zero of="$DEV" bs=1M count=100 conv=fsync status=none || \
die "cannot write to $DEV. If this is an I/O error the drive is locked (OPAL/eDrive); do a PSID revert first."
local size_b end_mib
size_b="$(blockdev --getsize64 "$DEV")"
end_mib=$(( size_b / 1048576 - 100 ))
if [ "$end_mib" -gt 0 ]; then
info "zeroing the last 100 MiB (backup GPT)"
dd if=/dev/zero of="$DEV" bs=1M count=100 seek="$end_mib" conv=fsync status=none || \
warn "could not zero the tail of the disk"
fi
sync
partprobe "$DEV" 2>/dev/null || blockdev --rereadpt "$DEV" 2>/dev/null || true
ok "plain wipe complete"
}
# --------------------------------------------------------------- the runner --
confirm() {
[ "$ASSUME_YES" -eq 1 ] && return 0
printf '\n'
printf ' This will IRREVERSIBLY ERASE %s and install SteamOS on it.\n' "$DEV"
lsblk -o NAME,SIZE,MODEL,SERIAL,TYPE,MOUNTPOINTS "$DEV" || true
printf '\n Type exactly: ERASE\n > '
local answer
read -r answer
[ "$answer" = "ERASE" ] || die "not confirmed, nothing was changed"
}
run_install() {
check_device
write_patched_script
confirm
manual_wipe
info "handing off to the patched installer"
printf '\n'
# `all` is the full wipe + repartition + reinstall path. The patched
# sanitize_all() inside it is now a no-op wipe, so this proceeds.
bash "$PATCHED" all
}
# ------------------------------------------------------------------- main --
need_root
case "$ACTION" in
diagnose) diagnose ;;
patch) check_device; write_patched_script ;;
run) run_install ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment