{"record":{"id":"8aee0891ef9ec26a","repo":"commaai/openpilot","slug":"cannot-recover-from-the-rom-bootloader-without-a-c","errorCode":null,"errorMessage":"cannot recover from the ROM bootloader without a config backup at {backup}","messagePattern":"cannot recover from the ROM bootloader without a config backup at (.+?)","errorType":"console","errorClass":"RuntimeError","httpStatus":null,"severity":"critical","filePath":"openpilot/system/hardware/chestnut/flash.py","lineNumber":491,"sourceCode":"\n  previous = {sig: signal.signal(sig, defer_signal) for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP)}\n  try:\n    if in_rom_bootloader(vid_pid, product):\n      if not recover_from_rom(image, expected_product):\n        return\n      # firmware is back, verify it against the bundled image\n      force, product = True, None\n    write_image(image, expected_product, product, force)\n  finally:\n    for sig, handler in previous.items():\n      signal.signal(sig, handler)\n\n\ndef recover_from_rom(image, expected_product):\n  # returns whether the chestnut came back on custom firmware\n  backup = config_path()\n  if not os.path.isfile(backup):\n    raise RuntimeError(f\"cannot recover from the ROM bootloader without a config backup at {backup}\")\n  config = open(backup, \"rb\").read()\n  if len(config) != 0x100:\n    raise RuntimeError(f\"invalid config backup: {backup}\")\n\n  committed = False\n  while True:\n    check_budget()\n    path, vid_pid, product = find_chestnut()\n    if path is None:\n      if committed:\n        print(\"chestnut is offline, recovered firmware boots on its next power cycle\", flush=True)\n        return False\n      vbus_cycle()\n      continue\n    if not in_rom_bootloader(vid_pid, product):\n      return True\n    if committed:\n      print(\"chestnut stayed powered, recovered firmware boots on its next power cycle\", flush=True)","sourceCodeStart":473,"sourceCodeEnd":509,"githubUrl":"https://github.com/commaai/openpilot/blob/516ec1e68203439a73f340f1d0b3b91eabc626ee/openpilot/system/hardware/chestnut/flash.py#L473-L509","documentation":"recover_from_rom() needs a previously saved 0x100-byte config blob (config_path()) to restore the chestnut's configuration while reviving it from the ROM bootloader. If that backup file does not exist, recovery cannot proceed and this RuntimeError is raised. Without the config, the device cannot be brought back onto custom firmware from ROM mode.","triggerScenarios":"A flash was interrupted and the device fell into the ROM bootloader, but no successful earlier flash ever wrote a config backup at config_path(); the backup file was deleted or is on a different machine/user's home; first-ever flash on a fresh device failing mid-way before a backup existed.","commonSituations":"Running flash.py as a different user (root vs user) so config_path() resolves elsewhere; cleaning home directories or /tmp wipes the backup; recovering a device that shipped in a bad state and was never successfully flashed once.","solutions":["Check whether the backup exists under another account: look at config_path() for the user that originally ran flash.py and copy it to the current location","Re-run flash.py from the machine/user where the chestnut was originally flashed so config_path() points at the existing backup","If no backup exists anywhere, the device must be recovered through comma's official recovery/reflash path - contact support with the device state","For the future: never delete the file at config_path(); back it up alongside device records"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import os\nfrom openpilot.system.hardware.chestnut.flash import config_path\nbackup = config_path()\nassert os.path.isfile(backup), f\"no config backup at {backup} - do NOT enter recovery without it\"\nassert os.path.getsize(backup) == 0x100, f\"bad backup size {os.path.getsize(backup)}\"","typeGuard":null,"tryCatchPattern":"try:\n    recover_from_rom(image, expected_product)\nexcept RuntimeError as e:\n    if 'config backup' in str(e):\n        # stop recovery attempts; escalate - device must not be power-cycled repeatedly\n        raise SystemExit(f\"recovery blocked: {e}\")\n    raise","preventionTips":["Treat the config backup at config_path() as critical device state: back it up to durable storage right after every successful flash","Never run flash.py from accounts whose config_path() differs from the one holding the backup","Before any risky flash (--force, first flash), confirm the backup exists and is 256 bytes so recovery remains possible"],"tags":["firmware","recovery","bootloader","missing-file","openpilot"],"backgroundTag":null,"analyzedSha":"516ec1e68203439a73f340f1d0b3b91eabc626ee","analyzedAt":"2026-08-15T00:17:37.461Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}