From c2833e6ecb8be3675cc22f691686847f14500a9c Mon Sep 17 00:00:00 2001 From: EzYDark Date: Mon, 31 Aug 2026 01:12:18 +0200 Subject: [PATCH] Fix Ubuntu VM provisioning, desktop apps, and display refresh Resolve offline build dependencies against the Ubuntu release, updates, and security repositories. Include the required build tools and matching kernel headers so the guest agent can build and complete installation instead of leaving AppSandbox stuck on Installing Linux. Restore snap-confine capabilities, disable GTK Vulkan rendering while retaining OpenGL, and preserve the DRM framebuffer ID before freeing the plane data. --- tools/iso-patch/prefetch_build_deps.c | 140 ++++++++++++++++++++----- tools/iso-patch/prefetch_build_deps.h | 4 +- tools/iso-patch/ubuntu_vhdx.c | 42 ++++++++ tools/linux/agent/appsandbox-display.c | 10 +- 4 files changed, 168 insertions(+), 28 deletions(-) diff --git a/tools/iso-patch/prefetch_build_deps.c b/tools/iso-patch/prefetch_build_deps.c index a5dd66e..0394c18 100644 --- a/tools/iso-patch/prefetch_build_deps.c +++ b/tools/iso-patch/prefetch_build_deps.c @@ -1,7 +1,9 @@ /* prefetch_build_deps.c - see header for design. * * Pipeline: - * 1. WinHTTP-download Packages.xz for /main/binary- + * 1. WinHTTP-download Packages.xz for the release, updates, and + * security pockets under main/binary-. Append them from + * lowest to highest priority so the latest parsed record wins. * 2. Decompress it in-process with the vendored xz-embedded decoder * (xz_decompress_file_to_file below; Packages.gz is raw-gzipped * text that tar.exe rejects, so we target the .xz instead). @@ -94,6 +96,58 @@ static BOOL u_rmdir_recursive(const wchar_t *path) return TRUE; } +/* Append src to dst, with a blank-line stanza separator. Packages + * indexes are concatenated from lowest to highest pocket priority; + * parse_packages() prepends records to each hash bucket, so lookups + * then select the record from the newest available pocket. */ +static int append_file(const wchar_t *src, const wchar_t *dst, BOOL truncate) +{ + HANDLE hIn = CreateFileW(src, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hIn == INVALID_HANDLE_VALUE) { + log_err(L"prefetch: open %s failed: %lu", src, GetLastError()); + return -1; + } + + HANDLE hOut = CreateFileW(dst, GENERIC_WRITE, 0, NULL, + truncate ? CREATE_ALWAYS : OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + if (hOut == INVALID_HANDLE_VALUE) { + log_err(L"prefetch: open %s failed: %lu", dst, GetLastError()); + CloseHandle(hIn); + return -1; + } + if (!truncate) { + LARGE_INTEGER zero = { 0 }; + SetFilePointerEx(hOut, zero, NULL, FILE_END); + } + + BYTE buf[65536]; + DWORD br = 0; + int rc = 0; + for (;;) { + if (!ReadFile(hIn, buf, sizeof(buf), &br, NULL)) { + log_err(L"prefetch: append read failed: %lu", GetLastError()); + rc = -1; + break; + } + if (br == 0) break; + DWORD wr = 0; + if (!WriteFile(hOut, buf, br, &wr, NULL) || wr != br) { + log_err(L"prefetch: append write failed: %lu", GetLastError()); + rc = -1; + break; + } + } + if (rc == 0) { + DWORD wr = 0; + if (!WriteFile(hOut, "\n\n", 2, &wr, NULL) || wr != 2) rc = -1; + } + CloseHandle(hOut); + CloseHandle(hIn); + return rc; +} + /* ==================================================================== * In-process XZ decompression. Reads .xz file, writes uncompressed * bytes to dst. Uses the engine/xz/ vendored xz-embedded decoder @@ -372,7 +426,7 @@ static int sha256_file(const wchar_t *path, char *hex_out /* 65 bytes */) * Memory model: read the entire Packages file into one big malloc'd * buffer; each pkg_record_t holds pointers + lengths INTO that buffer. * The pkg_table_t hashtable maps Package name (uppercased+hashed) to - * the first record with that name. + * the latest parsed record with that name. * ==================================================================== */ typedef struct pkg_record { @@ -699,24 +753,49 @@ int do_prefetch_build_deps(const wchar_t *codename, u_mkdir_p(out_dir); const wchar_t *staging = out_dir; - /* ---- 1. Download Packages.xz ---- */ - wchar_t pkgs_xz[MAX_PATH], pkgs[MAX_PATH]; - swprintf_s(pkgs_xz, MAX_PATH, L"%s\\Packages.xz", staging); - swprintf_s(pkgs, MAX_PATH, L"%s\\Packages", staging); - - wchar_t url[1024]; - swprintf_s(url, 1024, L"%s/dists/%s/main/binary-" IP_DEB_ARCH L"/Packages.xz", - mirror, codename); - log_msg(L"prefetch: GET %s", url); - if (http_download(url, pkgs_xz) != 0) { - log_err(L"prefetch: download Packages.xz failed"); - return -1; - } + /* ---- 1-2. Download + merge release pocket indexes ---- + A point-release squashfs already contains packages from -updates + and -security. Resolving dependencies only against the base + pocket can therefore select older exact-version dependencies and + make apt reject the whole offline transaction. */ + wchar_t pkgs[MAX_PATH]; + swprintf_s(pkgs, MAX_PATH, L"%s\\Packages.indexes", staging); + const wchar_t *security_mirror = mirror_arg + ? mirror + : L"http://security.ubuntu.com/ubuntu"; + for (int pocket_i = 0; pocket_i < 3; pocket_i++) { + wchar_t pocket[128]; + const wchar_t *pocket_mirror = mirror; + if (pocket_i == 0) { + wcsncpy_s(pocket, ARRAYSIZE(pocket), codename, _TRUNCATE); + } else if (pocket_i == 1) { + swprintf_s(pocket, ARRAYSIZE(pocket), L"%s-updates", codename); + } else { + swprintf_s(pocket, ARRAYSIZE(pocket), L"%s-security", codename); + pocket_mirror = security_mirror; + } - /* ---- 2. In-process xz decompression via vendored xz-embedded ---- */ - if (xz_decompress_file_to_file(pkgs_xz, pkgs) != 0) { - log_err(L"prefetch: xz decompress failed"); - return -1; + wchar_t pkgs_xz[MAX_PATH], pkgs_part[MAX_PATH], url[1024]; + swprintf_s(pkgs_xz, MAX_PATH, L"%s\\Packages.%d.xz", staging, pocket_i); + swprintf_s(pkgs_part, MAX_PATH, L"%s\\Packages.%d", staging, pocket_i); + swprintf_s(url, ARRAYSIZE(url), + L"%s/dists/%s/main/binary-" IP_DEB_ARCH L"/Packages.xz", + pocket_mirror, pocket); + log_msg(L"prefetch: GET %s", url); + if (http_download(url, pkgs_xz) != 0) { + log_err(L"prefetch: download index for %s failed", pocket); + return -1; + } + if (xz_decompress_file_to_file(pkgs_xz, pkgs_part) != 0) { + log_err(L"prefetch: decompress index for %s failed", pocket); + return -1; + } + if (append_file(pkgs_part, pkgs, pocket_i == 0) != 0) { + log_err(L"prefetch: merge index for %s failed", pocket); + return -1; + } + DeleteFileW(pkgs_xz); + DeleteFileW(pkgs_part); } /* ---- 3. Slurp Packages into memory + parse ---- */ @@ -741,14 +820,20 @@ int do_prefetch_build_deps(const wchar_t *codename, /* ---- 4. Mark seeds in_closure ---- */ const char *seeds[] = { + "build-essential", "dkms", "libasound2-dev", "libxcb1-dev", "libxcb-xfixes0-dev", "libdrm-dev", "pkg-config", "openssh-server" /* for ssh_enabled VMs; firstboot installs conditionally */ }; int closure_count = 0; + int missing_required = 0; for (size_t i = 0; i < sizeof(seeds) / sizeof(seeds[0]); i++) { pkg_record_t *r = lookup_pkg(&T, seeds[i], strlen(seeds[i])); - if (!r) { log_msg(L"prefetch: WARN seed '%hs' not in archive", seeds[i]); continue; } + if (!r) { + log_err(L"prefetch: required seed '%hs' not in archive", seeds[i]); + missing_required = 1; + continue; + } if (!r->in_closure) { r->in_closure = 1; closure_count++; } } /* Also linux-headers-. */ @@ -758,8 +843,15 @@ int do_prefetch_build_deps(const wchar_t *codename, WideCharToMultiByte(CP_UTF8, 0, kernel_ver, -1, kver_utf8, sizeof(kver_utf8), NULL, NULL); snprintf(hdr, sizeof(hdr), "linux-headers-%s", kver_utf8); pkg_record_t *r = lookup_pkg(&T, hdr, strlen(hdr)); - if (r && !r->in_closure) { r->in_closure = 1; closure_count++; } + if (!r) { + log_err(L"prefetch: required kernel headers '%hs' not in archive", hdr); + missing_required = 1; + } else if (!r->in_closure) { + r->in_closure = 1; + closure_count++; + } } + if (missing_required) return -1; /* ---- 5. BFS until stable ---- */ int total_added = closure_count; @@ -841,9 +933,9 @@ int do_prefetch_build_deps(const wchar_t *codename, write_closure_json(&T, cj, codename, kernel_ver); } - /* Clean up Packages.xz — we don't ship it (we wrote our own - synthetic Packages with closure entries only). */ - DeleteFileW(pkgs_xz); + /* Do not ship the merged upstream indexes; only the synthetic + closure index belongs in the guest staging manifest. */ + DeleteFileW(pkgs); free(T.buf); free(T.records); diff --git a/tools/iso-patch/prefetch_build_deps.h b/tools/iso-patch/prefetch_build_deps.h index aa6aa56..b87d663 100644 --- a/tools/iso-patch/prefetch_build_deps.h +++ b/tools/iso-patch/prefetch_build_deps.h @@ -4,8 +4,8 @@ * Runs as a new iso-patch.exe mode (--prefetch-build-deps) so we * don't depend on PowerShell being available on the host (corporate * AppLocker / ExecutionPolicy / etc. routinely block it). Pure - * Win32 C: WinHTTP for downloads, tar.exe shell-out for gunzip, - * BCrypt for SHA256. + * Win32 C: WinHTTP for downloads, vendored xz-embedded for index + * decompression, BCrypt for SHA256. */ #ifndef PREFETCH_BUILD_DEPS_H #define PREFETCH_BUILD_DEPS_H diff --git a/tools/iso-patch/ubuntu_vhdx.c b/tools/iso-patch/ubuntu_vhdx.c index 696650f..292be1c 100644 --- a/tools/iso-patch/ubuntu_vhdx.c +++ b/tools/iso-patch/ubuntu_vhdx.c @@ -1246,6 +1246,37 @@ static void plant_firstboot_service(ext4_writer_t *ew) "# ============================================================\n" "EXTRAS=/opt/appsandbox\n" "\n" + "# --- STEP 7.2: restore extracted filesystem capabilities + safe GTK renderer ---\n" + "# The minimal ext4 writer intentionally has no xattr support, so security.capability\n" + "# metadata from the ISO's squashfs cannot survive extraction. Ubuntu 26.04's\n" + "# snap-confine is non-setuid and receives its required permitted capabilities from\n" + "# /usr/lib/snapd/snap-confine.caps during the snapd package's postinst. We bypass\n" + "# that installer when constructing the VHDX, so reproduce its exact setcap step.\n" + "echo \"==== STEP 7.2: snap capabilities + GTK renderer policy ====\"\n" + "SNAP_CONFINE=/usr/lib/snapd/snap-confine\n" + "SNAP_CAPS=/usr/lib/snapd/snap-confine.caps\n" + "if [ -x \"$SNAP_CONFINE\" ] && [ -s \"$SNAP_CAPS\" ] && command -v setcap >/dev/null 2>&1; then\n" + " if setcap -q - \"$SNAP_CONFINE\" < \"$SNAP_CAPS\"; then\n" + " echo \"OK: restored snap-confine file capabilities\"\n" + " getcap \"$SNAP_CONFINE\" 2>&1 || true\n" + " else\n" + " echo \"FAIL: could not restore snap-confine file capabilities\"\n" + " fi\n" + "else\n" + " echo \"WARN: snap-confine capability inputs unavailable\"\n" + "fi\n" + "# Mesa dzn is an experimental Vulkan-on-D3D12 implementation. GTK4 selects Vulkan\n" + "# by default and dzn can exhaust its descriptor pool, leaving libadwaita text blank.\n" + "# Disable only GTK's Vulkan backend; its OpenGL renderer remains accelerated.\n" + "install -d -m 0755 /etc/environment.d\n" + "printf 'GDK_DISABLE=vulkan\\n' > /etc/environment.d/90-appsandbox-gtk.conf\n" + "if grep -q '^GDK_DISABLE=' /etc/environment 2>/dev/null; then\n" + " sed -i 's/^GDK_DISABLE=.*/GDK_DISABLE=vulkan/' /etc/environment\n" + "else\n" + " printf '\\nGDK_DISABLE=vulkan\\n' >> /etc/environment\n" + "fi\n" + "echo \"OK: GTK Vulkan disabled; OpenGL fallback enabled\"\n" + "\n" "# --- STEP 7.4: isolated apt sources + offline update + install all build tools ---\n" "#\n" "# Use a DEDICATED sources-parts dir so apt-get update + install only\n" @@ -1597,6 +1628,17 @@ static void plant_firstboot_service(ext4_writer_t *ew) "_check_file /etc/apt/appsandbox-sources.list.d/appsandbox-local.list\n" "_check_file /etc/apt/appsandbox-sources.list.d/appsandbox-local-extras.list\n" "_check_glob '/opt/appsandbox/local-apt-extras/*.deb'\n" + "echo \"-- desktop runtime repairs --\"\n" + "if getcap /usr/lib/snapd/snap-confine 2>/dev/null | grep -q 'cap_dac_override'; then\n" + " echo \" [OK] snap-confine capabilities\"\n" + "else\n" + " echo \" [MISSING] snap-confine capabilities\"\n" + "fi\n" + "if grep -q '^GDK_DISABLE=vulkan$' /etc/environment 2>/dev/null; then\n" + " echo \" [OK] GTK Vulkan disabled\"\n" + "else\n" + " echo \" [MISSING] GTK renderer policy\"\n" + "fi\n" "echo \"-- staged source trees (from host prefetch) --\"\n" "_check_file /opt/appsandbox/agent-src/Makefile\n" "_check_file /opt/appsandbox/asb_drm-src/dkms.conf\n" diff --git a/tools/linux/agent/appsandbox-display.c b/tools/linux/agent/appsandbox-display.c index d92de73..7211575 100644 --- a/tools/linux/agent/appsandbox-display.c +++ b/tools/linux/agent/appsandbox-display.c @@ -193,7 +193,13 @@ static int drm_acquire_fb(struct capture_ctx *c) goto out; } - drmModeFB2 *fb2 = drmModeGetFB2(c->fd, p->fb_id); + /* Preserve the framebuffer id before freeing the plane. Mutter + * rotates primary buffers continuously; reading p->fb_id after + * drmModeFreePlane() can tag the new mapping with stale heap data, + * making subsequent flips reuse the wrong (often initial blank) + * framebuffer. */ + uint32_t fb_id = p->fb_id; + drmModeFB2 *fb2 = drmModeGetFB2(c->fd, fb_id); drmModeFreePlane(p); if (!fb2) continue; @@ -223,7 +229,7 @@ static int drm_acquire_fb(struct capture_ctx *c) } drm_release_fb(c); - c->fb_id_last = p->fb_id; + c->fb_id_last = fb_id; c->width = width; c->height = height; c->stride = pitch;