Repository navigation
daemon: stop closing adopted serial and input fds twice - #93
Open
collegeming wants to merge 213 commits into
Open
collegeming wants to merge 213 commits into
collegeming wants to merge 213 commits into
Conversation
feat(vm): add prepare lend mthp option
Optimize UI event handling to reduce main thread saturation
- Add applyForward/removeForward abstract methods to FirewallHelper - Implement DNAT and forward rules in IptablesBackend with proper wildcard host IP handling - Add insert() method to ChainInfo for ordered rule insertion - Integrate VMPortForwarder lifecycle into VMInstance start/stop - Add PortForwardListHandler for IPC port forward queries - Add VMPortForwarder to manage configured port forwards
- Add PortForwardSetHandler for IPC communication - Implement VMEditPortForwardTab for editing port forwards - Add PortForwardEditAdapter and ViewHolder for RecyclerView - Enhance VMInfoActivity to display active port forwards - Create PortProtocol enum for TCP/UDP support - Add UI layouts for port forward editing and display - Add localized strings for port forwarding features This commit introduces a complete port forwarding management system with both edit-time configuration and runtime display.
- Add volatile modifier to ensure thread visibility - Replace polling-based loop with wait/notify event model - Refactor reconcile logic with atomic rule switching and better error handling - Improve handling of unresolved guest IPs across retry attempts
Add DHCP options and prefer prebuilt dnsmasq binary
Implement comprehensive IP-based port forwarding management
- Set prepare_lend_mthp to false in default AgentVM config
Set up run directory ownership and disable mthp lending in agent
feat(settings): add option to clear logs before starting VM
- bump actions/upload-artifact to v7 - set archive: false to avoid re-zipping already-compressed apk
chore(ci): skip re-compression when uploading artifacts
- Added ShareUtils for exporting logs to system share sheet via FileProvider - Integrated share option in Logs and VM Console menus - Configured FileProvider with cache directory for safe file sharing - Added bilingual string resources for share actions - Refactored VMConsoleActivity to extract console text fetching logic
feat(logs): add share functionality
Rework VM networking around typed network and NIC config, Linux/gVisor bridge backends, VLAN-aware L2/L3 behavior, bridgedhcp PD/DHCP service integration, pbridge/gvswitch tooling, and per-NIC static leases/port forwards. Add the netbox helper and focused network/store tests, and move the old VM-level port-forward editor into NIC/network configuration. Co-authored-by: KusakabeShi <github@kskb.eu.org> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add lbx-backed boot-entry scanning, BootConfig/BootPlan resolution, a VM boot menu, direct Linux boot UI, and protected-VM DMA warnings for kernels that lack CONFIG_DMA_RESTRICTED_POOL. Expose kernel analysis from URL and LXC image import flows without coupling it to the background download service. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Move disk image downloads out of the import activities into a foreground service backed by a self-written resumable downloader, with notification progress, retry/pause state, and one active import at a time. Rewire URL, flat image, and LXC import screens to attach to the service state while keeping DownloadWidget display-only. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Apply IME inset padding globally while exempting console/display surfaces, preventing the soft keyboard from covering editor UI. Make the LXC import screen usable immediately from bundled source metadata, then refresh mirrors in the background without clobbering an explicit user selection. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add a one-shot build script for submodules, third-party networking/boot tools, and APK output, and ignore generated third-party checkouts and staged binaries. Teach the app build and asset extraction path about the new gvswitch, pbridge, bridgedhcp, lbx, and netbox binaries, including lbx JNI packaging for UI-side execution. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
crosvm can't read zlib-compressed qcow2 (guest gets vda I/O errors and no partition table, so any root= hangs). Before starting a crosvm VM, detect compressed qcow2 disks via lbx (new daemon disk_compat handler + BootPlan.hasCompressedClusters) and prompt to convert (decompress in place via DiskOperationActivity, path-mode) then auto-start; no response in 5s = start anyway. URL analysis now uses lbx 'entries --quick' to skip unused per-entry reads. UI: kernel/initrd/cmdline detection fields are read-only but selectable with a copy button; tapping the detected cmdline seeds the (now multiline) override field at the same caret offset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Terminal copy/paste was a no-op: SimpleTerminalSessionClient's onCopyTextToClipboard/onPasteTextFromClipboard were empty, so Termux's selection 'Copy'/'Paste' never reached the system clipboard. Implement both (reusing CopyableField for copy, emulator.paste() for paste) and pass an owning Context to the client at all three terminal screens (VM console, disk op, agent op). Also default the terminals to Maple Mono NL NF via TerminalView.setTypeface, with a Termux-compatible $HOME/.termux/font.ttf user override and a system-monospace fallback (TerminalFonts). The ~2.3MB TTF is git-ignored and fetched in build.sh (pinned v7.9, sha256-verified); only the OFL license is committed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…om DroidVM-Prebuilt-Root - Drop bundled 7za; decompress the runtime payload in-process with org.tukaani:xz + a minimal USTAR reader (AssetUtils). - Runtime prebuilt is prebuilt-<abi>.tar.xz; the third-party daemons + lbx ship inside it (DATA/bin/<name>), so the 7za/thirdparty asset extraction and the BINARIES_PREBUILT/THIRDPARTY constants are gone. Stale assets/bin daemons removed; *.7z and *-comptime.zip excluded from APK assets. - lbx also ships in prebuilt-<abi>-comptime.zip, unpacked at Gradle build time into lib/<abi>/liblbx.so (UnpackComptimeJniLibsTask). - Terminal font fetched by a Gradle task (FetchTerminalFontTask) so CI and local builds match; build.sh slimmed to preflight + submodules + gradle, with a native-dev toolchain check when DroidVM-Prebuilt-Root is present. - regenPrebuilts runs auto-build.py when DroidVM-Prebuilt-Root is checked out. - Drop x86_64 from abiFilters (no x86_64 VM runtime exists). - Workflows: add update-latest-release.yml; add manual workflow_dispatch to build.yml and release.yml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Migrate pre-rework networks.json (flat schema) to the v2 L3/VLAN model; the app and daemon load sites validate the migrated result and skip it (with a warning) when it does not hold up. - Fold the legacy VM-level port_forwards array into each NIC's DHCPv4 lease (structural only -- the lease offset is left unassigned). - Refuse a static lease with no assigned offset at validation, so a migrated VM cannot boot at a wrong default IP. - When starting a VM from the app, allocate any missing offset conflict-free (from 64 up, skipping the dynamic pool) and persist it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FikGHtfpTGfjxfEfuUF1Ra
Convert the Simplified Chinese strings to Traditional Chinese with Taiwan vocabulary (e.g. memory 内存 -> 記憶體, 软件 -> 軟體, 镜像 -> 映像, 权限 -> 權限) and register zh-TW in the per-app locale config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FikGHtfpTGfjxfEfuUF1Ra
The dev-release and release workflows built and published an unsigned release APK whenever APP_SIGN_KEY_JKS was absent. Instead fall back to assembleDebug (signed with the auto-generated debug keystore) so the published artifact is always installable. The signed release path is unchanged when the key is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FikGHtfpTGfjxfEfuUF1Ra
…keover The daemon had no cross-process singleton guard: it picked a random port and overwrote the pid file, so a racy app-side check (or an explicit start) could leave two daemons running and fighting over VMs, networks and iptables. Take an exclusive advisory lock on droidvmd.lock at startup, held for the process lifetime so the kernel releases it automatically on death (incl. SIGKILL). A normal launch exits when the lock is held; with --force the new daemon terminates the running one (SIGINT, then SIGKILL) and takes over. restartDaemon() now force-launches instead of the racy stop-then-wait loop. Because a SIGKILL'd daemon skips cleanup, its crosvm/qemu children orphan and survive; reap them on startup alongside the existing dnsmasq/gvswitch/pbridge cleanup, matched by full prebuilt path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FikGHtfpTGfjxfEfuUF1Ra
Each host module under Settings supplies something a particular configuration reaches for at run time, and without it the VM fails as anything but a missing module: a guest that cannot see its own RAM, GPU allocations failing at random, a big VM refusing to boot with "out of memory" on a phone with memory to spare. A pre-start guard asks up front, from what the config actually turns on -- pseudo-unprotected RAM wants gunyah_host_share, GPU acceleration wants gh_unmovable and udmabuf, and a VM over 2 GB wants the kvcalloc fix. Which of those apply to this phone is not decided here. The Kernel Module list already answers it -- the KMI directory picks the build for the running kernel and match.json drops what was written for another SoC -- so a module the list does not offer is skipped without a word rather than warned about. That is also why the kvcalloc rule needs no "and only on the 8 Gen 3": the fix is built for the 6.1 GKI whose Gunyah driver has the bug, and for Qualcomm, so it can only surface there. It keeps working as the module set grows to other vendors. The dialog offers to start anyway (5s countdown, the answer an unattended start gets), to open the module page, or to cancel. Nothing here loads a module on the user's behalf: that is a decision of its own, made where the modules are managed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BTc7yZ1DPCX7xpbYcUo2V
…stead of crashing Stored values outlive the sets that produced them: an option gets retired between releases, and several graphics rows build their item set out of another row's value, so a config written under one combination is routinely read back under another. EnumPicker answered that with an exception, which made every restore path a crash waiting for the first user whose VM predates the current build -- and it arrived: an old VM naming a GL provider, under a backend whose set no longer lists one, took the editor down as it opened. setSelectedItem now lands an item the picker does not list, or lists only to refuse, on the row's default and returns whether the value itself was taken. setDefaultItem names that default wherever the head of the list is not the sensible answer -- a transport ladder whose head is the CPU copy should not be where "that rung is not built yet" ends up. Refused now means refused from every direction, a stored config included, so the picker no longer sits on a value it would not let the user pick; rotate-mode steps over refused rungs for the same reason. The graphics tab stops re-deriving which values belong to the current backend -- the copy of that rule at the call site is what did not cover the retired provider -- and hands each stored value to the picker that owns the set. If any row moved, the tab says so once: what is on screen is not what was saved, and the user should not have to notice that on their own. PanVK joins the same mechanism as a listed-and-refused item, replacing its toast-and-revert listener. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BTc7yZ1DPCX7xpbYcUo2V
All five came out of a PR review of this branch. What they have in common is that each fails somewhere the message does not point at: a black screen with the stream still arriving, a boot that stops at an ioctl, a daemon that answers nothing for ten seconds, a prompt about memory a VM never touches. The review's other findings were checked and left alone -- either correct as they are, or a pre-existing limitation this branch did not introduce. vnc: the sync frame outlives the pipeline that was not there to take it. The server sends the parameter sets exactly once per client, on the reset-flagged rect that starts its stream, and the presentation console has nowhere to put that rect: it connects when it opens and builds its decoder only once a display has been chosen. The rect was dropped, every bare IDR after it decoded to nothing, and no reconnect followed because rects were still arriving. H264SyncFrameCache holds it for the connection instead of for the pipeline, which also covers the window being dismissed and rebuilt, and the race where DesktopSize reaches the main thread a post later than the sync rect reaches the message loop. hugepage: the reserve is asked about only the VMs that draw on it. The pre-start check sized every VM against pool_avail, including the ones on hypervisors that hand no memory away and cannot hit the failure it exists to avoid. They got the dialog, or ten seconds of waiting, for a pool their boot will not spend a page of. display: a refused socket path gives back the sockets bound before it. startListening() published its slots only after the last one bound, so the release() the caller already makes on that path freed an empty set -- leaking listening fds, inodes under run/, and an accept thread each, parked in accept(2) for the life of the daemon. Unreachable today; the two screen ids are spelt g0 and sfb precisely so their names stay short, and a third screen is what would arrive without a tag. The test pins the budget such a name would be chosen against. daemon: auto-start runs behind the socket, not in front of it. The sweep waits on the reserve from inside the ServerContext constructor, which is before the socket binds, before the signal handlers exist and before the VM event callback is wired. It runs from Server.run() on its own thread now, which is also why the sweep snapshots its list rather than iterating across the wait, why start() takes a lock from the state test to the worker, and why stopAll() can call the wait off: a daemon answering clients while the sweep runs is a sweep with company. vm: refuse to start a VM whose LEND parcels this kernel will not take. 6.6 demand-pages a parcel; 6.1 commits every one as it arrives and refuses a later one, so a 3 GB VM configured for 256 MB parcels does not boot there and says only "failed to initialize virtual machine". A better default would not have helped, because the value travels: an imported VM carries the mode of the phone its package came from. The dialog offers to correct it and keep the correction. Verified on a 6.1 Qualcomm device. The daemon bound its socket, logged the reserve wait 32 ms later, and authenticated a client 122 ms after that with nine seconds of waiting still to go; a SIGINT mid-wait was answered rather than queued behind it. The LEND dialog appeared on start, its fix wrote single to vms.json, the next crosvm command line carried --prepare-lend-mthp-mode single, and the VM reached RUNNING where it had been dying at GH_VM_START. 293 unit tests and the five CI source checks pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QzTPwWuSaDfFJg62dtqZW Signed-off-by: HuJK <gh@hujk.org>
The UEFI branch of the crosvm command builder passed the builtin firmware whatever the config said, under a comment claiming crosvm had no custom-firmware support. It has nothing else: the path goes in the same positional slot a direct-boot kernel would, and crosvm opens it with open_file_or_duplicate like any other file. Nothing on that side is attached to the builtin path. Everything around it was already wired. The editor has the firmware field, its browse dialog offers builtin or external, and validateInput checks the path exists; the exporter packs a custom firmware into the package (it is skipped only when it matches a builtin), and the importer remaps it like any other boot file. The one line here is what dropped it, so a user could choose a firmware, save it, pass validation, and have it silently ignored at start -- worse than the feature being absent. No size check to add alongside the vars one. That limit is pflash's: AARCH64_PFLASH_MAX_SIZE is AARCH64_PLATFORM_MMIO_SIZE, 8 MiB, and setup_pflash refuses a larger image because pflash is a device on the MMIO bus between the PCI aperture and the Gunyah VMMIO limit. Firmware is a payload loaded into guest RAM -- the Bios arm of that match passes u64::MAX as its bound -- so it is limited by the VM's memory and nothing else. Verified on device: with the firmware set to a copy of the builtin at /data/local/tmp/fw/my-edk2.fd, the crosvm command line ended in that path instead of usr/share/droidvm/edk2-gunyah.fd, --pflash still named the builtin vars, and the VM reached RUNNING. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QzTPwWuSaDfFJg62dtqZW Signed-off-by: HuJK <gh@hujk.org>
feat: 3d acceleration, bunch of reworks
The editor warned that a width off a 16-pixel grain would cost the GPU copy, because simplefb's pitch was width * 4 with nothing padding it and the blit's LINEAR dma-buf import wants 64 bytes. The host pads the row now and decides the stride in one place, so the width costs nothing and there is nothing to warn about: the condition, the warning it fed and the test that pinned it all go. Squashed from 2 commits on wip/usb. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrfBDYZexyRUKdQyJ3jQ6q
…which Setting the root password or growing the disk is preparation, not the import. The download had already succeeded and the image was already there, and a failure in either ended the run and threw the VM away -- so a mistyped password or a resize that could not finish cost the user the whole thing. An action can be optional now. The queue script keeps its outcome in a state directory instead of acting on it where it happens: fail() and skip_action() run inside the action's own subshell and only record what went wrong, and end_action() reads that back and decides what it means. A required action still ends the run at the first error; an optional one adds itself to FAILED_ACTIONS and the actions behind it still get their turn. The import flow then creates the VM whatever the preparation managed, and names the steps that did not complete -- the resize, the password, or both when the rescue VM was cancelled or died and can say nothing about either. Those names survive a rotation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrfBDYZexyRUKdQyJ3jQ6q
…is app has Choosing English gave Simplified Chinese. The app was doing nothing wrong: the picker set the locale to "en", localeConfig declares en, and the resource table carries English. The platform refused it anyway. Measured on a device whose system list is [en_US, zh_Hans_CN, zh_Hant_TW, zh_Hant_HK]. Asking for ja, fr, de or ko -- none of which this app translates -- gives the activity a resource configuration of [zh_CN], and so does asking for en. Asking for zh-Hant-TW gives [zh_TW_#Hant, en_US, ...], the app locale followed by the system's, which is what a locale the app supports looks like. The collapse target is the proof. A refused locale falls back to the system list, and that list begins with en_US -- yet it lands on zh_CN, the second entry. It skipped English, so English is not in the set the platform matches against. de and ko collapse too, though AndroidX translates both, so that set is not the resource table either: it is the app's own locale-qualified folders, values-zh-rCN and values-zh-rTW. English lives in the unqualified values/, which is not a locale, so the app appears to have none. en-US is the exception because it is the system's first locale: asking for what the system already has needs no override, so nothing is filtered. That makes this a fix for the device in hand rather than for the cause. Curing the cause means a values-en holding the English strings, generated at build time so there is no second copy to keep in step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrfBDYZexyRUKdQyJ3jQ6q
The hint said the custom firmware field applied to QEMU only. crosvm honours a custom EDK2 the same way now, so the sentence describes a limit that is no longer there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrfBDYZexyRUKdQyJ3jQ6q
The daemon reached res/xml/qcom.xml through createPackageContext on its system context, which needs an attached ActivityThread the daemon does not have -- so the lookup threw where it was called and the host SoC came out as its raw model string, or as nothing at all. It reads the table straight out of the APK now, with the package context left as the fallback, and a failure anywhere in the chain is logged and answered with null instead of thrown. More chips in the table while we are here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrfBDYZexyRUKdQyJ3jQ6q
gh_hugepage_reserve exists because Gunyah could not take guest memory without migrating it out of CMA first. Qualcomm fixed that in 6.18, so on those kernels the module is not a fix, it is a module the user is asked to load for nothing. match.json entries take a kernel_below now, and HostKernel can order two series the way version numbers go rather than the way strings do -- 6.6 is below 6.18, which no textual comparison of the two will tell you. A kernel it cannot read matches as before: not knowing which side of the line a host falls on is not a reason to hide a module that might be needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrfBDYZexyRUKdQyJ3jQ6q
A host USB device can be handed to a running VM, by hand or by rule. The daemon owns it. It watches /dev/bus/usb and the kernel's uevent socket -- the second because binding or unbinding a driver moves no node and nothing else reports it -- and keeps an inventory of every device including the root hubs, whose arrival nothing else reports either and whose absence is a bus on which nothing can ever enumerate. Attaching takes the interfaces off their host drivers and hands the device to crosvm; releasing gives them back, and a VMM that died holding one has its claim taken off it and the device restored. Rules decide it without anyone watching. Four ordered zones -- exact, port, device, and the catch-all -- each a list tried in order, each rule naming a target: a VM's xHCI controller, the host, or idle. A fifth zone nobody sees is "no match means the host". Passes run on a plug, on a driver change, when a VM reaches RUNNING, when the rules are saved, and at daemon start, and a device somebody moved by hand is locked against them until it is unplugged. The gate is drivers_autoprobe. With passthrough on, an arriving device binds no driver of its own and the daemon stands in for the kernel: it probes the device so the configuration is chosen, then its interfaces, and only for what the rules say goes to the host. What a device is doing is read from the drivers bound to it -- hostuse, vmuse, idle -- and stored nowhere. The UI is three surfaces: a USB passthrough page of four folding zones with a card per rule and a master switch over them, a management page that moves one device now without touching the rules, and an xHCI controller peripheral in the VM editor whose card binds rules straight to that controller. Squashed from 39 commits on wip/usb. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrfBDYZexyRUKdQyJ3jQ6q
… in lent memory lbx now reports what each boot entry actually is -- linux or windows -- and can read a Linux entry's own config-*. Two things the app could only guess at before follow from that. The boot menu stops offering a direct kernel boot for an image that holds Windows. Direct boot loads a kernel and an initrd; a Windows install has neither, so the entries were listed and the start failed later with nothing useful to say. The menu now says so where the choice is made, and points at the built-in kernel or the UEFI protocol. And a protected VM asks, before it starts, whether this guest can live with its memory lent to the hypervisor. Two ways it cannot, and the image decides which applies rather than the boot protocol. A Linux kernel built without CONFIG_DMA_RESTRICTED_POOL has nothing for virtio to bounce through and comes up with no disk and no network -- lbx reads that out of the entry's config-*. A Windows guest with USB passthrough has nowhere to map transfer buffers, so the rules pointing at it do nothing; Linux guests are fine there, because the pool they need anyway covers USB too. Both answers are the same answer, which is why it is one check: pseudo-unprotected shares the memory instead of lending it. Only what is certain is raised. A bootloader lbx cannot read, an OS this build has no rule for, a kernel with no config-* to read -- each is a question with no answer, and a warning nobody can act on is worse than none, so each ends the check quietly. The dialog offers the run that works (pseudo-unprotected, once) beside the one that was asked for, and the button counts down so a start nobody is watching is never blocked. A finger on the dialog stops that countdown for good and puts the button's own label back -- someone is there, reading, and the answer is theirs to give, which is what pressing a key during GRUB's menu timeout does. The boot menu's own countdown stops the same way. Every guard prompt lays its answers out the same way now, by what pressing one means: neutral is go ahead changing nothing, positive is go ahead with the change that fixes it, negative is do not go ahead. The countdown sits on the neutral button and falls to the positive one only when a dialog has no neutral, which is to say when going ahead unchanged was never on offer -- so a start nobody is watching is never blocked and never has a setting changed under it. The kernel-module guard and the boot menu follow the same layout, and the theme sets the positive button in bold, since it is the only one that alters anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrfBDYZexyRUKdQyJ3jQ6q
…follows it A keyboard attached to Android could not send a guest the keys that matter most in one: Android decides what an Activity may see before the Activity sees it, and Home, the task switcher, Alt+Tab and every Meta shortcut are taken by the window manager and never dispatched. No console written on top of dispatchKeyEvent can be sent them. So the daemon takes the keyboards instead (EVIOCGRAB), which makes its descriptor the only recipient the kernel's input core hands events to -- Android's InputReader keeps its own open and is simply never woken again -- and writes the raw evdev into the same per-screen keyboard socket the console's on-screen keys already use, so nothing downstream has to know where a key came from. A keyboard is any /dev/input node whose EV_KEY bitmap has q, a, z, enter and space: a vendor folio, a Bluetooth keyboard and a USB one alike, while the volume rocker and the touchscreen are left where they are. When, and only when: the console is in front, the screen takes input, and the typing surface is not the one whose text comes from the IME. A grabbed keyboard is invisible to an IME, so KeyboardMode.SYSTEM hands it straight back and composed input goes on working exactly as before. A console stays armed while it holds nothing, because it is usually up before the guest it starts and because a Bluetooth keyboard comes back as whichever eventN is free: the VM state hook and a uevent watcher grab those. Releasing lifts every key still held, so no guest is left with a Ctrl down, and the console's binder token releases the grab if the console dies. The escape chord is implemented and unbound -- which combination will not fight the guest is the user's to say once there is a page to say it on; until then the two ways out want no keyboard anyway (leave the display, or switch to the system keyboard). The guest talks back, and nothing was listening. Its driver reports the keyboard lamps on virtio-input's status queue, crosvm writes them onto the same socket, and the daemon only ever wrote to it -- so the records sat in a buffer nobody drained and the caps light on the real keyboard stayed dark. Read them: the lamps are set on every grabbed keyboard (one that joins late is synced to what the guest already said) and passed to the console. The guest's own view is the only correct one, since software inside it can turn caps lock on with no key involved, and a lamp it has never reported is left unknown rather than guessed at, because virtio-input gives no way to ask. The console gets a copy of the keys for the same reason it had none: they never pass through the app. IPhysicalKeyEcho is oneway, called after the keys have reached the guest, and registered only in the mode that draws a keyboard. A drawn key lights from the union of three reasons -- the panel holds it sticky, the physical keyboard holds it, the guest says its lock is on -- so releasing a hardware Ctrl no longer un-paints one the panel is holding, and the key faces follow a physical Shift as well as a sticky one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzSVsTJs6p3V2pFZLaXq9U
The pin was three publishes old (5cc4aa8, from DroidVM-Prebuilt-Root 14c5886): it predates the USB passthrough work, both simplefb fixes and the keyboard bitmap, so an APK built from this branch ran a crosvm that had none of them and the branch could only be tested with a hand-copied binary. Move it to f0af07a, which CI published from DroidVM-Prebuilt-Root 4215d7b. Checked rather than assumed: that publish changed one sha256 line and the payload, its usr/bin/crosvm is sha256 ee531987e942dc2ed22441d915e6d6a403a8761a 3bc1f70683fea2742dcd1334, and that is byte for byte the binary built from crosvm pr/usb 8d5268b (wip/usb 4fe20fa) and verified on the device -- an Ubuntu guest started with it advertises KEY_LEFTMETA through KEY_MICMUTE on its virtio keyboard. The EDK2 firmware in the payload is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzSVsTJs6p3V2pFZLaXq9U
feat: USB passthrough
The pin was f0af07a, whose usr/bin/crosvm is ee531987e942dc2ed22441d915e6d6a403a8761a3bc1f70683fea2742dcd1334 -- the binary built from crosvm pr/usb 8d5268b. It predates the DMA-window fix, so an APK built from this branch still died the moment a guest enabled display output on a passed-through DisplayLink adapter. Move it to c4998e1, which CI published from DroidVM-Prebuilt-Root 82c59c2, the merge of Droid-VM/DroidVM-Prebuilt-Root#4. Checked rather than assumed: that publish's usr/bin/crosvm is sha256 cd3693ba876720f7ab7c08e5d6358101336f3bbcdb3d7d4f324ac29274bf6a7b, which is byte for byte the binary built from crosvm wip/usb-fix-displaylink-crash (Droid-VM/crosvm#7, merged) and shipped to the customer who reported the crash. It carries two fixes: usb: a transfer too big for the DMA window goes through a plain buffer block: the queue size is a per-disk option, and its default stops throttling viostor c4998e1 rather than 0344eea, the identical payload CI published from the branch before it merged: that one is reachable only from a wip branch, and a pin whose commit lives on a branch someone is about to delete breaks `git submodule update` for everyone on master. The payload also loses usr/bin/crosvm.bak1400, a 12 MB backup from 2026-09-03 that every APK had carried and every device had unpacked; the debug APK goes from 130 MB to 126 MB. The EDK2 firmware is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrTFaLfTucJb9LBtxtU6Ab
…crash prebuilts: the payload carries the DisplayLink crash fix
Added a custom sponsorship URL for funding.
Due to both having ARMv9 and they also have gunyah (for most of 7s Gen 3 as well as the sibling of it is 7s Gen 4) and as for SM8635 the sibling of it is SM8650.
Include Snapdragon 7s Gen 3/4 and Snapdragon 8s Gen 3
fix(ui): Material menu not showing on some device
Signed-off-by: Molly Sophia <mollysophia379@gmail.com>
…d-gunyah-only fix(vm): restrict pseudo-unprotected mode to crosvm Gunyah
SerialPipe hands both local pipe ends to FDPipeConsoleStream, which adopts them into ParcelFileDescriptor; the stream is their sole owner from then on. close() closed the stream first and then closed the saved integers again. The kernel can reuse a released number between those two steps, so the second close takes down whatever now owns it, and fdsan aborts the daemon when that happens to be a Java stream. Track which ends were transferred, detach them before any closer can run, and never close those raw again. closeRemoteFd also stops reading pipe_in under a pipe_out guard. Constructor failure still closes only the ends that were not adopted. NativeDisplayInputBridge released its slot map without synchronizing, so two concurrent releases could take the same list and close the same listening descriptors twice. The accept loop also needed a way to stop without another thread closing the descriptors it was still using. Give each generation one owner: release detaches the whole map and closes only the wake pipe writer, whose HUP wakes poll, while the accept worker closes the listener and read end after its last syscall. The listener is non-blocking so a readiness that disappeared cannot leave accept parked. Tests are pure-Java unit tests for both owners: descriptor-number reuse after a close, concurrent release, and partial transfer failure. They do not run on Android or exercise the JNI layer. Signed-off-by: zhengwenming <zhengwenming@apexsoft.com.cn>
collegeming
force-pushed
the
fix/fd-double-close
branch
from
November 8, 2026 10:33
e5bb782 to
641d44e
Compare
BigfootACA
requested changes
Nov 10, 2026
BigfootACA
left a comment
Member
There was a problem hiding this comment.
Please rebase your commits and fix conflicts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
Two descriptor-lifetime defects in the daemon. Both are reachable in normal operation and both close descriptors the code no longer owns.
SerialPipecloses adopted pipe ends twice. The constructor hands both local ends toFDPipeConsoleStream, which adopts them intoParcelFileDescriptor; the stream is the sole owner from then on.close()closed the stream first (releasing those numbers) and then closed the saved integers again. If the kernel hands a released number to anotherFileInputStreamin between, the second close takes that one down.closeRemoteFd()also testedpipe_outwhile readingpipe_in.NativeDisplayInputBridge.release()is not atomic. Two concurrent releases can read the same slot map and close the same listening descriptors twice, and the accept loop had no way to stop without another thread closing descriptors it was still using.Why it matters
On a
0.0.6.r242.g3348830build, one VM shutdown left the daemon dead:That abort is a raw close of a number owned by a Java stream while
Runtime.execwas building its process pipes: the pipe bookkeeping held a descriptor that someone else had already closed and the kernel had reused. Both defects produce exactly this shape (close of an adopted end after the stream already released it, with a reuse window). I am not claiming it is the only possible source of that abort; what is certain is that both paths close descriptors they do not own, and the serial one runs on every VM stop.Changes
SerialPipenow delegates its descriptor bookkeeping to a smallSerialPipeFdsowner. It records which ends were transferred to the stream, detaches before invoking any closer, and never raw-closes a transferred end.close()andcloseRemoteFd()are serialized. Constructor failure closes only ends that were not adopted.NativeDisplayInputBridgegives each generation aNativeInputSocketOwner.startListening/releaseare serialized and detach the map atomically; release closes only the wake-pipe writer, whose HUP wakes poll, while the accept worker closes the listener and read end after its last syscall. The listener is non-blocking so a readiness that disappeared cannot leaveacceptparked. Duplicate slots are rejected before binding, and an old worker never unlinks a path a newer generation owns.Tests
SerialPipeFdsTestandNativeInputSocketOwnerTestare plain JUnit tests, no Android or JNI:closeRemoteFd, reentrant closer, concurrent full/remote close,Local static checks (
check_ascii,check_string_concat,check_arity,check_translations,check_vector_icons) pass on this branch.I could not run this on a device: installing a modified APK requires the project signing key, and updating with a different key would wipe app data. If you build a signed dev APK with this applied, I can run the VM start/stop and input-session lifecycle on a Xiaomi tablet (Android 17, arm64, gfxstream and PanVK routes) and report the result here.
Not changed
adoptFd, that end stays with the caller's stream (the caller keeps the stream and closes it on normal VM teardown). Reclaiming it immediately would need a targeted release API onFDPipeConsoleStream; that is left out of this change.