felixrieseberg/windows95 · error

hda block cache not reachable after restore

Error message

hda block cache not reachable after restore

What it means

recoverLegacyDisk boots a throwaway v86 emulator with autostart:false, restores a legacy state file, and then expects to reach the async HDA's internal block cache (the map of 256-byte blocks the guest wrote) via the deep path emulator.v86.cpu.devices.ide.primary.master.buffer.block_cache. That path is an implementation detail of v86 and is guarded only by optional chaining. If any link is missing — the emulator object shape changed, the IDE device was never attached, hda.async wasn't enabled, or restore_state didn't populate the buffer — the code throws this error because without the overlay cache it cannot reconstruct the written sectors of the old disk.

Source

Thrown at src/renderer/utils/recover-legacy-disk.ts:59

    disable_speaker: true,
  });

  await new Promise<void>((resolve) =>
    emulator.add_listener("emulator-loaded", resolve),
  );

  let files = 0;
  const baseFd = fs.openSync(CONSTANTS.IMAGE_PATH, "r");
  try {
    const state = fs.readFileSync(legacyStatePath);
    await emulator.restore_state(state.buffer);

    const buf = emulator.v86?.cpu?.devices?.ide?.primary?.master?.buffer as {
      block_cache: Map<number, Uint8Array>;
      block_cache_is_write: Set<number>;
    };
    if (!buf?.block_cache) {
      throw new Error("hda block cache not reachable after restore");
    }

    // v86 caches in 256-byte blocks; FAT works in 512-byte sectors.
    const sec = Buffer.allocUnsafe(512);
    const readSector = (lba: number) => {
      const lo = buf.block_cache_is_write.has(lba * 2)
        ? buf.block_cache.get(lba * 2)
        : undefined;
      const hi = buf.block_cache_is_write.has(lba * 2 + 1)
        ? buf.block_cache.get(lba * 2 + 1)
        : undefined;
      if (lo && hi) return Buffer.concat([lo, hi]);
      fs.readSync(baseFd, sec, 0, 512, lba * 512);
      if (lo) sec.set(lo, 0);
      if (hi) sec.set(hi, 256);
      return Buffer.from(sec);
    };
    const isDirty = (lba: number) =>

View on GitHub (pinned to 051065e5ae)

Solutions

  1. Confirm the V86 constructor config includes `hda: { async: true, size: <exact image size> }` — without async mode the buffer/block_cache structure doesn't exist.
  2. Check the installed v86 version: dump `emulator.v86.cpu.devices.ide` in a debug run and update the access path (e.g. `ide0` vs `ide.primary`, `buffer` wrapper changes) to match the bundled libv86.js.
  3. Re-verify the legacy state file is valid and matches the current image/geometry (STATE_VERSION); a bad restore can leave devices uninitialized — try restoring with a known-good state.
  4. Log which link in the chain is undefined (`emulator.v86?`, `.cpu?`, `.devices?`, `.ide?`, `.primary?`, `.master?`, `.buffer?`) to pinpoint whether it's a config problem vs an API change.
  5. Pin/downgrade v86 to the version the recovery code was written against, or vendor a patched fork with a stable accessor for the hda block cache.

Example fix

// before
const buf = emulator.v86?.cpu?.devices?.ide?.primary?.master?.buffer;
if (!buf?.block_cache) throw new Error("hda block cache not reachable after restore");

// after — fail with a diagnostic instead of a bare throw:
const ide = emulator.v86?.cpu?.devices?.ide ?? emulator.v86?.cpu?.devices?.ide0;
const buf = ide?.primary?.master?.buffer ?? ide?.master?.buffer;
if (!buf?.block_cache) {
  console.error("ide device tree:", JSON.stringify(Object.keys(emulator.v86?.cpu?.devices ?? {})));
  throw new Error("hda block cache not reachable — check hda.async:true and v86 version compatibility");
}
Defensive patterns

Strategy: type-guard

Validate before calling

// After emulator-loaded and restore_state, before relying on the cache:
function resolveHdaBuffer(emulator: any): { block_cache: Map<number, Uint8Array>; block_cache_is_write: Set<number> } | null {
  const ide = emulator?.v86?.cpu?.devices?.ide;
  const buf = ide?.primary?.master?.buffer;
  return buf && buf.block_cache instanceof Map && buf.block_cache_is_write instanceof Set ? buf : null;
}
// Also verify construction options:
// new V86({ hda: { url: IMAGE_PATH, async: true, size }, autostart: false, ... })

Type guard

function hasBlockCache(buf: unknown): buf is { block_cache: Map<number, Uint8Array>; block_cache_is_write: Set<number> } {
  return (
    typeof buf === "object" && buf !== null &&
    "block_cache" in buf &&
    (buf as any).block_cache instanceof Map &&
    "block_cache_is_write" in buf &&
    (buf as any).block_cache_is_write instanceof Set
  );
}

Try / catch

try {
  const { dir, files } = await recoverLegacyDisk(legacyStatePath, outDir);
} catch (e) {
  if (e instanceof Error && e.message.includes("hda block cache not reachable")) {
    // v86 internals changed or hda.async missing — surface actionable guidance
    throw new Error("Recovery failed: v86 HDA block cache unavailable. Ensure hda.async:true and that the bundled v86 version matches the recovery code's expected device tree.", { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling recoverLegacyDisk with a v86 build whose internal device tree differs (e.g. ide0/ide1 naming, buffer wrapper class without block_cache); constructing V86 without hda.async:true; the state restore failing silently or completing before the async HDA buffer is attached; passing a state saved from a machine configured without a primary-master hda.

Common situations: Upgrading v86 (build/v86.wasm + libv86.js) where the internal ide device structure was refactored; a corrupt or mismatched legacy state-vN.bin that restore_state rejects or partially applies; forgetting the `async: true` hda option; running against a different hda image size so the IDE drive isn't registered as expected.


AI-assisted analysis of felixrieseberg/windows95@051065e5ae (2026-08-31). Data as JSON: /api/errors/8e384c408563263b. Report an issue: GitHub.