/* * iso-patch.exe -- Windows ISO utilities: * * iso-patch.exe * Rebuild ISO with noprompt UEFI boot files (no "Press Any Key"). * Output: \_noprompt.iso * * iso-patch.exe --to-vhdx [image-index] [size-gb] [--output path] [--stage manifest] * Convert a Windows installer ISO into a bootable UEFI VHDX. * Output defaults to \.vhdx, or --output overrides. * image-index defaults to 1, size-gb defaults to 64 (min 16). * --stage: copy files listed in manifest onto the VHDX. * Manifest: tab-separated lines of \t */ #include #include #include #include #include #include #include "ubuntu_vhdx.h" #include "prefetch_build_deps.h" #include "prefetch_wsl_deps.h" #include "prefetch_repo.h" #include "target_arch.h" #pragma comment(lib, "virtdisk.lib") #pragma comment(lib, "ole32.lib") #pragma comment(lib, "oleaut32.lib") #pragma comment(lib, "shlwapi.lib") /* ---- GUIDs ---- */ /* Use custom names to avoid clashing with EXTERN_C declarations in imapi2fs.h */ static const GUID CLSID_FSImage = {0x2C941FC5, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}}; static const GUID IID_IFSImage = {0x2C941FE1, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}}; static const GUID CLSID_BootOpts = {0x2C941FCE, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}}; static const GUID IID_IBootOpts = {0x2C941FD4, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}}; static const GUID IID_IFsiDirItem = {0x2C941FDC, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}}; static const GUID VHDX_VENDOR_MS = { 0xec984aec, 0xa0f9, 0x47e9, { 0x90, 0x1f, 0x71, 0x41, 0x5a, 0x66, 0x34, 0x5b } }; /* ---- Helpers ---- */ /* Output protocol (machine-parseable, one per line): STATUS: — step change (shown in UI log) PROGRESS:: — progress update (updates in place in UI) ERROR: — failure (shown in UI log as error) DONE: — success, path to output file */ void log_msg(const wchar_t *fmt, ...) { va_list ap; va_start(ap, fmt); wprintf(L"STATUS:"); vwprintf(fmt, ap); wprintf(L"\n"); fflush(stdout); va_end(ap); } void log_progress(int pct, const wchar_t *step) { wprintf(L"PROGRESS:%d:%s\n", pct, step); fflush(stdout); } void log_done(const wchar_t *path) { wprintf(L"DONE:%s\n", path); fflush(stdout); } void log_err(const wchar_t *fmt, ...) { va_list ap; va_start(ap, fmt); fwprintf(stderr, L"ERROR:"); vfwprintf(stderr, fmt, ap); fwprintf(stderr, L"\n"); fflush(stderr); va_end(ap); } /* Build output path: \ */ static void build_output_path(wchar_t *out, size_t out_len, const wchar_t *input_path, const wchar_t *suffix) { wchar_t exe_dir[MAX_PATH]; const wchar_t *base_name; wchar_t stem[MAX_PATH]; wchar_t *slash, *dot; GetModuleFileNameW(NULL, exe_dir, MAX_PATH); slash = wcsrchr(exe_dir, L'\\'); if (slash) *slash = L'\0'; base_name = wcsrchr(input_path, L'\\'); base_name = base_name ? base_name + 1 : input_path; wcscpy_s(stem, MAX_PATH, base_name); dot = wcsrchr(stem, L'.'); if (dot) *dot = L'\0'; swprintf_s(out, out_len, L"%s\\%s%s", exe_dir, stem, suffix); } /* Run a command and wait for it to finish. If quiet=TRUE, child stdout/stderr are suppressed. Returns process exit code, or -1 on failure. */ static int run_command(const wchar_t *cmdline, BOOL quiet) { STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD exit_code = (DWORD)-1; wchar_t *cmd_buf; size_t len; HANDLE hNul = INVALID_HANDLE_VALUE; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); if (quiet) { SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; hNul = CreateFileW(L"NUL", GENERIC_WRITE, FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, NULL); if (hNul != INVALID_HANDLE_VALUE) { si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); si.hStdOutput = hNul; si.hStdError = hNul; } } /* CreateProcessW needs a mutable command line */ len = wcslen(cmdline) + 1; cmd_buf = (wchar_t *)malloc(len * sizeof(wchar_t)); if (!cmd_buf) { if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul); return -1; } wcscpy_s(cmd_buf, len, cmdline); if (!CreateProcessW(NULL, cmd_buf, NULL, NULL, quiet ? TRUE : FALSE, 0, NULL, NULL, &si, &pi)) { log_err(L"Failed to run: %s (error %lu)", cmdline, GetLastError()); free(cmd_buf); if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul); return -1; } free(cmd_buf); WaitForSingleObject(pi.hProcess, INFINITE); GetExitCodeProcess(pi.hProcess, &exit_code); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul); return (int)exit_code; } /* Run a command, capture stdout, and report percentage progress. Scans output for patterns like "45.0%" and calls log_msg with updates. prefix: status message shown before the percentage (e.g. "Applying Windows image"). Returns process exit code, or -1 on failure. */ static int run_command_with_progress(const wchar_t *cmdline, const wchar_t *prefix) { SECURITY_ATTRIBUTES sa; HANDLE hReadPipe = NULL, hWritePipe = NULL; STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD exit_code = (DWORD)-1; wchar_t *cmd_buf; size_t len; int last_pct = -1; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) return -1; /* Ensure the read handle is not inherited */ SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0); ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); si.hStdOutput = hWritePipe; si.hStdError = hWritePipe; ZeroMemory(&pi, sizeof(pi)); len = wcslen(cmdline) + 1; cmd_buf = (wchar_t *)malloc(len * sizeof(wchar_t)); if (!cmd_buf) { CloseHandle(hReadPipe); CloseHandle(hWritePipe); return -1; } wcscpy_s(cmd_buf, len, cmdline); if (!CreateProcessW(NULL, cmd_buf, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { log_err(L"Failed to run: %s (error %lu)", cmdline, GetLastError()); free(cmd_buf); CloseHandle(hReadPipe); CloseHandle(hWritePipe); return -1; } free(cmd_buf); /* Close write end — we only read */ CloseHandle(hWritePipe); hWritePipe = NULL; /* Read output and scan for percentage */ { char buf[4096]; DWORD bytes_read; while (ReadFile(hReadPipe, buf, sizeof(buf) - 1, &bytes_read, NULL) && bytes_read > 0) { buf[bytes_read] = '\0'; /* Scan for XX.X% pattern */ for (DWORD i = 0; i < bytes_read; i++) { if (buf[i] == '%') { /* Walk backwards to find the number */ int j = (int)i - 1; while (j >= 0 && (buf[j] == '.' || (buf[j] >= '0' && buf[j] <= '9'))) j--; j++; if (j < (int)i) { /* Parse the percentage as integer */ double pct_val = atof(buf + j); int pct = (int)pct_val; if (pct >= 0 && pct <= 100 && pct != last_pct) { last_pct = pct; log_progress(pct, prefix); } } } } } } CloseHandle(hReadPipe); WaitForSingleObject(pi.hProcess, INFINITE); GetExitCodeProcess(pi.hProcess, &exit_code); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return (int)exit_code; } /* Run a command and capture stdout into a caller-provided buffer. Returns process exit code, or -1 on failure. */ static int run_command_capture(const wchar_t *cmdline, char *out_buf, int out_buf_size) { SECURITY_ATTRIBUTES sa; HANDLE hReadPipe = NULL, hWritePipe = NULL; STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD exit_code = (DWORD)-1; wchar_t *cmd_buf; size_t len; int total = 0; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) return -1; SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0); ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); si.hStdOutput = hWritePipe; si.hStdError = hWritePipe; ZeroMemory(&pi, sizeof(pi)); len = wcslen(cmdline) + 1; cmd_buf = (wchar_t *)malloc(len * sizeof(wchar_t)); if (!cmd_buf) { CloseHandle(hReadPipe); CloseHandle(hWritePipe); return -1; } wcscpy_s(cmd_buf, len, cmdline); if (!CreateProcessW(NULL, cmd_buf, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { free(cmd_buf); CloseHandle(hReadPipe); CloseHandle(hWritePipe); return -1; } free(cmd_buf); CloseHandle(hWritePipe); hWritePipe = NULL; /* Read all output into buffer */ { DWORD bytes_read; while (total < out_buf_size - 1 && ReadFile(hReadPipe, out_buf + total, (DWORD)(out_buf_size - 1 - total), &bytes_read, NULL) && bytes_read > 0) { total += (int)bytes_read; } out_buf[total] = '\0'; } CloseHandle(hReadPipe); WaitForSingleObject(pi.hProcess, INFINITE); GetExitCodeProcess(pi.hProcess, &exit_code); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return (int)exit_code; } /* Snapshot of which drive letters are CDROM before mounting */ static DWORD snapshot_cdrom_drives(void) { DWORD mask = 0; wchar_t root[4] = L"A:\\"; for (int i = 0; i < 26; i++) { root[0] = (wchar_t)(L'A' + i); if (GetDriveTypeW(root) == DRIVE_CDROM) mask |= (1u << i); } return mask; } /* Find a new CDROM drive letter that appeared after mounting */ static wchar_t find_new_cdrom(DWORD before_mask) { wchar_t root[4] = L"A:\\"; for (int i = 0; i < 26; i++) { root[0] = (wchar_t)(L'A' + i); if (!(before_mask & (1u << i)) && GetDriveTypeW(root) == DRIVE_CDROM) return (wchar_t)(L'A' + i); } return 0; } /* Write an IStream to a file */ static HRESULT write_stream_to_file(IStream *stream, const wchar_t *path) { HANDLE hFile; BYTE buf[65536]; ULONG bytes_read; DWORD bytes_written; HRESULT hr; LARGE_INTEGER zero; hFile = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return HRESULT_FROM_WIN32(GetLastError()); zero.QuadPart = 0; stream->lpVtbl->Seek(stream, zero, STREAM_SEEK_SET, NULL); for (;;) { hr = stream->lpVtbl->Read(stream, buf, sizeof(buf), &bytes_read); if (FAILED(hr)) { CloseHandle(hFile); return hr; } if (bytes_read == 0) break; if (!WriteFile(hFile, buf, bytes_read, &bytes_written, NULL)) { hr = HRESULT_FROM_WIN32(GetLastError()); CloseHandle(hFile); return hr; } } CloseHandle(hFile); return S_OK; } /* Navigate a directory tree: root -> child1 -> child2 -> ... -> NULL Returns the deepest IFsiDirectoryItem. Caller must Release(). */ static HRESULT navigate_fsi_path(IFsiDirectoryItem *root, IFsiDirectoryItem **out, const wchar_t *name1, ...) { va_list ap; IFsiDirectoryItem *current = root; IFsiItem *item = NULL; const wchar_t *name = name1; HRESULT hr = S_OK; BSTR bstr; current->lpVtbl->AddRef(current); va_start(ap, name1); while (name) { bstr = SysAllocString(name); hr = current->lpVtbl->get_Item(current, bstr, &item); SysFreeString(bstr); if (FAILED(hr)) { current->lpVtbl->Release(current); va_end(ap); return hr; } IFsiDirectoryItem *next = NULL; hr = item->lpVtbl->QueryInterface(item, &IID_IFsiDirItem, (void **)&next); item->lpVtbl->Release(item); if (FAILED(hr)) { current->lpVtbl->Release(current); va_end(ap); return hr; } current->lpVtbl->Release(current); current = next; name = va_arg(ap, const wchar_t *); } va_end(ap); *out = current; return S_OK; } /* ====================================================================== * --to-vhdx: Convert Windows installer ISO to bootable UEFI VHDX * ====================================================================== */ /* Partition sizes */ #define EFI_SIZE_MB 200 #define MSR_SIZE_MB 128 #define DEFAULT_VHDX_SIZE_GB 64 /* Find the volume GUID path for a partition on a given disk. disk_number: the disk number from GetVirtualDiskPhysicalPath partition_number: 1-based partition index in the GPT layout. Pass 0 to match any volume on the disk (e.g. ISO with no partitions). Returns TRUE and fills vol_path (e.g. \\?\Volume{guid}\) on success. */ static BOOL find_volume_for_partition(DWORD disk_number, DWORD partition_number, wchar_t *vol_path, size_t vol_path_len) { HANDLE hFind; wchar_t vol_name[MAX_PATH]; BOOL found = FALSE; hFind = FindFirstVolumeW(vol_name, MAX_PATH); if (hFind == INVALID_HANDLE_VALUE) return FALSE; do { /* Open the volume — strip trailing backslash for CreateFileW */ wchar_t vol_dev[MAX_PATH]; HANDLE hVol; wcscpy_s(vol_dev, MAX_PATH, vol_name); size_t vlen = wcslen(vol_dev); if (vlen > 0 && vol_dev[vlen - 1] == L'\\') vol_dev[vlen - 1] = L'\0'; hVol = CreateFileW(vol_dev, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hVol != INVALID_HANDLE_VALUE) { /* Get the disk extents for this volume */ BYTE buf[256]; DWORD bytes; if (DeviceIoControl(hVol, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, NULL, 0, buf, sizeof(buf), &bytes, NULL)) { VOLUME_DISK_EXTENTS *ext = (VOLUME_DISK_EXTENTS *)buf; if (ext->NumberOfDiskExtents >= 1 && ext->Extents[0].DiskNumber == disk_number) { if (partition_number == 0) { /* Match any volume on this disk (e.g. ISO) */ wcscpy_s(vol_path, vol_path_len, vol_name); found = TRUE; } else { /* Check partition number via IOCTL_STORAGE_GET_DEVICE_NUMBER */ STORAGE_DEVICE_NUMBER sdn; if (DeviceIoControl(hVol, IOCTL_STORAGE_GET_DEVICE_NUMBER, NULL, 0, &sdn, sizeof(sdn), &bytes, NULL)) { if (sdn.PartitionNumber == partition_number) { wcscpy_s(vol_path, vol_path_len, vol_name); found = TRUE; } } } } } CloseHandle(hVol); } if (found) break; } while (FindNextVolumeW(hFind, vol_name, MAX_PATH)); FindVolumeClose(hFind); return found; } /* Extract disk number from physical path like \\.\PhysicalDrive3 */ static DWORD get_disk_number_from_path(const wchar_t *phys_path) { const wchar_t *p = wcsrchr(phys_path, L'e'); /* PhysicalDriv'e' */ if (p && p[1] >= L'0' && p[1] <= L'9') return (DWORD)_wtoi(p + 1); /* Fallback: find last run of digits */ size_t len = wcslen(phys_path); while (len > 0 && phys_path[len - 1] >= L'0' && phys_path[len - 1] <= L'9') len--; return (DWORD)_wtoi(phys_path + len); } /* Create a temporary directory under %TEMP% with given prefix. Returns TRUE and fills path on success. Path includes trailing backslash. */ static BOOL create_temp_mount_dir(const wchar_t *prefix, wchar_t *path, size_t path_len) { wchar_t temp_dir[MAX_PATH]; DWORD len = GetTempPathW(MAX_PATH, temp_dir); if (len == 0) return FALSE; swprintf_s(path, path_len, L"%s%s_%u\\", temp_dir, prefix, GetCurrentProcessId()); if (!CreateDirectoryW(path, NULL)) { if (GetLastError() != ERROR_ALREADY_EXISTS) return FALSE; } return TRUE; } /* Mount a volume to a directory (no drive letter). vol_path must have trailing backslash. mount_dir must have trailing backslash and must be an empty directory. */ static BOOL mount_volume_to_dir(const wchar_t *vol_path, const wchar_t *mount_dir) { return SetVolumeMountPointW(mount_dir, vol_path); } /* Unmount a volume from a directory. */ static BOOL unmount_volume_from_dir(const wchar_t *mount_dir) { return DeleteVolumeMountPointW(mount_dir); } /* Format a volume using format.exe. fs_type is "FAT32" or "NTFS". vol_path is e.g. \\?\Volume{guid}\ — but format.exe needs a drive letter or mount point. We pass the mount_dir which has a trailing backslash. */ static BOOL format_volume(const wchar_t *mount_dir, const wchar_t *fs_type, const wchar_t *label) { wchar_t cmd[512]; wchar_t sys_dir[MAX_PATH]; int ret; GetSystemDirectoryW(sys_dir, MAX_PATH); /* Strip trailing backslash — format.com doesn't like "path\" */ { wchar_t mount_clean[MAX_PATH]; wcscpy_s(mount_clean, MAX_PATH, mount_dir); size_t mlen = wcslen(mount_clean); if (mlen > 0 && mount_clean[mlen - 1] == L'\\') mount_clean[mlen - 1] = L'\0'; if (label && label[0]) { swprintf_s(cmd, 512, L"%s\\format.com \"%s\" /FS:%s /Q /Y /V:%s", sys_dir, mount_clean, fs_type, label); } else { swprintf_s(cmd, 512, L"%s\\format.com \"%s\" /FS:%s /Q /Y", sys_dir, mount_clean, fs_type); } } ret = run_command(cmd, TRUE); if (ret != 0) { log_err(L"Failed to format volume (exit code %d)", ret); return FALSE; } return TRUE; } /* Recursively create directories for a path. path should be the full file path — directories are created for all components except the last (the filename). */ static void ensure_parent_dirs(const wchar_t *file_path) { wchar_t dir[MAX_PATH]; wchar_t *p; wcscpy_s(dir, MAX_PATH, file_path); /* Find the last backslash (parent directory) */ p = wcsrchr(dir, L'\\'); if (!p) return; *p = L'\0'; /* If directory already exists, done */ if (GetFileAttributesW(dir) != INVALID_FILE_ATTRIBUTES) return; /* Recursively create parent */ ensure_parent_dirs(dir); CreateDirectoryW(dir, NULL); } /* Process a staging manifest file. Each line is tab-separated: \t Lines starting with # are comments. Empty lines are skipped. Returns the number of files staged, or -1 on error. */ static int stage_files(const wchar_t *manifest_path, const wchar_t *mount_root) { FILE *f = NULL; wchar_t line[2048]; int count = 0; int total = 0; int skipped = 0; if (_wfopen_s(&f, manifest_path, L"r, ccs=UTF-8") != 0 || !f) { log_err(L"Cannot open staging manifest: %s", manifest_path); return -1; } /* First pass: count lines for progress */ while (fgetws(line, 2048, f)) { size_t len = wcslen(line); while (len > 0 && (line[len - 1] == L'\n' || line[len - 1] == L'\r')) line[--len] = L'\0'; if (len == 0 || line[0] == L'#') continue; if (wcschr(line, L'\t')) total++; } fseek(f, 0, SEEK_SET); /* Second pass: copy files */ while (fgetws(line, 2048, f)) { wchar_t *src, *rel_dest, *tab; wchar_t dest[MAX_PATH]; const wchar_t *filename; size_t len = wcslen(line); /* Strip newline */ while (len > 0 && (line[len - 1] == L'\n' || line[len - 1] == L'\r')) line[--len] = L'\0'; if (len == 0 || line[0] == L'#') continue; /* Skip UTF-8 BOM (U+FEFF) that ccs=UTF-8 may produce on first line */ if (line[0] == L'\xFEFF') { memmove(line, line + 1, len * sizeof(wchar_t)); len--; if (len == 0) continue; } /* Split on tab */ tab = wcschr(line, L'\t'); if (!tab) continue; *tab = L'\0'; src = line; rel_dest = tab + 1; /* Skip leading backslash on relative dest */ if (rel_dest[0] == L'\\') rel_dest++; /* Build full destination: mount_root + rel_dest */ swprintf_s(dest, MAX_PATH, L"%s%s", mount_root, rel_dest); /* Extract just the filename for status */ filename = wcsrchr(rel_dest, L'\\'); filename = filename ? filename + 1 : rel_dest; count++; if (total > 0) { int pct = (count * 100) / total; log_progress(pct, L"Staging files..."); } /* Create parent directories */ ensure_parent_dirs(dest); log_msg(L"Copying: %s -> %s", src, dest); if (!CopyFileW(src, dest, FALSE)) { DWORD err = GetLastError(); /* Only fatal for core files (unattend, setup scripts, agent). GPU/driver files are best-effort since Plan9 shares provide a fallback. */ if (wcsstr(rel_dest, L"HostDriverStore") != NULL || wcsstr(rel_dest, L"hostdriverstore") != NULL) { log_msg(L"Warning: skipped %s (error %lu)", filename, err); skipped++; } else { log_err(L"Failed to copy %s -> %s (error %lu)", src, dest, err); fclose(f); return -1; } } } fclose(f); if (skipped > 0) log_msg(L"Warning: %d GPU driver file(s) skipped (will be copied by agent at boot)", skipped); return count; } static int do_to_vhdx(const wchar_t *iso_path_arg, int image_index, int size_gb, const wchar_t *manifest_path, const wchar_t *output_path_arg) { int exit_code = 1; DWORD result; wchar_t iso_path[MAX_PATH]; wchar_t vhdx_path[MAX_PATH]; /* ISO mount state */ HANDLE iso_handle = INVALID_HANDLE_VALUE; wchar_t iso_drive = 0; DWORD cdrom_before = 0; wchar_t iso_root[4]; /* VHDX state */ HANDLE vhdx_handle = INVALID_HANDLE_VALUE; HANDLE disk_handle = INVALID_HANDLE_VALUE; wchar_t phys_path[MAX_PATH]; DWORD disk_number = 0; /* Mount points */ wchar_t efi_mount[MAX_PATH] = {0}; wchar_t win_mount[MAX_PATH] = {0}; BOOL efi_mounted = FALSE; BOOL win_mounted = FALSE; /* WIM/ESD path found on ISO */ wchar_t wim_path[MAX_PATH]; /* Resolve ISO path */ if (!GetFullPathNameW(iso_path_arg, MAX_PATH, iso_path, NULL)) { log_err(L"Invalid path: %s", iso_path_arg); return 1; } if (GetFileAttributesW(iso_path) == INVALID_FILE_ATTRIBUTES) { log_err(L"File not found: %s", iso_path); return 1; } if (output_path_arg) { if (!GetFullPathNameW(output_path_arg, MAX_PATH, vhdx_path, NULL)) { log_err(L"Invalid output path: %s", output_path_arg); return 1; } ensure_parent_dirs(vhdx_path); } else { build_output_path(vhdx_path, MAX_PATH, iso_path, L".vhdx"); } /* ---- Step 1: Mount the ISO ---- */ log_msg(L"Mounting ISO..."); { VIRTUAL_STORAGE_TYPE st; OPEN_VIRTUAL_DISK_PARAMETERS op; ZeroMemory(&st, sizeof(st)); st.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_ISO; ZeroMemory(&op, sizeof(op)); op.Version = OPEN_VIRTUAL_DISK_VERSION_1; cdrom_before = snapshot_cdrom_drives(); result = OpenVirtualDisk(&st, iso_path, VIRTUAL_DISK_ACCESS_READ, OPEN_VIRTUAL_DISK_FLAG_NONE, &op, &iso_handle); if (result != ERROR_SUCCESS) { log_err(L"OpenVirtualDisk(ISO) failed (error %lu)", result); goto cleanup; } result = AttachVirtualDisk(iso_handle, NULL, ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY, 0, NULL, NULL); if (result != ERROR_SUCCESS) { log_err(L"AttachVirtualDisk(ISO) failed (error %lu)", result); goto cleanup; } /* Wait for drive letter */ for (int i = 0; i < 20; i++) { DWORD cdrom_now = snapshot_cdrom_drives(); DWORD new_drives = cdrom_now & ~cdrom_before; if (new_drives) { for (int b = 0; b < 26; b++) { if (new_drives & (1u << b)) { iso_drive = (wchar_t)(L'A' + b); break; } } break; } Sleep(500); } if (!iso_drive) { log_err(L"Mounted ISO but no drive letter appeared"); goto cleanup; } } swprintf_s(iso_root, 4, L"%c:\\", iso_drive); /* Wait for filesystem to be accessible */ { wchar_t label[64]; int ready = 0; for (int i = 0; i < 30; i++) { if (GetVolumeInformationW(iso_root, label, 64, NULL, NULL, NULL, NULL, 0)) { ready = 1; break; } Sleep(500); } if (!ready) { log_err(L"ISO volume never became accessible"); goto cleanup; } } /* ---- Step 2: Find install.wim or install.esd ---- */ swprintf_s(wim_path, MAX_PATH, L"%c:\\sources\\install.wim", iso_drive); if (GetFileAttributesW(wim_path) == INVALID_FILE_ATTRIBUTES) { swprintf_s(wim_path, MAX_PATH, L"%c:\\sources\\install.esd", iso_drive); if (GetFileAttributesW(wim_path) == INVALID_FILE_ATTRIBUTES) { log_err(L"Neither install.wim nor install.esd found on ISO"); goto cleanup; } } { const wchar_t *wim_name = wcsrchr(wim_path, L'\\'); wim_name = wim_name ? wim_name + 1 : wim_path; log_msg(L"Found %s", wim_name); } /* ---- Step 2.5: Detect image language ---- */ { wchar_t dism_cmd[1024]; char dism_output[8192]; wchar_t sys_dir[MAX_PATH]; const char *detected_lang = "en-US"; /* fallback */ GetSystemDirectoryW(sys_dir, MAX_PATH); swprintf_s(dism_cmd, 1024, L"%s\\dism.exe /English /Get-WimInfo /WimFile:\"%s\" /Index:%d", sys_dir, wim_path, image_index); if (run_command_capture(dism_cmd, dism_output, sizeof(dism_output)) == 0) { /* Look for "xx-YY (Default)" pattern in DISM output */ char *p = strstr(dism_output, "(Default)"); if (p) { /* Walk backwards to find the language tag */ char *end = p; while (end > dism_output && *(end - 1) == ' ') end--; if (end > dism_output) { char *start = end; while (start > dism_output && *(start - 1) != ' ' && *(start - 1) != '\n' && *(start - 1) != '\r' && *(start - 1) != '\t') start--; if (start < end) { *end = '\0'; detected_lang = start; } } } } printf("LANG:%s\n", detected_lang); fflush(stdout); } /* ---- Step 3: Create VHDX ---- */ log_msg(L"Creating %d GB VHDX...", size_gb); { VIRTUAL_STORAGE_TYPE st; CREATE_VIRTUAL_DISK_PARAMETERS params; st.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; st.VendorId = VHDX_VENDOR_MS; ZeroMemory(¶ms, sizeof(params)); params.Version = CREATE_VIRTUAL_DISK_VERSION_2; params.Version2.MaximumSize = (ULONGLONG)size_gb * 1024ULL * 1024ULL * 1024ULL; params.Version2.BlockSizeInBytes = 0; params.Version2.SectorSizeInBytes = 512; params.Version2.PhysicalSectorSizeInBytes = 4096; result = CreateVirtualDisk(&st, vhdx_path, VIRTUAL_DISK_ACCESS_NONE, NULL, CREATE_VIRTUAL_DISK_FLAG_NONE, 0, ¶ms, NULL, &vhdx_handle); if (result != ERROR_SUCCESS) { log_err(L"CreateVirtualDisk failed (error %lu)", result); goto cleanup; } } /* ---- Step 4: Attach VHDX ---- */ { ATTACH_VIRTUAL_DISK_PARAMETERS ap; ZeroMemory(&ap, sizeof(ap)); ap.Version = ATTACH_VIRTUAL_DISK_VERSION_1; result = AttachVirtualDisk(vhdx_handle, NULL, ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER, 0, &ap, NULL); if (result != ERROR_SUCCESS) { log_err(L"AttachVirtualDisk(VHDX) failed (error %lu)", result); goto cleanup; } } /* Get physical path */ { DWORD phys_len = sizeof(phys_path); result = GetVirtualDiskPhysicalPath(vhdx_handle, &phys_len, phys_path); if (result != ERROR_SUCCESS) { log_err(L"GetVirtualDiskPhysicalPath failed (error %lu)", result); goto cleanup; } disk_number = get_disk_number_from_path(phys_path); } /* Open the physical disk */ disk_handle = CreateFileW(phys_path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (disk_handle == INVALID_HANDLE_VALUE) { log_err(L"Failed to open disk %s (error %lu)", phys_path, GetLastError()); goto cleanup; } /* ---- Step 5: Initialize GPT and create partitions ---- */ log_msg(L"Partitioning disk..."); { CREATE_DISK cd; DWORD bytes; ZeroMemory(&cd, sizeof(cd)); cd.PartitionStyle = PARTITION_STYLE_GPT; /* Let Windows generate a disk GUID */ CoCreateGuid(&cd.Gpt.DiskId); if (!DeviceIoControl(disk_handle, IOCTL_DISK_CREATE_DISK, &cd, sizeof(cd), NULL, 0, &bytes, NULL)) { log_err(L"IOCTL_DISK_CREATE_DISK failed (error %lu)", GetLastError()); goto cleanup; } } /* Refresh partition table */ { DWORD bytes; DeviceIoControl(disk_handle, IOCTL_DISK_UPDATE_PROPERTIES, NULL, 0, NULL, 0, &bytes, NULL); } /* Create EFI, MSR, and Windows partitions */ { DWORD bytes; DWORD layout_size; DRIVE_LAYOUT_INFORMATION_EX *layout; PARTITION_INFORMATION_EX *p; LARGE_INTEGER efi_size, msr_size; LARGE_INTEGER efi_offset; GET_LENGTH_INFORMATION leninfo; /* Get total disk size */ if (!DeviceIoControl(disk_handle, IOCTL_DISK_GET_LENGTH_INFO, NULL, 0, &leninfo, sizeof(leninfo), &bytes, NULL)) { log_err(L"IOCTL_DISK_GET_LENGTH_INFO failed (error %lu)", GetLastError()); goto cleanup; } efi_size.QuadPart = (LONGLONG)EFI_SIZE_MB * 1024LL * 1024LL; msr_size.QuadPart = (LONGLONG)MSR_SIZE_MB * 1024LL * 1024LL; efi_offset.QuadPart = 1024LL * 1024LL; /* 1MB offset (standard GPT alignment) */ /* Allocate layout buffer for 3 partitions */ layout_size = (DWORD)(sizeof(DRIVE_LAYOUT_INFORMATION_EX) + 3 * sizeof(PARTITION_INFORMATION_EX)); layout = (DRIVE_LAYOUT_INFORMATION_EX *)calloc(1, layout_size); if (!layout) { log_err(L"Out of memory"); goto cleanup; } layout->PartitionStyle = PARTITION_STYLE_GPT; layout->PartitionCount = 3; CoCreateGuid(&layout->Gpt.DiskId); /* Partition 1: EFI System Partition */ p = &layout->PartitionEntry[0]; p->PartitionStyle = PARTITION_STYLE_GPT; p->StartingOffset.QuadPart = efi_offset.QuadPart; p->PartitionLength.QuadPart = efi_size.QuadPart; p->PartitionNumber = 1; /* PARTITION_SYSTEM_GUID = {c12a7328-f81f-11d2-ba4b-00a0c93ec93b} */ p->Gpt.PartitionType.Data1 = 0xc12a7328; p->Gpt.PartitionType.Data2 = 0xf81f; p->Gpt.PartitionType.Data3 = 0x11d2; p->Gpt.PartitionType.Data4[0] = 0xba; p->Gpt.PartitionType.Data4[1] = 0x4b; p->Gpt.PartitionType.Data4[2] = 0x00; p->Gpt.PartitionType.Data4[3] = 0xa0; p->Gpt.PartitionType.Data4[4] = 0xc9; p->Gpt.PartitionType.Data4[5] = 0x3e; p->Gpt.PartitionType.Data4[6] = 0xc9; p->Gpt.PartitionType.Data4[7] = 0x3b; CoCreateGuid(&p->Gpt.PartitionId); wcscpy_s(p->Gpt.Name, 36, L"EFI System Partition"); /* Partition 2: Microsoft Reserved */ p = &layout->PartitionEntry[1]; p->PartitionStyle = PARTITION_STYLE_GPT; p->StartingOffset.QuadPart = efi_offset.QuadPart + efi_size.QuadPart; p->PartitionLength.QuadPart = msr_size.QuadPart; p->PartitionNumber = 2; /* PARTITION_MSFT_RESERVED_GUID = {e3c9e316-0b5c-4db8-817d-f92df00215ae} */ p->Gpt.PartitionType.Data1 = 0xe3c9e316; p->Gpt.PartitionType.Data2 = 0x0b5c; p->Gpt.PartitionType.Data3 = 0x4db8; p->Gpt.PartitionType.Data4[0] = 0x81; p->Gpt.PartitionType.Data4[1] = 0x7d; p->Gpt.PartitionType.Data4[2] = 0xf9; p->Gpt.PartitionType.Data4[3] = 0x2d; p->Gpt.PartitionType.Data4[4] = 0xf0; p->Gpt.PartitionType.Data4[5] = 0x02; p->Gpt.PartitionType.Data4[6] = 0x15; p->Gpt.PartitionType.Data4[7] = 0xae; CoCreateGuid(&p->Gpt.PartitionId); /* Partition 3: Windows (Basic Data) — uses remaining space minus 1MB at end */ p = &layout->PartitionEntry[2]; p->PartitionStyle = PARTITION_STYLE_GPT; p->StartingOffset.QuadPart = efi_offset.QuadPart + efi_size.QuadPart + msr_size.QuadPart; p->PartitionLength.QuadPart = leninfo.Length.QuadPart - p->StartingOffset.QuadPart - 1024LL * 1024LL; p->PartitionNumber = 3; /* PARTITION_BASIC_DATA_GUID = {ebd0a0a2-b9e5-4433-87c0-68b6b72699c7} */ p->Gpt.PartitionType.Data1 = 0xebd0a0a2; p->Gpt.PartitionType.Data2 = 0xb9e5; p->Gpt.PartitionType.Data3 = 0x4433; p->Gpt.PartitionType.Data4[0] = 0x87; p->Gpt.PartitionType.Data4[1] = 0xc0; p->Gpt.PartitionType.Data4[2] = 0x68; p->Gpt.PartitionType.Data4[3] = 0xb6; p->Gpt.PartitionType.Data4[4] = 0xb7; p->Gpt.PartitionType.Data4[5] = 0x26; p->Gpt.PartitionType.Data4[6] = 0x99; p->Gpt.PartitionType.Data4[7] = 0xc7; CoCreateGuid(&p->Gpt.PartitionId); if (!DeviceIoControl(disk_handle, IOCTL_DISK_SET_DRIVE_LAYOUT_EX, layout, layout_size, NULL, 0, &bytes, NULL)) { log_err(L"IOCTL_DISK_SET_DRIVE_LAYOUT_EX failed (error %lu)", GetLastError()); free(layout); goto cleanup; } free(layout); } /* Refresh again after partitioning */ { DWORD bytes; DeviceIoControl(disk_handle, IOCTL_DISK_UPDATE_PROPERTIES, NULL, 0, NULL, 0, &bytes, NULL); } /* Give the OS time to enumerate the new volumes */ Sleep(2000); /* ---- Step 6: Find and mount the EFI and Windows volumes ---- */ log_msg(L"Formatting partitions..."); { wchar_t efi_vol[MAX_PATH]; if (!find_volume_for_partition(disk_number, 1, efi_vol, MAX_PATH)) { log_err(L"Could not find EFI partition volume"); goto cleanup; } if (!create_temp_mount_dir(L"asb_efi", efi_mount, MAX_PATH)) { log_err(L"Failed to create temp mount directory"); goto cleanup; } if (!mount_volume_to_dir(efi_vol, efi_mount)) { log_err(L"Failed to mount EFI partition (error %lu)", GetLastError()); goto cleanup; } efi_mounted = TRUE; } { wchar_t win_vol[MAX_PATH]; if (!find_volume_for_partition(disk_number, 3, win_vol, MAX_PATH)) { log_err(L"Could not find Windows partition volume"); goto cleanup; } if (!create_temp_mount_dir(L"asb_win", win_mount, MAX_PATH)) { log_err(L"Failed to create temp mount directory"); goto cleanup; } if (!mount_volume_to_dir(win_vol, win_mount)) { log_err(L"Failed to mount Windows partition (error %lu)", GetLastError()); goto cleanup; } win_mounted = TRUE; } /* ---- Step 7: Format partitions ---- */ if (!format_volume(efi_mount, L"FAT32", L"SYSTEM")) { log_err(L"Failed to format EFI partition"); goto cleanup; } if (!format_volume(win_mount, L"NTFS", L"Windows")) { log_err(L"Failed to format Windows partition"); goto cleanup; } /* ---- Step 8: Apply WIM/ESD image using dism.exe ---- */ { wchar_t cmd[1024]; wchar_t sys_dir[MAX_PATH]; wchar_t win_clean[MAX_PATH]; int ret; GetSystemDirectoryW(sys_dir, MAX_PATH); /* Strip trailing backslash from win_mount for dism */ wcscpy_s(win_clean, MAX_PATH, win_mount); { size_t wl = wcslen(win_clean); if (wl > 0 && win_clean[wl - 1] == L'\\') win_clean[wl - 1] = L'\0'; } swprintf_s(cmd, 1024, L"%s\\dism.exe /Apply-Image /ImageFile:\"%s\" /Index:%d /ApplyDir:\"%s\"", sys_dir, wim_path, image_index, win_clean); ret = run_command_with_progress(cmd, L"Applying Windows image..."); if (ret != 0) { log_err(L"Failed to apply Windows image (dism exit code %d)", ret); goto cleanup; } } /* ---- Step 9: Stage additional files from manifest ---- */ if (manifest_path) { log_msg(L"Staging files..."); { int staged = stage_files(manifest_path, win_mount); if (staged < 0) { log_err(L"File staging failed"); goto cleanup; } if (staged > 0) log_msg(L"Staged %d file(s)", staged); } } /* ---- Step 10: Set up UEFI boot files with bcdboot ---- */ log_msg(L"Installing boot files..."); { wchar_t windows_dir[MAX_PATH]; wchar_t efi_clean[MAX_PATH]; wchar_t cmd[1024]; wchar_t sys_dir[MAX_PATH]; int ret; GetSystemDirectoryW(sys_dir, MAX_PATH); swprintf_s(windows_dir, MAX_PATH, L"%sWindows", win_mount); /* Strip trailing backslash from efi_mount for bcdboot */ wcscpy_s(efi_clean, MAX_PATH, efi_mount); { size_t el = wcslen(efi_clean); if (el > 0 && efi_clean[el - 1] == L'\\') efi_clean[el - 1] = L'\0'; } swprintf_s(cmd, 1024, L"%s\\bcdboot.exe \"%s\" /s \"%s\" /f UEFI", sys_dir, windows_dir, efi_clean); ret = run_command(cmd, TRUE); if (ret != 0) { log_err(L"Failed to install boot files (bcdboot exit code %d)", ret); goto cleanup; } } log_done(vhdx_path); exit_code = 0; cleanup: /* Unmount volumes */ if (win_mounted) { unmount_volume_from_dir(win_mount); RemoveDirectoryW(win_mount); } if (efi_mounted) { unmount_volume_from_dir(efi_mount); RemoveDirectoryW(efi_mount); } /* Close disk handle before detaching */ if (disk_handle != INVALID_HANDLE_VALUE) CloseHandle(disk_handle); /* Detach and close VHDX */ if (vhdx_handle != INVALID_HANDLE_VALUE) { DetachVirtualDisk(vhdx_handle, DETACH_VIRTUAL_DISK_FLAG_NONE, 0); CloseHandle(vhdx_handle); } /* Detach and close ISO */ if (iso_handle != INVALID_HANDLE_VALUE) { DetachVirtualDisk(iso_handle, DETACH_VIRTUAL_DISK_FLAG_NONE, 0); CloseHandle(iso_handle); } /* If we failed, delete the partial VHDX */ if (exit_code != 0 && vhdx_path[0]) { DeleteFileW(vhdx_path); } return exit_code; } /* ====================================================================== * Noprompt ISO rebuild (original functionality) * ====================================================================== */ static int do_noprompt(const wchar_t *iso_path_arg) { HRESULT hr; DWORD result; int exit_code = 1; /* State that needs cleanup */ HANDLE iso_handle = INVALID_HANDLE_VALUE; wchar_t iso_drive = 0; DWORD cdrom_before = 0; IFileSystemImage *pImage = NULL; IFsiDirectoryItem *pRoot = NULL; IFsiDirectoryItem *pBootDir = NULL; IBootOptions *pBootOpts = NULL; IStream *pBootStream = NULL; IStream *pCdbootStream = NULL; IFileSystemImageResult *pResult = NULL; IStream *pOutStream = NULL; /* Paths */ wchar_t iso_path[MAX_PATH]; wchar_t output_path[MAX_PATH]; wchar_t drive_root[4]; wchar_t noprompt_efisys[MAX_PATH]; wchar_t noprompt_cdboot[MAX_PATH]; wchar_t vol_label[MAX_PATH]; /* Resolve full path */ if (!GetFullPathNameW(iso_path_arg, MAX_PATH, iso_path, NULL)) { log_err(L"Invalid path: %s", iso_path_arg); return 1; } if (GetFileAttributesW(iso_path) == INVALID_FILE_ATTRIBUTES) { log_err(L"File not found: %s", iso_path); return 1; } build_output_path(output_path, MAX_PATH, iso_path, L"_noprompt.iso"); /* ---- Step 1: Initialize COM ---- */ hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if (FAILED(hr)) { log_err(L"CoInitializeEx failed (0x%08X)", hr); return 1; } /* ---- Step 2: Mount the source ISO ---- */ log_msg(L"Mounting %s...", iso_path); { VIRTUAL_STORAGE_TYPE st; OPEN_VIRTUAL_DISK_PARAMETERS op; ZeroMemory(&st, sizeof(st)); st.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_ISO; ZeroMemory(&op, sizeof(op)); op.Version = OPEN_VIRTUAL_DISK_VERSION_1; cdrom_before = snapshot_cdrom_drives(); log_msg(L"CDROM drives before mount: 0x%08X", cdrom_before); result = OpenVirtualDisk(&st, iso_path, VIRTUAL_DISK_ACCESS_READ, OPEN_VIRTUAL_DISK_FLAG_NONE, &op, &iso_handle); if (result != ERROR_SUCCESS) { log_err(L"OpenVirtualDisk failed (error %lu)", result); goto cleanup; } log_msg(L"OpenVirtualDisk succeeded"); result = AttachVirtualDisk(iso_handle, NULL, ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY, 0, NULL, NULL); if (result != ERROR_SUCCESS) { log_err(L"AttachVirtualDisk failed (error %lu)", result); goto cleanup; } log_msg(L"AttachVirtualDisk succeeded"); /* Get the physical path assigned by the system */ { wchar_t phys_path[MAX_PATH]; DWORD phys_len = sizeof(phys_path); result = GetVirtualDiskPhysicalPath(iso_handle, &phys_len, phys_path); if (result == ERROR_SUCCESS) log_msg(L"Physical path: %s", phys_path); else log_msg(L"GetVirtualDiskPhysicalPath failed (error %lu)", result); } /* Wait for the drive letter to appear */ for (int i = 0; i < 20; i++) { DWORD cdrom_now = snapshot_cdrom_drives(); DWORD new_drives = cdrom_now & ~cdrom_before; if (i == 0 || (i % 4 == 0)) log_msg(L"Poll %d: CDROM drives now: 0x%08X, new: 0x%08X", i, cdrom_now, new_drives); if (new_drives) { for (int b = 0; b < 26; b++) { if (new_drives & (1u << b)) { iso_drive = (wchar_t)(L'A' + b); break; } } break; } Sleep(500); } if (!iso_drive) { wchar_t root[4] = L"A:\\"; for (int i = 0; i < 26; i++) { root[0] = (wchar_t)(L'A' + i); UINT dt = GetDriveTypeW(root); if (dt != DRIVE_NO_ROOT_DIR) log_msg(L" %c: type=%u", (wchar_t)(L'A' + i), dt); } log_err(L"Mounted ISO but no new drive letter appeared"); goto cleanup; } } swprintf_s(drive_root, 4, L"%c:\\", iso_drive); log_msg(L"Mounted at %c:\\", iso_drive); /* Wait for the filesystem to become accessible */ { int ready = 0; wchar_t test_label[64]; for (int i = 0; i < 30; i++) { if (GetVolumeInformationW(drive_root, test_label, 64, NULL, NULL, NULL, NULL, 0)) { ready = 1; break; } log_msg(L"Waiting for volume to be ready... (attempt %d, error %lu)", i + 1, GetLastError()); Sleep(500); } if (!ready) { log_err(L"Volume %c:\\ never became accessible", iso_drive); goto cleanup; } } /* ---- Step 3: Verify noprompt files ---- */ swprintf_s(noprompt_efisys, MAX_PATH, L"%c:\\efi\\microsoft\\boot\\efisys_noprompt.bin", iso_drive); swprintf_s(noprompt_cdboot, MAX_PATH, L"%c:\\efi\\microsoft\\boot\\cdboot_noprompt.efi", iso_drive); log_msg(L"Checking: %s", noprompt_efisys); if (GetFileAttributesW(noprompt_efisys) == INVALID_FILE_ATTRIBUTES) { log_err(L"efisys_noprompt.bin not found on ISO (error %lu)", GetLastError()); goto cleanup; } log_msg(L"Checking: %s", noprompt_cdboot); if (GetFileAttributesW(noprompt_cdboot) == INVALID_FILE_ATTRIBUTES) { log_err(L"cdboot_noprompt.efi not found on ISO (error %lu)", GetLastError()); goto cleanup; } log_msg(L"Found efisys_noprompt.bin and cdboot_noprompt.efi"); /* ---- Step 4: Read volume label ---- */ vol_label[0] = L'\0'; GetVolumeInformationW(drive_root, vol_label, MAX_PATH, NULL, NULL, NULL, NULL, 0); if (vol_label[0]) log_msg(L"Volume label: %s", vol_label); /* ---- Step 5: Create IMAPI2 IFileSystemImage ---- */ hr = CoCreateInstance(&CLSID_FSImage, NULL, CLSCTX_ALL, &IID_IFSImage, (void **)&pImage); if (FAILED(hr)) { log_err(L"CoCreateInstance(MsftFileSystemImage) failed (0x%08X)", hr); goto cleanup; } pImage->lpVtbl->put_FileSystemsToCreate(pImage, FsiFileSystemUDF); pImage->lpVtbl->put_UDFRevision(pImage, 0x0150); pImage->lpVtbl->put_FreeMediaBlocks(pImage, 4194304); if (vol_label[0]) { BSTR bstrLabel = SysAllocString(vol_label); pImage->lpVtbl->put_VolumeName(pImage, bstrLabel); SysFreeString(bstrLabel); } /* ---- Step 6: Set UEFI boot image ---- */ hr = SHCreateStreamOnFileW(noprompt_efisys, STGM_READ | STGM_SHARE_DENY_WRITE, &pBootStream); if (FAILED(hr)) { log_err(L"SHCreateStreamOnFileW(efisys_noprompt.bin) failed (0x%08X)", hr); goto cleanup; } hr = CoCreateInstance(&CLSID_BootOpts, NULL, CLSCTX_ALL, &IID_IBootOpts, (void **)&pBootOpts); if (FAILED(hr)) { log_err(L"CoCreateInstance(BootOptions) failed (0x%08X)", hr); goto cleanup; } pBootOpts->lpVtbl->AssignBootImage(pBootOpts, pBootStream); pBootOpts->lpVtbl->put_PlatformId(pBootOpts, 0xEF); pBootOpts->lpVtbl->put_Emulation(pBootOpts, 0); hr = pImage->lpVtbl->put_BootImageOptions(pImage, pBootOpts); if (FAILED(hr)) { log_err(L"put_BootImageOptions failed (0x%08X)", hr); goto cleanup; } /* ---- Step 7: Import source ISO file tree ---- */ log_msg(L"Importing files from source ISO..."); hr = pImage->lpVtbl->get_Root(pImage, &pRoot); if (FAILED(hr)) { log_err(L"get_Root failed (0x%08X)", hr); goto cleanup; } { BSTR bstrDrive = SysAllocString(drive_root); hr = pRoot->lpVtbl->AddTree(pRoot, bstrDrive, VARIANT_FALSE); SysFreeString(bstrDrive); } if (FAILED(hr)) { log_err(L"AddTree failed (0x%08X)", hr); goto cleanup; } /* ---- Step 8: Swap boot files ---- */ log_msg(L"Swapping boot files with noprompt variants..."); hr = navigate_fsi_path(pRoot, &pBootDir, L"efi", L"microsoft", L"boot", NULL); if (FAILED(hr)) { log_err(L"Failed to navigate to \\efi\\microsoft\\boot\\ (0x%08X)", hr); goto cleanup; } { BSTR name; name = SysAllocString(L"efisys.bin"); hr = pBootDir->lpVtbl->Remove(pBootDir, name); SysFreeString(name); if (FAILED(hr)) { log_err(L"Failed to remove efisys.bin from tree (0x%08X)", hr); goto cleanup; } name = SysAllocString(L"cdboot.efi"); hr = pBootDir->lpVtbl->Remove(pBootDir, name); SysFreeString(name); if (FAILED(hr)) { log_err(L"Failed to remove cdboot.efi from tree (0x%08X)", hr); goto cleanup; } } { BSTR name; IStream *stream = NULL; hr = SHCreateStreamOnFileW(noprompt_efisys, STGM_READ | STGM_SHARE_DENY_WRITE, &stream); if (FAILED(hr)) { log_err(L"SHCreateStreamOnFileW(efisys_noprompt) failed (0x%08X)", hr); goto cleanup; } name = SysAllocString(L"efisys.bin"); hr = pBootDir->lpVtbl->AddFile(pBootDir, name, stream); SysFreeString(name); stream->lpVtbl->Release(stream); stream = NULL; if (FAILED(hr)) { log_err(L"AddFile(efisys.bin) failed (0x%08X)", hr); goto cleanup; } hr = SHCreateStreamOnFileW(noprompt_cdboot, STGM_READ | STGM_SHARE_DENY_WRITE, &stream); if (FAILED(hr)) { log_err(L"SHCreateStreamOnFileW(cdboot_noprompt) failed (0x%08X)", hr); goto cleanup; } name = SysAllocString(L"cdboot.efi"); hr = pBootDir->lpVtbl->AddFile(pBootDir, name, stream); SysFreeString(name); stream->lpVtbl->Release(stream); stream = NULL; if (FAILED(hr)) { log_err(L"AddFile(cdboot.efi) failed (0x%08X)", hr); goto cleanup; } } /* ---- Step 9: Build and write the patched ISO ---- */ log_msg(L"Building patched ISO (this may take several minutes)..."); hr = pImage->lpVtbl->CreateResultImage(pImage, &pResult); if (FAILED(hr)) { log_err(L"CreateResultImage failed (0x%08X)", hr); goto cleanup; } hr = pResult->lpVtbl->get_ImageStream(pResult, &pOutStream); if (FAILED(hr)) { log_err(L"get_ImageStream failed (0x%08X)", hr); goto cleanup; } log_msg(L"Writing %s...", output_path); hr = write_stream_to_file(pOutStream, output_path); if (FAILED(hr)) { log_err(L"write_stream_to_file failed (0x%08X)", hr); goto cleanup; } log_msg(L"Done."); exit_code = 0; cleanup: if (pOutStream) pOutStream->lpVtbl->Release(pOutStream); if (pResult) pResult->lpVtbl->Release(pResult); if (pCdbootStream) pCdbootStream->lpVtbl->Release(pCdbootStream); if (pBootStream) pBootStream->lpVtbl->Release(pBootStream); if (pBootOpts) pBootOpts->lpVtbl->Release(pBootOpts); if (pBootDir) pBootDir->lpVtbl->Release(pBootDir); if (pRoot) pRoot->lpVtbl->Release(pRoot); if (pImage) pImage->lpVtbl->Release(pImage); if (iso_handle != INVALID_HANDLE_VALUE) { DetachVirtualDisk(iso_handle, DETACH_VIRTUAL_DISK_FLAG_NONE, 0); CloseHandle(iso_handle); } CoUninitialize(); return exit_code; } /* ====================================================================== * --qcow2-to-vhdx: convert an Ubuntu qcow2 cloud image to a bootable VHDX * ====================================================================== * * Pipeline: qcow2 -> raw disk image -> append VHD-fixed footer -> use * Microsoft's VirtDisk API (CreateVirtualDisk SourcePath) to copy the * VHD into a fresh dynamic VHDX. Output is a Hyper-V Gen 2 boot-capable * image suitable for differencing children. * * Lives in iso-patch.exe (not the host DLL) so the format-conversion * work runs out-of-process: process isolation, separate progress pipe, * cleaner debugging. Run by `disk_util.c::ensure_ubuntu_cloud_image_cached` * after it downloads the qcow2. * ====================================================================== */ /* Emit a DEBUG line on stdout — recognized by the disk_util.c parser alongside STATUS/PROGRESS/ERROR/DONE. Use for fine-grained diagnostics (inflate state, qcow2 table walks, etc.). */ static void log_debug(const wchar_t *fmt, ...) { va_list ap; va_start(ap, fmt); wprintf(L"DEBUG:"); vwprintf(fmt, ap); wprintf(L"\n"); fflush(stdout); va_end(ap); } /* Enable a Windows token privilege by name (e.g. SE_MANAGE_VOLUME_NAME). Admin tokens have these privileges present-but-disabled by default; AttachVirtualDisk on a freshly-created VHDX requires SE_MANAGE_VOLUME to actually be ENABLED. AppSandbox.exe gets it implicitly via Hyper-V Administrators membership, but iso-patch.exe spawned from a vanilla elevated cmd doesn't. Call this at startup; it's a no-op if the privilege isn't in the token at all. */ static BOOL enable_privilege(LPCWSTR name) { HANDLE token = NULL; TOKEN_PRIVILEGES tp; BOOL ok = FALSE; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) return FALSE; tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if (LookupPrivilegeValueW(NULL, name, &tp.Privileges[0].Luid)) { AdjustTokenPrivileges(token, FALSE, &tp, 0, NULL, NULL); /* AdjustTokenPrivileges returns TRUE even when the privilege isn't in the token; check GetLastError() for the real story. */ ok = (GetLastError() == ERROR_SUCCESS); } CloseHandle(token); return ok; } /* ---- Big-endian readers/writers (qcow2 + VHD footer are both BE) ---- */ static DWORD read_be32(const BYTE *p) { return ((DWORD)p[0] << 24) | ((DWORD)p[1] << 16) | ((DWORD)p[2] << 8) | (DWORD)p[3]; } static ULONGLONG read_be64(const BYTE *p) { return ((ULONGLONG)read_be32(p) << 32) | (ULONGLONG)read_be32(p + 4); } static void put_be32(BYTE *p, DWORD v) { p[0] = (BYTE)(v >> 24); p[1] = (BYTE)(v >> 16); p[2] = (BYTE)(v >> 8); p[3] = (BYTE)v; } static void put_be64(BYTE *p, ULONGLONG v) { p[0] = (BYTE)(v >> 56); p[1] = (BYTE)(v >> 48); p[2] = (BYTE)(v >> 40); p[3] = (BYTE)(v >> 32); p[4] = (BYTE)(v >> 24); p[5] = (BYTE)(v >> 16); p[6] = (BYTE)(v >> 8); p[7] = (BYTE)v; } /* Read exactly `len` bytes at `offset` from `h`. */ static BOOL pread_full(HANDLE h, ULONGLONG offset, void *buf, DWORD len) { LARGE_INTEGER li; DWORD got; BYTE *p = (BYTE *)buf; li.QuadPart = (LONGLONG)offset; if (!SetFilePointerEx(h, li, NULL, FILE_BEGIN)) return FALSE; while (len > 0) { if (!ReadFile(h, p, len, &got, NULL) || got == 0) return FALSE; p += got; len -= got; } return TRUE; } static BOOL pwrite_full(HANDLE h, ULONGLONG offset, const void *buf, DWORD len) { LARGE_INTEGER li; DWORD wrote; const BYTE *p = (const BYTE *)buf; li.QuadPart = (LONGLONG)offset; if (!SetFilePointerEx(h, li, NULL, FILE_BEGIN)) return FALSE; while (len > 0) { if (!WriteFile(h, p, len, &wrote, NULL) || wrote == 0) return FALSE; p += wrote; len -= wrote; } return TRUE; } /* ===================== DEFLATE inflater (RFC 1951) ===================== * * Standalone: caller supplies a finite output buffer (one qcow2 cluster * = 64 KB for our use), we decompress until either a BFINAL block ends * or the output is full. Strict-no-deps: no zlib, no contrib code. * * Debug logging is structured per call: when an error fires, we emit a * DEBUG: line tagged with the failing stage + state (block index, src * position, dst position, last symbol attempted, etc.) so the host-side * log shows where to look. Healthy paths emit one DEBUG: per block at * minimum to confirm progress. */ static const WORD kLenBase[29] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 }; static const BYTE kLenExtra[29] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; static const WORD kDistBase[30] = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 }; static const BYTE kDistExtra[30] = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }; static const BYTE kCLOrder[19] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; typedef struct { SHORT count[16]; SHORT symbol[288]; } HuffTable; typedef struct { const BYTE *src; size_t src_len; size_t src_pos; BYTE *dst; size_t dst_cap; size_t dst_pos; DWORD bit_buf; int bit_count; int error; int block_idx; /* debug: which block within this stream */ const char *fail_stage; /* debug: what step failed */ } InflateState; static int infl_bits(InflateState *s, int n) { int v; while (s->bit_count < n) { if (s->src_pos >= s->src_len) { s->error = 1; if (!s->fail_stage) s->fail_stage = "bits-eof"; return -1; } s->bit_buf |= ((DWORD)s->src[s->src_pos++]) << s->bit_count; s->bit_count += 8; } v = (int)(s->bit_buf & (((DWORD)1 << n) - 1)); s->bit_buf >>= n; s->bit_count -= n; return v; } static int infl_build(HuffTable *t, const BYTE *lens, int n) { int left = 1; int i; SHORT offs[16]; for (i = 0; i < 16; i++) t->count[i] = 0; for (i = 0; i < n; i++) { if (lens[i] >= 16) return -1; t->count[lens[i]]++; } if (t->count[0] == n) return 0; for (i = 1; i <= 15; i++) { left <<= 1; left -= t->count[i]; if (left < 0) return -1; } offs[1] = 0; for (i = 1; i < 15; i++) offs[i+1] = offs[i] + t->count[i]; for (i = 0; i < n; i++) { if (lens[i] != 0) { t->symbol[offs[lens[i]]++] = (SHORT)i; } } return 0; } static int infl_decode(InflateState *s, const HuffTable *t) { int code = 0, first = 0, index = 0, len; for (len = 1; len <= 15; len++) { int bit = infl_bits(s, 1); if (bit < 0) return -1; code = (code << 1) | bit; if (code - t->count[len] < first) { return t->symbol[index + (code - first)]; } index += t->count[len]; first = (first + t->count[len]) << 1; } s->error = 1; if (!s->fail_stage) s->fail_stage = "decode-no-match-15bits"; return -1; } static int infl_codes(InflateState *s, const HuffTable *litlen, const HuffTable *dist) { int sym; while ((sym = infl_decode(s, litlen)) >= 0) { if (sym < 256) { if (s->dst_pos >= s->dst_cap) { s->error = 1; s->fail_stage = "literal-overflow"; return -1; } s->dst[s->dst_pos++] = (BYTE)sym; } else if (sym == 256) { return 0; } else { int idx = sym - 257; int extra, length, dist_sym, dist_extra, distance; if (idx < 0 || idx >= 29) { s->error = 1; s->fail_stage = "bad-length-sym"; return -1; } extra = infl_bits(s, kLenExtra[idx]); if (extra < 0) return -1; length = kLenBase[idx] + extra; dist_sym = infl_decode(s, dist); if (dist_sym < 0 || dist_sym >= 30) { s->error = 1; s->fail_stage = "bad-dist-sym"; return -1; } dist_extra = infl_bits(s, kDistExtra[dist_sym]); if (dist_extra < 0) return -1; distance = kDistBase[dist_sym] + dist_extra; if ((size_t)distance > s->dst_pos) { s->error = 1; s->fail_stage = "dist-past-start"; return -1; } { size_t base = s->dst_pos - distance; int i; if (s->dst_pos + length > s->dst_cap) { s->error = 1; s->fail_stage = "copy-overflow"; return -1; } for (i = 0; i < length; i++) { s->dst[s->dst_pos] = s->dst[base + i]; s->dst_pos++; } } } } if (!s->fail_stage) s->fail_stage = "decode-litlen"; return -1; } static int infl_stored(InflateState *s) { int len, nlen; s->bit_buf = 0; s->bit_count = 0; if (s->src_pos + 4 > s->src_len) { s->error = 1; s->fail_stage = "stored-header-eof"; return -1; } len = s->src[s->src_pos++]; len |= ((int)s->src[s->src_pos++]) << 8; nlen = s->src[s->src_pos++]; nlen|= ((int)s->src[s->src_pos++]) << 8; if ((len ^ nlen) != 0xFFFF) { s->error = 1; s->fail_stage = "stored-len-nlen-mismatch"; return -1; } if (s->src_pos + len > s->src_len) { s->error = 1; s->fail_stage = "stored-body-eof"; return -1; } if (s->dst_pos + (size_t)len > s->dst_cap) { s->error = 1; s->fail_stage = "stored-output-overflow"; return -1; } if (len > 0) { memcpy(s->dst + s->dst_pos, s->src + s->src_pos, len); s->src_pos += len; s->dst_pos += len; } return 0; } static int infl_fixed(InflateState *s) { static HuffTable litlen, dist; static int built = 0; if (!built) { BYTE lens[288]; int i; for (i = 0; i < 144; i++) lens[i] = 8; for (i = 144; i < 256; i++) lens[i] = 9; for (i = 256; i < 280; i++) lens[i] = 7; for (i = 280; i < 288; i++) lens[i] = 8; if (infl_build(&litlen, lens, 288) != 0) { s->error = 1; s->fail_stage = "fixed-litlen-build"; return -1; } for (i = 0; i < 30; i++) lens[i] = 5; if (infl_build(&dist, lens, 30) != 0) { s->error = 1; s->fail_stage = "fixed-dist-build"; return -1; } built = 1; } return infl_codes(s, &litlen, &dist); } static int infl_dynamic(InflateState *s) { int hlit, hdist, hclen; BYTE cl_lens[19]; BYTE all_lens[288 + 30]; HuffTable cl_table, litlen, dist; int i, total; hlit = infl_bits(s, 5); if (hlit < 0) return -1; hlit += 257; hdist = infl_bits(s, 5); if (hdist < 0) return -1; hdist += 1; hclen = infl_bits(s, 4); if (hclen < 0) return -1; hclen += 4; if (hlit > 286 || hdist > 30 || hclen > 19) { s->error = 1; s->fail_stage = "dyn-header-out-of-range"; return -1; } for (i = 0; i < 19; i++) cl_lens[i] = 0; for (i = 0; i < hclen; i++) { int v = infl_bits(s, 3); if (v < 0) return -1; cl_lens[kCLOrder[i]] = (BYTE)v; } if (infl_build(&cl_table, cl_lens, 19) != 0) { s->error = 1; s->fail_stage = "cl-table-build"; return -1; } total = hlit + hdist; i = 0; while (i < total) { int sym = infl_decode(s, &cl_table); if (sym < 0) return -1; if (sym < 16) { all_lens[i++] = (BYTE)sym; } else if (sym == 16) { int rep = infl_bits(s, 2); if (rep < 0) return -1; rep += 3; if (i == 0) { s->error = 1; s->fail_stage = "cl-rep16-at-start"; return -1; } while (rep-- && i < total) { all_lens[i] = all_lens[i-1]; i++; } } else if (sym == 17) { int rep = infl_bits(s, 3); if (rep < 0) return -1; rep += 3; while (rep-- && i < total) all_lens[i++] = 0; } else if (sym == 18) { int rep = infl_bits(s, 7); if (rep < 0) return -1; rep += 11; while (rep-- && i < total) all_lens[i++] = 0; } else { s->error = 1; s->fail_stage = "cl-bad-symbol"; return -1; } } if (infl_build(&litlen, all_lens, hlit) != 0) { s->error = 1; s->fail_stage = "dyn-litlen-build"; return -1; } if (infl_build(&dist, all_lens + hlit, hdist) != 0) { s->error = 1; s->fail_stage = "dyn-dist-build"; return -1; } return infl_codes(s, &litlen, &dist); } static int inflate_deflate(BYTE *out, size_t out_max, const BYTE *in, size_t in_len, size_t *out_actual, int verbose) { InflateState s; int bfinal, btype; s.src = in; s.src_len = in_len; s.src_pos = 0; s.dst = out; s.dst_cap = out_max; s.dst_pos = 0; s.bit_buf = 0; s.bit_count = 0; s.error = 0; s.block_idx = 0; s.fail_stage = NULL; do { size_t pos_before = s.src_pos; bfinal = infl_bits(&s, 1); if (bfinal < 0) goto fail; btype = infl_bits(&s, 2); if (btype < 0) goto fail; if (verbose) { log_debug(L"deflate block %d: bfinal=%d btype=%d src_pos=%zu dst_pos=%zu", s.block_idx, bfinal, btype, pos_before, s.dst_pos); } if (btype == 0) { if (infl_stored(&s) != 0) goto fail; } else if (btype == 1) { if (infl_fixed(&s) != 0) goto fail; } else if (btype == 2) { if (infl_dynamic(&s) != 0) goto fail; } else { s.error = 1; s.fail_stage = "btype-3-reserved"; goto fail; } if (s.error) goto fail; s.block_idx++; } while (!bfinal); if (out_actual) *out_actual = s.dst_pos; return 0; fail: log_debug(L"deflate failed at block %d, stage=%S, src_pos=%zu/%zu, dst_pos=%zu/%zu, bits_buffered=%d", s.block_idx, s.fail_stage ? s.fail_stage : "unknown", s.src_pos, s.src_len, s.dst_pos, s.dst_cap, s.bit_count); /* Hex dump the source bytes near the failure (16 bytes before + 16 after). */ { size_t start = (s.src_pos > 16) ? (s.src_pos - 16) : 0; size_t end = (s.src_pos + 16 < s.src_len) ? (s.src_pos + 16) : s.src_len; wchar_t hex[256] = L""; size_t i; wchar_t pair[8]; for (i = start; i < end && wcslen(hex) < 240; i++) { swprintf_s(pair, 8, L"%02x ", s.src[i]); wcscat_s(hex, 256, pair); } log_debug(L" bytes [%zu..%zu]: %s", start, end, hex); } return -1; } static int inflate_zlib(BYTE *out, size_t out_max, const BYTE *in, size_t in_len, size_t *out_actual, int verbose) { if (in_len < 2) { log_debug(L"zlib: stream too short (%zu bytes)", in_len); return -1; } if (verbose) { log_debug(L"zlib header: CMF=0x%02x FLG=0x%02x", in[0], in[1]); } if ((in[0] & 0x0F) != 8) { log_debug(L"zlib: bad CMF method (low nibble != 8)"); return -1; } if ((in[1] & 0x20) != 0) { log_debug(L"zlib: FDICT set (preset dictionary not supported)"); return -1; } if (((((int)in[0]) << 8) | in[1]) % 31 != 0) { log_debug(L"zlib: CMF*256+FLG not multiple of 31 (%d)", (((int)in[0]) << 8) | in[1]); return -1; } return inflate_deflate(out, out_max, in + 2, in_len - 2, out_actual, verbose); } /* ===================== qcow2 -> raw ===================== * * Walks L1 + L2 cluster tables, copies uncompressed clusters verbatim, * decompresses compressed clusters via inflate_zlib, leaves unallocated * clusters as sparse-file holes (NTFS). */ #define QCOW2_MAGIC 0x514649FBu #define QCOW2_L1E_OFFSET_MASK 0x00FFFFFFFFFFFE00ULL #define QCOW2_L2E_COMPRESSED_BIT 0x4000000000000000ULL #define QCOW2_L2E_OFFSET_MASK 0x00FFFFFFFFFFFE00ULL static HRESULT qcow2_to_raw(const wchar_t *src_path, const wchar_t *dst_path) { HANDLE hSrc = INVALID_HANDLE_VALUE; HANDLE hDst = INVALID_HANDLE_VALUE; BYTE header[112]; BYTE *l1_table = NULL; BYTE *l2_table = NULL; BYTE *cluster_buf = NULL; HRESULT hr = E_FAIL; DWORD version, cluster_bits, l1_size, crypt_method; ULONGLONG virtual_size, l1_table_offset, incompat_features; DWORD cluster_size, l2_entries; ULONGLONG total_clusters; DWORD l2_table_bytes; ULONGLONG cluster_idx; int compressed_seen = 0; /* trace: only verbose-log the first few */ hSrc = CreateFileW(src_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (hSrc == INVALID_HANDLE_VALUE) { log_err(L"qcow2_to_raw: open src failed (%lu)", GetLastError()); return HRESULT_FROM_WIN32(GetLastError()); } if (!pread_full(hSrc, 0, header, sizeof(header))) { hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } if (read_be32(header + 0) != QCOW2_MAGIC) { log_err(L"qcow2_to_raw: bad magic (not a qcow2 file)"); goto cleanup; } version = read_be32(header + 4); cluster_bits = read_be32(header + 20); virtual_size = read_be64(header + 24); crypt_method = read_be32(header + 32); l1_size = read_be32(header + 36); l1_table_offset = read_be64(header + 40); incompat_features = (version >= 3) ? read_be64(header + 72) : 0; log_msg(L"qcow2 v%lu, cluster=%lu B, virtual=%llu B", version, 1ul << cluster_bits, virtual_size); log_debug(L"qcow2 header: crypt=%lu l1_size=%lu l1_off=0x%llx incompat=0x%llx", crypt_method, l1_size, l1_table_offset, incompat_features); if (version >= 3) { log_debug(L"qcow2 v3: header_length=%lu compression_type=%d", read_be32(header + 100), header[104]); } if (version != 2 && version != 3) { log_err(L"qcow2_to_raw: unsupported version %lu", version); goto cleanup; } if (crypt_method != 0) { log_err(L"qcow2_to_raw: encrypted qcow2 not supported"); goto cleanup; } if (incompat_features != 0) { log_err(L"qcow2_to_raw: incompat_features=0x%llx not supported", incompat_features); goto cleanup; } if (cluster_bits < 9 || cluster_bits > 21) { log_err(L"qcow2_to_raw: cluster_bits %lu out of range", cluster_bits); goto cleanup; } cluster_size = 1u << cluster_bits; l2_entries = cluster_size / 8; total_clusters = (virtual_size + cluster_size - 1) / cluster_size; { ULONGLONG need_l1 = (total_clusters + l2_entries - 1) / l2_entries; DWORD l1_bytes; /* l1_size is attacker-controlled (header). Reject an undersized table (would under-read against the cluster loop) and an oversized one (l1_size * 8 must not overflow the 32-bit allocation/read length; 0x1FFFFFFF keeps l1_size*8 <= 0xFFFFFFF8). */ if (l1_size < need_l1 || l1_size > 0x1FFFFFFFu) { log_err(L"qcow2_to_raw: invalid l1_size %lu (need %llu)", l1_size, need_l1); goto cleanup; } l1_bytes = (DWORD)((SIZE_T)l1_size * 8); l1_table = (BYTE *)HeapAlloc(GetProcessHeap(), 0, l1_bytes); if (!l1_table) { hr = E_OUTOFMEMORY; goto cleanup; } if (!pread_full(hSrc, l1_table_offset, l1_table, l1_bytes)) { log_err(L"qcow2_to_raw: read L1 table failed (%lu)", GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } } hDst = CreateFileW(dst_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (hDst == INVALID_HANDLE_VALUE) { log_err(L"qcow2_to_raw: create dst failed (%lu)", GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } { DWORD bytes; DeviceIoControl(hDst, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &bytes, NULL); } { LARGE_INTEGER li; li.QuadPart = (LONGLONG)virtual_size; if (!SetFilePointerEx(hDst, li, NULL, FILE_BEGIN) || !SetEndOfFile(hDst)) { hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } } l2_table_bytes = l2_entries * 8; l2_table = (BYTE *)HeapAlloc(GetProcessHeap(), 0, l2_table_bytes); cluster_buf = (BYTE *)HeapAlloc(GetProcessHeap(), 0, cluster_size); if (!l2_table || !cluster_buf) { hr = E_OUTOFMEMORY; goto cleanup; } { ULONGLONG cached_l2_index = (ULONGLONG)-1; ULONG last_pct = 0; for (cluster_idx = 0; cluster_idx < total_clusters; cluster_idx++) { ULONGLONG l1_idx = cluster_idx / l2_entries; ULONGLONG l2_idx = cluster_idx % l2_entries; ULONGLONG l1_entry, l2_table_off, l2_entry, cluster_off; if (l1_idx >= l1_size) break; l1_entry = read_be64(l1_table + l1_idx * 8); l2_table_off = l1_entry & QCOW2_L1E_OFFSET_MASK; if (l2_table_off == 0) { cluster_idx = (l1_idx + 1) * l2_entries - 1; continue; } if (l1_idx != cached_l2_index) { if (!pread_full(hSrc, l2_table_off, l2_table, l2_table_bytes)) { log_err(L"qcow2_to_raw: read L2 table failed (%lu)", GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } cached_l2_index = l1_idx; } l2_entry = read_be64(l2_table + l2_idx * 8); if (l2_entry == 0) continue; if (l2_entry & QCOW2_L2E_COMPRESSED_BIT) { DWORD x = 62 - (cluster_bits - 8); ULONGLONG comp_off = l2_entry & (((ULONGLONG)1 << x) - 1); DWORD comp_secs_m1= (DWORD)((l2_entry >> x) & (((ULONGLONG)1 << (62 - x)) - 1)); DWORD comp_bytes = (comp_secs_m1 + 1) * 512u - (DWORD)(comp_off & 511); BYTE *comp_buf; size_t out_actual = 0; int verbose = (compressed_seen < 3); /* log first few in detail */ if (verbose) { log_debug(L"compressed cluster %llu: L2=0x%llx comp_off=0x%llx secs_m1=%lu bytes=%lu", cluster_idx, l2_entry, comp_off, comp_secs_m1, comp_bytes); } compressed_seen++; comp_buf = (BYTE *)HeapAlloc(GetProcessHeap(), 0, comp_bytes); if (!comp_buf) { hr = E_OUTOFMEMORY; goto cleanup; } if (!pread_full(hSrc, comp_off, comp_buf, comp_bytes)) { DWORD err = GetLastError(); HeapFree(GetProcessHeap(), 0, comp_buf); log_err(L"qcow2_to_raw: read compressed cluster %llu failed (%lu)", cluster_idx, err); hr = HRESULT_FROM_WIN32(err); goto cleanup; } if (verbose) { log_debug(L"compressed cluster %llu first 16 bytes: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x", cluster_idx, comp_buf[0], comp_buf[1], comp_buf[2], comp_buf[3], comp_buf[4], comp_buf[5], comp_buf[6], comp_buf[7], comp_buf[8], comp_buf[9], comp_buf[10], comp_buf[11], comp_buf[12], comp_buf[13], comp_buf[14], comp_buf[15]); } /* qcow2 calls its compression type "zlib" but QEMU actually writes RAW DEFLATE streams (deflateInit2 with -MAX_WBITS, no 2-byte zlib header, no Adler-32). So feed comp_buf directly to inflate_deflate, NOT inflate_zlib. */ if (inflate_deflate(cluster_buf, cluster_size, comp_buf, comp_bytes, &out_actual, verbose) != 0) { HeapFree(GetProcessHeap(), 0, comp_buf); log_err(L"qcow2_to_raw: inflate failed on cluster %llu", cluster_idx); goto cleanup; } HeapFree(GetProcessHeap(), 0, comp_buf); if (verbose) { log_debug(L"compressed cluster %llu inflated to %zu bytes (target %lu)", cluster_idx, out_actual, cluster_size); } if (out_actual < cluster_size) memset(cluster_buf + out_actual, 0, cluster_size - out_actual); if (!pwrite_full(hDst, cluster_idx * cluster_size, cluster_buf, cluster_size)) { hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } continue; } cluster_off = l2_entry & QCOW2_L2E_OFFSET_MASK; if (cluster_off == 0) continue; if (!pread_full(hSrc, cluster_off, cluster_buf, cluster_size)) { log_err(L"qcow2_to_raw: read cluster %llu failed (%lu)", cluster_idx, GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } if (!pwrite_full(hDst, cluster_idx * cluster_size, cluster_buf, cluster_size)) { hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } { ULONG pct = (ULONG)((cluster_idx + 1) * 100 / total_clusters); if (pct >= last_pct + 5) { log_progress(pct, L"Converting qcow2 to raw..."); last_pct = pct; } } } } hr = S_OK; log_msg(L"qcow2_to_raw: done"); cleanup: if (l1_table) HeapFree(GetProcessHeap(), 0, l1_table); if (l2_table) HeapFree(GetProcessHeap(), 0, l2_table); if (cluster_buf) HeapFree(GetProcessHeap(), 0, cluster_buf); if (hSrc != INVALID_HANDLE_VALUE) CloseHandle(hSrc); if (hDst != INVALID_HANDLE_VALUE) CloseHandle(hDst); return hr; } /* ---- VHD fixed footer + VHD -> VHDX conversion ---- */ #define VHDX_VENDOR_MS_LOCAL { 0xec984aec, 0xa0f9, 0x47e9, \ { 0x90, 0x1f, 0x71, 0x41, 0x5a, 0x66, 0x34, 0x5b } } static const GUID VHDX_VENDOR_MS_GUID = VHDX_VENDOR_MS_LOCAL; static void vhd_compute_chs(ULONGLONG total_sectors, WORD *out_cyls, BYTE *out_heads, BYTE *out_spt) { BYTE heads, spt; DWORD cth; WORD cyls; if (total_sectors > 65535ULL * 16 * 255) total_sectors = 65535ULL * 16 * 255; if (total_sectors >= 65535ULL * 16 * 63) { spt = 255; heads = 16; cth = (DWORD)(total_sectors / spt); } else { spt = 17; cth = (DWORD)(total_sectors / spt); heads = (BYTE)((cth + 1023) / 1024); if (heads < 4) heads = 4; if (cth >= ((DWORD)heads * 1024) || heads > 16) { spt = 31; heads = 16; cth = (DWORD)(total_sectors / spt); } if (cth >= ((DWORD)heads * 1024)) { spt = 63; heads = 16; cth = (DWORD)(total_sectors / spt); } } cyls = (WORD)(cth / heads); *out_cyls = cyls; *out_heads = heads; *out_spt = spt; } static HRESULT vhd_append_fixed_footer(const wchar_t *path, ULONGLONG size_bytes) { BYTE footer[512]; HANDLE h; DWORD bytes_written; SYSTEMTIME st; FILETIME ft; ULARGE_INTEGER vhd_epoch_ft; DWORD timestamp; ULONGLONG total_sectors; WORD cyls; BYTE heads, spt; GUID uid; DWORD sum; int i; ZeroMemory(footer, sizeof(footer)); memcpy(footer + 0, "conectix", 8); put_be32(footer + 8, 0x00000002u); put_be32(footer + 12, 0x00010000u); put_be64(footer + 16, 0xFFFFFFFFFFFFFFFFull); GetSystemTime(&st); SystemTimeToFileTime(&st, &ft); { ULARGE_INTEGER now_ft; now_ft.LowPart = ft.dwLowDateTime; now_ft.HighPart = ft.dwHighDateTime; vhd_epoch_ft.QuadPart = 125911584000000000ULL; timestamp = (now_ft.QuadPart < vhd_epoch_ft.QuadPart) ? 0 : (DWORD)((now_ft.QuadPart - vhd_epoch_ft.QuadPart) / 10000000ULL); } put_be32(footer + 24, timestamp); memcpy(footer + 28, "asb!", 4); put_be32(footer + 32, 0x00010000u); put_be32(footer + 36, 0x5769326Bu); /* "Wi2k" */ put_be64(footer + 40, size_bytes); put_be64(footer + 48, size_bytes); total_sectors = size_bytes / 512; vhd_compute_chs(total_sectors, &cyls, &heads, &spt); footer[56] = (BYTE)(cyls >> 8); footer[57] = (BYTE)cyls; footer[58] = heads; footer[59] = spt; put_be32(footer + 60, 2u); /* fixed */ if (CoCreateGuid(&uid) != S_OK) ZeroMemory(&uid, sizeof(uid)); memcpy(footer + 68, &uid, 16); sum = 0; for (i = 0; i < 512; i++) sum += footer[i]; sum = ~sum; put_be32(footer + 64, sum); h = CreateFileW(path, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (h == INVALID_HANDLE_VALUE) return HRESULT_FROM_WIN32(GetLastError()); { LARGE_INTEGER z; z.QuadPart = 0; SetFilePointerEx(h, z, NULL, FILE_END); } if (!WriteFile(h, footer, sizeof(footer), &bytes_written, NULL) || bytes_written != sizeof(footer)) { DWORD err = GetLastError(); CloseHandle(h); return HRESULT_FROM_WIN32(err); } CloseHandle(h); return S_OK; } /* Create an empty dynamic VHDX of `size_bytes`, attach it, then byte-copy the contents of `raw_src` onto the attached physical drive. Bypasses the VHD intermediate entirely — CreateVirtualDisk's SourcePath doesn't do cross-format VHD->VHDX conversion (returns ERROR_VHD_INVALID_TYPE). This is the same pattern do_to_vhdx() uses for Windows ISO installs: create + attach + treat-as-block-device. Skips sparse regions in the source (FSCTL_QUERY_ALLOCATED_RANGES) so we don't write 3+ GB of zeros through the VHDX layer for a mostly-empty cloud image. */ static HRESULT raw_to_vhdx(const wchar_t *raw_src, const wchar_t *vhdx_dst, ULONGLONG size_bytes) { VIRTUAL_STORAGE_TYPE vt; CREATE_VIRTUAL_DISK_PARAMETERS params; ATTACH_VIRTUAL_DISK_PARAMETERS aparams; HANDLE hVhdx = INVALID_HANDLE_VALUE; HANDLE hRaw = INVALID_HANDLE_VALUE; HANDLE hDrive = INVALID_HANDLE_VALUE; HRESULT hr = E_FAIL; DWORD result; wchar_t phys_path[MAX_PATH]; DWORD phys_len = sizeof(phys_path); vt.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; vt.VendorId = VHDX_VENDOR_MS_GUID; /* --- 1. Create empty dynamic VHDX at the raw image's natural size. Per-VM disk sizing is handled separately by the differencing child created in disk_util.c. */ ZeroMemory(¶ms, sizeof(params)); params.Version = CREATE_VIRTUAL_DISK_VERSION_2; params.Version2.MaximumSize = size_bytes; params.Version2.BlockSizeInBytes = 0; /* default 32 MiB */ params.Version2.SectorSizeInBytes = 512; params.Version2.PhysicalSectorSizeInBytes = 4096; log_debug(L"raw_to_vhdx: creating VHDX size=%llu at %s", size_bytes, vhdx_dst); result = CreateVirtualDisk(&vt, vhdx_dst, VIRTUAL_DISK_ACCESS_NONE, NULL, CREATE_VIRTUAL_DISK_FLAG_NONE, 0, ¶ms, NULL, &hVhdx); if (result != ERROR_SUCCESS) { log_err(L"raw_to_vhdx: CreateVirtualDisk failed (0x%08X)", result); return HRESULT_FROM_WIN32(result); } /* --- 2. Attach so the host kernel surfaces it as a physical disk --- */ ZeroMemory(&aparams, sizeof(aparams)); aparams.Version = ATTACH_VIRTUAL_DISK_VERSION_1; result = AttachVirtualDisk(hVhdx, NULL, ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER, 0, &aparams, NULL); if (result != ERROR_SUCCESS) { log_err(L"raw_to_vhdx: AttachVirtualDisk failed (0x%08X)", result); hr = HRESULT_FROM_WIN32(result); goto cleanup; } /* --- 3. Resolve to \\.\PhysicalDriveN --- */ result = GetVirtualDiskPhysicalPath(hVhdx, &phys_len, phys_path); if (result != ERROR_SUCCESS) { log_err(L"raw_to_vhdx: GetVirtualDiskPhysicalPath failed (0x%08X)", result); hr = HRESULT_FROM_WIN32(result); goto cleanup; } log_debug(L"raw_to_vhdx: attached at %s", phys_path); /* --- 4. Open source raw + target physical drive --- */ hRaw = CreateFileW(raw_src, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (hRaw == INVALID_HANDLE_VALUE) { log_err(L"raw_to_vhdx: open raw source failed (%lu)", GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } hDrive = CreateFileW(phys_path, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH, NULL); if (hDrive == INVALID_HANDLE_VALUE) { log_err(L"raw_to_vhdx: open physical drive failed (%lu)", GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } /* --- 5. Query allocated ranges in the source raw image (sparse-aware copy: skip ranges that are pure holes in NTFS). Falls back to a full linear copy if the query fails. --- */ log_msg(L"Copying raw image to VHDX (sparse-aware)..."); { FILE_ALLOCATED_RANGE_BUFFER query_range; FILE_ALLOCATED_RANGE_BUFFER ranges[256]; DWORD bytes_returned; BOOL more; DWORD copy_buf_size = 1024 * 1024; /* 1 MiB */ BYTE *copy_buf; ULONGLONG total_copied = 0; ULONG last_pct = 0; copy_buf = (BYTE *)HeapAlloc(GetProcessHeap(), 0, copy_buf_size); if (!copy_buf) { hr = E_OUTOFMEMORY; goto cleanup; } query_range.FileOffset.QuadPart = 0; query_range.Length.QuadPart = (LONGLONG)size_bytes; do { more = !DeviceIoControl(hRaw, FSCTL_QUERY_ALLOCATED_RANGES, &query_range, sizeof(query_range), ranges, sizeof(ranges), &bytes_returned, NULL); DWORD nr = bytes_returned / sizeof(FILE_ALLOCATED_RANGE_BUFFER); DWORD i; if (more && GetLastError() != ERROR_MORE_DATA) { log_debug(L"raw_to_vhdx: FSCTL_QUERY_ALLOCATED_RANGES failed (%lu) — falling back to linear copy", GetLastError()); HeapFree(GetProcessHeap(), 0, copy_buf); copy_buf = NULL; break; } for (i = 0; i < nr; i++) { ULONGLONG off = (ULONGLONG)ranges[i].FileOffset.QuadPart; ULONGLONG len = (ULONGLONG)ranges[i].Length.QuadPart; LARGE_INTEGER li; /* Position both source and target at the same offset. */ li.QuadPart = (LONGLONG)off; if (!SetFilePointerEx(hRaw, li, NULL, FILE_BEGIN) || !SetFilePointerEx(hDrive, li, NULL, FILE_BEGIN)) { hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } while (len > 0) { DWORD chunk = (DWORD)(len > copy_buf_size ? copy_buf_size : len); DWORD got; if (!ReadFile(hRaw, copy_buf, chunk, &got, NULL) || got != chunk) { log_err(L"raw_to_vhdx: read at %llu len %lu failed (%lu)", off + (ranges[i].Length.QuadPart - len), chunk, GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } /* FILE_FLAG_NO_BUFFERING demands sector-aligned writes; 1 MiB chunks are sector-aligned, the last short chunk might not be — pad to 512 in that case. */ if (chunk & 511) { DWORD padded = (chunk + 511) & ~511u; memset(copy_buf + chunk, 0, padded - chunk); chunk = padded; } { DWORD wrote; if (!WriteFile(hDrive, copy_buf, chunk, &wrote, NULL) || wrote != chunk) { log_err(L"raw_to_vhdx: write at %llu len %lu failed (%lu)", off + (ranges[i].Length.QuadPart - len), chunk, GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } } len -= (chunk > len) ? len : chunk; total_copied += chunk; { ULONG pct = (ULONG)(total_copied * 100 / size_bytes); if (pct >= last_pct + 5) { log_progress(pct, L"Copying to VHDX..."); last_pct = pct; } } } } /* Advance query window past the last reported range so the next call gets the next batch. */ if (more && nr > 0) { ULONGLONG last_end = (ULONGLONG)ranges[nr - 1].FileOffset.QuadPart + (ULONGLONG)ranges[nr - 1].Length.QuadPart; if (last_end >= size_bytes) break; query_range.FileOffset.QuadPart = (LONGLONG)last_end; query_range.Length.QuadPart = (LONGLONG)(size_bytes - last_end); } else if (!more) { /* Final batch (DeviceIoControl returned TRUE — no more data). */ break; } else if (nr == 0) { /* ERROR_MORE_DATA but zero ranges — shouldn't happen, bail. */ break; } } while (1); if (copy_buf) HeapFree(GetProcessHeap(), 0, copy_buf); log_debug(L"raw_to_vhdx: copied %llu bytes (sparse holes left as zero in VHDX)", total_copied); } hr = S_OK; cleanup: if (hDrive != INVALID_HANDLE_VALUE) CloseHandle(hDrive); if (hRaw != INVALID_HANDLE_VALUE) CloseHandle(hRaw); if (hVhdx != INVALID_HANDLE_VALUE) { DetachVirtualDisk(hVhdx, DETACH_VIRTUAL_DISK_FLAG_NONE, 0); CloseHandle(hVhdx); } if (FAILED(hr)) DeleteFileW(vhdx_dst); return hr; } /* Walk every volume on `disk_no` and return the first one whose mounted filesystem reports as FAT32. That's how we locate the Ubuntu cloud-image ESP regardless of which partition slot it lives in (Canonical uses 15, but the layout has shifted across releases). Also logs what we see, so a failure tells us exactly which volumes Windows did detect. */ static BOOL find_fat32_volume_on_disk(DWORD disk_no, wchar_t *vol_path, size_t vol_path_len) { HANDLE hFind; wchar_t vol_name[MAX_PATH]; BOOL found = FALSE; int seen = 0; hFind = FindFirstVolumeW(vol_name, MAX_PATH); if (hFind == INVALID_HANDLE_VALUE) return FALSE; do { wchar_t vol_dev[MAX_PATH]; HANDLE hVol; BYTE buf[256]; DWORD bytes; VOLUME_DISK_EXTENTS *ext; wchar_t fs_name[32] = {0}; wchar_t label[64] = {0}; STORAGE_DEVICE_NUMBER sdn = {0}; wcscpy_s(vol_dev, MAX_PATH, vol_name); { size_t vlen = wcslen(vol_dev); if (vlen > 0 && vol_dev[vlen - 1] == L'\\') vol_dev[vlen - 1] = L'\0'; } hVol = CreateFileW(vol_dev, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hVol == INVALID_HANDLE_VALUE) continue; if (DeviceIoControl(hVol, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, NULL, 0, buf, sizeof(buf), &bytes, NULL)) { ext = (VOLUME_DISK_EXTENTS *)buf; if (ext->NumberOfDiskExtents >= 1 && ext->Extents[0].DiskNumber == disk_no) { DeviceIoControl(hVol, IOCTL_STORAGE_GET_DEVICE_NUMBER, NULL, 0, &sdn, sizeof(sdn), &bytes, NULL); /* GetVolumeInformationW wants the trailing-slash form. */ if (GetVolumeInformationW(vol_name, label, (DWORD)(sizeof(label)/sizeof(label[0])), NULL, NULL, NULL, fs_name, (DWORD)(sizeof(fs_name)/sizeof(fs_name[0])))) { log_debug(L"patch_esp: vol part=%u fs=%s label=%s (%s)", sdn.PartitionNumber, fs_name, label, vol_name); seen++; if (!found && _wcsicmp(fs_name, L"FAT32") == 0) { wcscpy_s(vol_path, vol_path_len, vol_name); found = TRUE; } } else { log_debug(L"patch_esp: vol part=%u (no fs info, err %lu) %s", sdn.PartitionNumber, GetLastError(), vol_name); seen++; } } } CloseHandle(hVol); } while (FindNextVolumeW(hFind, vol_name, MAX_PATH)); FindVolumeClose(hFind); if (!found) log_debug(L"patch_esp: enumerated %d volume(s) on disk %u, none were FAT32", seen, disk_no); return found; } /* Re-attach the just-built Ubuntu cloud-image VHDX, mount its FAT32 ESP, and replace /EFI/ubuntu/grub.cfg with a stanza that forwards the kernel ring buffer and systemd journal to the serial console. Without this the only kernel messages on Hyper-V COM1 are the pre-pivot ones; once dracut hands off to systemd, printk goes to /dev/kmsg only and we go blind on serial. We boot the same kernel/initrd via the symlinks /boot/vmlinuz and /boot/initrd.img — grub on ext4 follows symlinks, so this stays valid across kernel-package upgrades. */ static HRESULT patch_ubuntu_esp_grub_for_debug(const wchar_t *vhdx_path) { VIRTUAL_STORAGE_TYPE vt; OPEN_VIRTUAL_DISK_PARAMETERS op; ATTACH_VIRTUAL_DISK_PARAMETERS ap; HANDLE hVhdx = INVALID_HANDLE_VALUE; DWORD result; HRESULT hr = E_FAIL; wchar_t phys_path[MAX_PATH]; DWORD phys_len = sizeof(phys_path); DWORD disk_no = 0; wchar_t esp_vol[MAX_PATH] = {0}; wchar_t esp_mount[MAX_PATH] = {0}; wchar_t grub_cfg_path[MAX_PATH]; BOOL attached = FALSE; BOOL mounted = FALSE; HANDLE hCfg = INVALID_HANDLE_VALUE; DWORD written = 0; /* GRUB script. Notes (verified in grub-2.12 source, not assumed): - search.fs_label LABEL VAR sets VAR from the partition with label LABEL (search_label.c). - Ubuntu cloud images put the kernel/initrd at the *root* of a separate FS labeled "BOOT" (per prior serial-log evidence "Mounting boot.mount" + "BOOT_IMAGE=/vmlinuz-..."). So we search by label rather than guess partition numbers. - GRUB's wildcard translator (wildcard.c) glob-expands every script argument: '*' → regex '.*', anchored '^...$'. We use that to find the kernel/initrd files without baking in a version, which would break on kernel updates. The `for` loop just picks whichever filename the glob resolves to (in practice exactly one). */ static const char cfg[] = "# Generated by iso-patch.exe (debug-serial)\n" "set timeout=0\n" "search.fs_label BOOT root\n" "for f in ($root)/vmlinuz-*-generic; do set vmlinuz=$f; done\n" "for f in ($root)/initrd.img-*-generic; do set initrdimg=$f; done\n" "linux $vmlinuz root=LABEL=cloudimg-rootfs ro " IP_EARLYCON_A "console=tty1 console=" IP_SERIAL_A " " "systemd.journald.forward_to_console=1 " "systemd.log_target=console " "systemd.log_level=info " "loglevel=7 " IP_EARLYPRINTK_A "\n" "initrd $initrdimg\n" "boot\n"; log_msg(L"Patching ESP /EFI/ubuntu/grub.cfg for serial debug"); vt.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX; vt.VendorId = VHDX_VENDOR_MS_GUID; ZeroMemory(&op, sizeof(op)); op.Version = OPEN_VIRTUAL_DISK_VERSION_2; op.Version2.GetInfoOnly = FALSE; result = OpenVirtualDisk(&vt, vhdx_path, VIRTUAL_DISK_ACCESS_NONE, OPEN_VIRTUAL_DISK_FLAG_NONE, &op, &hVhdx); if (result != ERROR_SUCCESS) { log_err(L"patch_esp: OpenVirtualDisk failed (%lu)", result); return HRESULT_FROM_WIN32(result); } ZeroMemory(&ap, sizeof(ap)); ap.Version = ATTACH_VIRTUAL_DISK_VERSION_1; result = AttachVirtualDisk(hVhdx, NULL, ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER, 0, &ap, NULL); if (result != ERROR_SUCCESS) { log_err(L"patch_esp: AttachVirtualDisk failed (%lu)", result); hr = HRESULT_FROM_WIN32(result); goto cleanup; } attached = TRUE; result = GetVirtualDiskPhysicalPath(hVhdx, &phys_len, phys_path); if (result != ERROR_SUCCESS) { log_err(L"patch_esp: GetVirtualDiskPhysicalPath failed (%lu)", result); hr = HRESULT_FROM_WIN32(result); goto cleanup; } disk_no = get_disk_number_from_path(phys_path); log_debug(L"patch_esp: disk_no=%u (%s)", disk_no, phys_path); /* The raw_to_vhdx pass wrote a fresh GPT into PhysicalDriveN, but partmgr cached the (empty) layout from when we first attached. Force a partition-table re-read so Windows surfaces the new partitions as volumes. Without this, FindFirstVolume never sees the ESP. */ { HANDLE hDisk = CreateFileW(phys_path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hDisk == INVALID_HANDLE_VALUE) { log_err(L"patch_esp: open %s for rescan failed (%lu)", phys_path, GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } else { DWORD ret_bytes = 0; if (!DeviceIoControl(hDisk, IOCTL_DISK_UPDATE_PROPERTIES, NULL, 0, NULL, 0, &ret_bytes, NULL)) { log_debug(L"patch_esp: IOCTL_DISK_UPDATE_PROPERTIES failed (%lu) " L"— proceeding anyway", GetLastError()); } else { log_debug(L"patch_esp: partition-table rescan ok"); } CloseHandle(hDisk); } } /* Let the volume manager finish enumerating partitions after the rescan. Auto-mount of FAT32 partitions happens asynchronously. */ Sleep(3000); if (!find_fat32_volume_on_disk(disk_no, esp_vol, MAX_PATH)) { log_err(L"patch_esp: no FAT32 volume found on disk %u", disk_no); hr = E_FAIL; goto cleanup; } log_debug(L"patch_esp: ESP volume %s", esp_vol); if (!create_temp_mount_dir(L"asb_esp", esp_mount, MAX_PATH)) { log_err(L"patch_esp: create_temp_mount_dir failed (%lu)", GetLastError()); hr = E_FAIL; goto cleanup; } if (!mount_volume_to_dir(esp_vol, esp_mount)) { log_err(L"patch_esp: SetVolumeMountPointW(%s -> %s) failed (%lu)", esp_mount, esp_vol, GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } mounted = TRUE; log_debug(L"patch_esp: mounted at %s", esp_mount); swprintf_s(grub_cfg_path, MAX_PATH, L"%sEFI\\ubuntu\\grub.cfg", esp_mount); hCfg = CreateFileW(grub_cfg_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hCfg == INVALID_HANDLE_VALUE) { log_err(L"patch_esp: open %s for write failed (%lu)", grub_cfg_path, GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } if (!WriteFile(hCfg, cfg, (DWORD)(sizeof(cfg) - 1), &written, NULL) || written != sizeof(cfg) - 1) { log_err(L"patch_esp: write grub.cfg failed (%lu)", GetLastError()); hr = HRESULT_FROM_WIN32(GetLastError()); goto cleanup; } log_msg(L"patch_esp: wrote %lu bytes to %s", written, grub_cfg_path); hr = S_OK; cleanup: if (hCfg != INVALID_HANDLE_VALUE) CloseHandle(hCfg); if (mounted) unmount_volume_from_dir(esp_mount); if (esp_mount[0]) RemoveDirectoryW(esp_mount); if (attached) DetachVirtualDisk(hVhdx, DETACH_VIRTUAL_DISK_FLAG_NONE, 0); if (hVhdx != INVALID_HANDLE_VALUE) CloseHandle(hVhdx); return hr; } /* ---- Top-level: do_qcow2_to_vhdx ---- */ static int do_qcow2_to_vhdx(const wchar_t *qcow2_in, const wchar_t *vhdx_out) { wchar_t raw_path[MAX_PATH]; wchar_t cache_dir[MAX_PATH]; HRESULT hr; HANDLE h; LARGE_INTEGER sz; /* AttachVirtualDisk on a freshly-created VHDX requires SE_MANAGE_VOLUME. Admin tokens have it present-but-disabled; enable it now. */ if (!enable_privilege(SE_MANAGE_VOLUME_NAME)) { log_debug(L"SE_MANAGE_VOLUME_NAME not enabled (token lacks it, or AdjustTokenPrivileges failed). " L"AttachVirtualDisk will probably fail with 0x522 — run as Administrator."); } else { log_debug(L"SE_MANAGE_VOLUME_NAME enabled."); } /* Derive intermediate path in the same directory as the output. */ wcscpy_s(cache_dir, MAX_PATH, vhdx_out); { wchar_t *s = wcsrchr(cache_dir, L'\\'); if (s) *s = L'\0'; } swprintf_s(raw_path, MAX_PATH, L"%s\\.iso-patch-raw.img", cache_dir); /* Delete any stale VHDX so CreateVirtualDisk doesn't trip on FILE_EXISTS from a previous failed run. */ DeleteFileW(vhdx_out); log_msg(L"qcow2 -> raw..."); hr = qcow2_to_raw(qcow2_in, raw_path); if (FAILED(hr)) { log_err(L"qcow2_to_raw failed (0x%08X)", hr); goto fail; } h = CreateFileW(raw_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (h == INVALID_HANDLE_VALUE || !GetFileSizeEx(h, &sz)) { if (h != INVALID_HANDLE_VALUE) CloseHandle(h); log_err(L"can't stat raw image"); goto fail; } CloseHandle(h); if ((sz.QuadPart & 511) != 0) { log_err(L"raw size not 512-aligned"); goto fail; } log_msg(L"Creating VHDX and copying raw image (%lld bytes)...", sz.QuadPart); hr = raw_to_vhdx(raw_path, vhdx_out, (ULONGLONG)sz.QuadPart); if (FAILED(hr)) { log_err(L"raw_to_vhdx failed (0x%08X)", hr); goto fail; } /* Inject debug-serial kernel cmdline so the COM1 pipe shows kernel + systemd messages past dracut pivot. Non-fatal if it fails — VM will still boot, just without the visibility. */ { HRESULT pr = patch_ubuntu_esp_grub_for_debug(vhdx_out); if (FAILED(pr)) log_err(L"patch_ubuntu_esp_grub_for_debug failed (0x%08X) — " L"VM will boot without serial debug visibility", pr); } DeleteFileW(raw_path); log_done(vhdx_out); return 0; fail: DeleteFileW(raw_path); return 1; } /* ---- Main ---- */ int wmain(int argc, wchar_t *argv[]) { if (argc < 2) { wprintf(L"Usage:\n"); wprintf(L" iso-patch.exe \n"); wprintf(L" Rebuild ISO with noprompt UEFI boot (no \"Press Any Key\").\n\n"); wprintf(L" iso-patch.exe --to-vhdx [image-index] [size-gb] [options]\n"); wprintf(L" Convert Windows installer ISO to a bootable UEFI VHDX.\n"); wprintf(L" image-index defaults to 1 (first edition in install.wim/esd).\n"); wprintf(L" size-gb defaults to %d.\n", DEFAULT_VHDX_SIZE_GB); wprintf(L" --output Output VHDX path (default: next to iso-patch.exe).\n"); wprintf(L" --stage Tab-separated manifest of files to copy onto the VHDX.\n"); wprintf(L" Each line: \\t\n\n"); wprintf(L" iso-patch.exe --qcow2-to-vhdx --output \n"); wprintf(L" Convert an Ubuntu cloud-image qcow2 to a Hyper-V Gen2 VHDX.\n"); wprintf(L" Builds a sparse raw image, wraps with VHD-fixed footer, then\n"); wprintf(L" CreateVirtualDisk converts to VHDX. Emits STATUS / PROGRESS /\n"); wprintf(L" DEBUG / ERROR / DONE lines like the other modes.\n\n"); wprintf(L" iso-patch.exe --ubuntu-to-vhdx --output [options]\n"); wprintf(L" Convert an Ubuntu desktop ISO directly to a fully-installed\n"); wprintf(L" bootable VHDX (no subiquity, no user interaction). Streams\n"); wprintf(L" casper/minimal.squashfs into a freshly-built ext4 partition.\n"); wprintf(L" --size-gb N virtual size in GiB (default 16, min 16)\n"); wprintf(L" --stage tab-separated manifest: \\t\n\n"); wprintf(L"Output defaults to next to iso-patch.exe.\n"); return 1; } if (_wcsicmp(argv[1], L"--patch-grub-debug") == 0) { const wchar_t *vhdx; DWORD attrs; HRESULT pr; if (argc < 3) { log_err(L"--patch-grub-debug requires "); return 1; } vhdx = argv[2]; if (!enable_privilege(SE_MANAGE_VOLUME_NAME)) log_debug(L"SE_MANAGE_VOLUME_NAME not enabled (run as Administrator)"); /* AppSandbox marks cached base.vhdx read-only after creation — clear that for the duration of this call. The patcher writes into the disk via AttachVirtualDisk, not via the file handle, but the OPEN_VIRTUAL_DISK call will still be rejected on a read-only file. */ attrs = GetFileAttributesW(vhdx); if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_READONLY)) SetFileAttributesW(vhdx, attrs & ~FILE_ATTRIBUTE_READONLY); pr = patch_ubuntu_esp_grub_for_debug(vhdx); if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_READONLY)) SetFileAttributesW(vhdx, attrs); return FAILED(pr) ? 1 : 0; } if (_wcsicmp(argv[1], L"--qcow2-to-vhdx") == 0) { const wchar_t *qcow2 = NULL; const wchar_t *output = NULL; int i; for (i = 2; i < argc; i++) { if (_wcsicmp(argv[i], L"--output") == 0) { if (i + 1 < argc) { output = argv[++i]; } else { log_err(L"--output requires a path"); return 1; } } else if (!qcow2) { qcow2 = argv[i]; } } if (!qcow2 || !output) { log_err(L"--qcow2-to-vhdx requires and --output "); return 1; } return do_qcow2_to_vhdx(qcow2, output); } if (_wcsicmp(argv[1], L"--prefetch-repo") == 0) { const wchar_t *branch = NULL, *out_dir = NULL; for (int i = 2; i < argc; i++) { if (_wcsicmp(argv[i], L"--branch") == 0 && i + 1 < argc) branch = argv[++i]; else if (_wcsicmp(argv[i], L"--out-dir") == 0 && i + 1 < argc) out_dir = argv[++i]; } if (!branch || !out_dir) { log_err(L"--prefetch-repo requires --branch and --out-dir"); return 1; } return do_prefetch_repo(branch, out_dir); } if (_wcsicmp(argv[1], L"--prefetch-wsl-deps") == 0) { const wchar_t *out_dir = NULL; for (int i = 2; i < argc; i++) { if (_wcsicmp(argv[i], L"--out-dir") == 0 && i + 1 < argc) out_dir = argv[++i]; } if (!out_dir) { log_err(L"--prefetch-wsl-deps requires --out-dir"); return 1; } return do_prefetch_wsl_deps(out_dir); } if (_wcsicmp(argv[1], L"--prefetch-build-deps") == 0) { const wchar_t *codename = NULL, *kver = NULL; const wchar_t *out_dir = NULL, *mirror = NULL, *iso_root = NULL; for (int i = 2; i < argc; i++) { if (_wcsicmp(argv[i], L"--codename") == 0 && i + 1 < argc) { codename = argv[++i]; } else if (_wcsicmp(argv[i], L"--kernel") == 0 && i + 1 < argc) { kver = argv[++i]; } else if (_wcsicmp(argv[i], L"--out-dir") == 0 && i + 1 < argc) { out_dir = argv[++i]; } else if (_wcsicmp(argv[i], L"--mirror") == 0 && i + 1 < argc) { mirror = argv[++i]; } else if (_wcsicmp(argv[i], L"--iso-root") == 0 && i + 1 < argc) { iso_root = argv[++i]; } } if (!codename || !kver || !out_dir || !iso_root) { log_err(L"--prefetch-build-deps requires --codename, --kernel, --out-dir, --iso-root"); return 1; } return do_prefetch_build_deps(codename, kver, out_dir, mirror, iso_root); } if (_wcsicmp(argv[1], L"--ubuntu-to-vhdx") == 0) { const wchar_t *iso = NULL; const wchar_t *output = NULL; const wchar_t *manifest = NULL; int size_gb = 16; for (int i = 2; i < argc; i++) { if (_wcsicmp(argv[i], L"--output") == 0) { if (i + 1 < argc) { output = argv[++i]; } else { log_err(L"--output requires a path"); return 1; } } else if (_wcsicmp(argv[i], L"--stage") == 0) { if (i + 1 < argc) { manifest = argv[++i]; } else { log_err(L"--stage requires a manifest file"); return 1; } } else if (_wcsicmp(argv[i], L"--size-gb") == 0) { if (i + 1 < argc) { size_gb = _wtoi(argv[++i]); } else { log_err(L"--size-gb requires a number"); return 1; } } else if (!iso) { iso = argv[i]; } } if (!iso || !output) { log_err(L"--ubuntu-to-vhdx requires and --output "); return 1; } return do_ubuntu_to_vhdx(iso, output, size_gb, manifest); } if (_wcsicmp(argv[1], L"--to-vhdx") == 0) { int image_index = 1; int size_gb = DEFAULT_VHDX_SIZE_GB; const wchar_t *manifest = NULL; const wchar_t *output = NULL; int positional = 0; if (argc < 3) { log_err(L"--to-vhdx requires an ISO path"); return 1; } /* Parse optional positional args and flags */ for (int i = 3; i < argc; i++) { if (_wcsicmp(argv[i], L"--stage") == 0) { if (i + 1 < argc) { manifest = argv[++i]; } else { log_err(L"--stage requires a manifest file path"); return 1; } } else if (_wcsicmp(argv[i], L"--output") == 0) { if (i + 1 < argc) { output = argv[++i]; } else { log_err(L"--output requires a file path"); return 1; } } else if (positional == 0) { image_index = _wtoi(argv[i]); if (image_index < 1) image_index = 1; positional++; } else if (positional == 1) { size_gb = _wtoi(argv[i]); if (size_gb < 16) size_gb = 16; positional++; } } return do_to_vhdx(argv[2], image_index, size_gb, manifest, output); } return do_noprompt(argv[1]); }