Skip to content

Fix bash -c argument splitting in kernel config step - #1

Open
gfnord wants to merge 11 commits into
ddcash:mainfrom
gfnord:fix/kernel-config-arg-splitting
Open

gfnord wants to merge 11 commits into
ddcash:mainfrom
gfnord:fix/kernel-config-arg-splitting

Conversation

@gfnord

@gfnord gfnord commented Sep 8, 2026

Copy link
Copy Markdown

What

Fixes a PowerShell parsing bug that made Install-Waydroid.ps1 fail at the "Configuring kernel" step with:

./scripts/config --set-val CONFIG_ANDROID_BINDER_IPC y : -c: line 2: syntax error: unexpected end of file
Command failed (exit 2): wsl -d <distro> -e bash -c cd ~/src/wsl-kernel && ...

Why

The kernel config command was built by concatenating string fragments inside an @(...) array literal:

Invoke-Wsl @("-d", $DistroName, "-e", "bash", "-c",
    "cd ~/src/wsl-kernel && zcat /proc/config.gz > .config && " +
    "./scripts/config --set-val CONFIG_ANDROID_BINDER_IPC y " +
    ...)

In PowerShell a newline inside an array literal separates elements, so a trailing + does not continue the expression. That array parses to 12 elements, not 6 — verified:

count=12
[-d] [X] [-e] [bash] [-c]
[cd ~/src/wsl-kernel && zcat /proc/config.gz > .config ]
[./scripts/config --set-val CONFIG_ANDROID_BINDER_IPC y ]
...

So bash -c received only the first fragment — a script ending in a dangling && — hence the syntax error. The remaining fragments were passed as $0, $1, …, which is why the error message quotes ./scripts/config --set-val CONFIG_ANDROID_BINDER_IPC y as the shell name.

A useful tell in the failing output: the echoed command line has doubled spaces at each fragment boundary, because Write-Info renders $WslArgs -join ' ' and each fragment already ended in a space.

How

Build the command in a $configCmd variable using backtick line continuations (which do continue across newlines), then pass it as a single argument. Added a comment explaining the trap so it doesn't get reintroduced.

Verification

  • [System.Management.Automation.Language.Parser]::ParseFile on the edited script: no errors.
  • The argument array now parses to 6 elements, with the full one-line command in the last one.
  • Ran the fixed command against a real wsl-kernel clone (Ubuntu WSL2 distro): exit 0, make olddefconfig completed, and .config contains:
    CONFIG_ANDROID_BINDER_IPC=y
    CONFIG_ANDROID_BINDERFS=y
    CONFIG_ANDROID_BINDER_DEVICES="binder,hwbinder,vndbinder"
    

Notes for the reviewer

  • I grepped both Install-Waydroid.ps1 and Uninstall-Waydroid.ps1 for the same pattern (a line ending in + inside an array literal). This was the only occurrence.
  • Behavior is otherwise unchanged — same command text, same options, just delivered as one argv entry.

gfnord and others added 11 commits September 7, 2026 17:27
The kernel config command was concatenated from string fragments inside
an @(...) array literal. In PowerShell a newline inside an array literal
separates elements, so a trailing "+" does not continue the expression --
the array parsed to 12 elements instead of 6.

As a result "bash -c" received only the first fragment,
"cd ~/src/wsl-kernel && zcat /proc/config.gz > .config &&", a script
ending in a dangling "&&", and failed with:

    -c: line 2: syntax error: unexpected end of file

The remaining fragments were passed as $0, $1, ... which is why the
error quoted "./scripts/config --set-val CONFIG_ANDROID_BINDER_IPC y"
as the shell name.

Build the command in a variable using backtick line continuations, then
pass it as a single argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Install-Waydroid.ps1 is CRLF, so every multi-line string literal carries
CR characters, and bash treats CR as part of the token. Two failures:

1. The /etc/wsl.conf step opens a heredoc with << "PYEOF". The CR after
   the closing quote makes the delimiter PYEOF<CR>, which no line matches,
   so bash consumed the terminator as script text:

       warning: here-document at line 1 delimited by end-of-file
       NameError: name 'PYEOF' is not defined

   The file was actually written, but python3 exited 1 and the installer
   aborted.

2. The launcher .sh templates were cp'd into /opt/waydroid-launcher
   verbatim, keeping CRLF. The CR after "#!/bin/bash" makes the kernel
   look for an interpreter literally named "/bin/bash<CR>", so every
   launcher would have died at runtime with exit 127:

       cannot execute: required file not found

Fixes:

- Invoke-Wsl normalizes CRLF to LF on every argument before handing it to
  wsl.exe, so any multi-line payload is what a Linux shell expects.
- The launcher install strips CR with tr instead of a plain cp. (tr rather
  than a sed "$"-anchored substitution, because "$" collides with
  PowerShell variable expansion inside a double-quoted string.)
- .gitattributes forces LF on *.sh and *.py so a fresh clone does not
  reintroduce the problem, while keeping *.ps1 / *.bat / *.tmpl CRLF.

Verified against a live Ubuntu WSL2 distro: the wsl.conf step now exits 0
and writes the expected [boot] section, and a launcher installed through
the new path reports "ASCII text executable" with a bare LF shebang and
passes bash -n.
The launcher runs Android inside a nested Weston compositor (the documented
workaround for Waydroid rendering a stuck 1x1 buffer against WSLg's
compositor), but nothing ever installed weston. A completed install would
produce desktop shortcuts that silently do nothing:

    /tmp/nested-weston.log
      nohup: failed to run command 'weston': No such file or directory
    /tmp/waydroid-session.log
      Wayland socket '/run/user/0/wayland-1' doesn't exist;
      are you running a Wayland compositor?

With no weston there is no wayland-1 socket, so both `waydroid session
start` and `waydroid show-full-ui` fail immediately. Because
waydroid-start-user.sh uses `set -u` without `set -e` and backgrounds
every real command via setsid/nohup with output redirected to logs, all
three failures were invisible and the script still exited 0 -- the .bat
printed "Done." and closed on a 5s timer.

Changes:

- Install weston as its own idempotent step. Deliberately not folded into
  the waydroid install block: that block is skipped wholesale when
  waydroid is already present, which would leave weston missing on a
  re-run or an upgrade. Hard-fails with an actionable message if weston is
  still absent afterwards.
- waydroid-start-user.sh checks for weston up front and exits 1 with an
  explanation, instead of running headlong into an unusable display.
- Start-Waydroid.bat checks errorlevel and pauses, so the failure is
  readable instead of scrolling past before the window closes.

Verified on Ubuntu 24.04 / WSL2: with weston 13.0.0 installed the launcher
brings the session up ("Android with user 0 is ready", status RUNNING,
wayland-1 socket created). With weston hidden from PATH the new guard exits
1 and prints the install hint.
The step that hides Waydroid's per-app .desktop files ran immediately after
`waydroid init`, before any session had ever started. Waydroid generates
those files when a session first starts, so the glob matched nothing and
the `mv` silently did nothing -- `2>/dev/null; true` swallowed the error and
the step reported success.

The result was the exact failure the step exists to prevent: after a
completed install every Android app was still registered with WSLg, and
Windows indexed all of them into the Start Menu. Observed on a real
install -- 12 files left in ~/.local/share/applications, 0 in
~/.local/share/applications-disabled.

Move it to the end, after the smoke test has brought a session up and the
files actually exist. Also report how many were moved, so a future silent
no-op is visible, and warn when -SkipSmokeTest means no session has run
yet and there is genuinely nothing to move.

Verified against a live install: the relocated step moves 12 files and
reports 12; the count command returns "0" (not empty) when the directory
does not exist, so the -SkipSmokeTest warning branch fires correctly.
Clicking "Start Waydroid" a second time left Android running but invisible,
with no way to recover short of a full stop.

waydroid-start-user.sh unconditionally began with:

    pkill -f 'weston --backend=wayland-backend'

so a second click killed the compositor out from under the running session.
The session process survives, but its Wayland connection is gone, and the
script's next step then hits:

    [19:34:20] Session is already running

`waydroid session start` is a no-op while a session exists, so the session
never reattaches to the newly spawned weston. Confirmed with `ss -xp`:
nothing at all connected to /run/user/1000/wayland-1, while the session
process from the first start was still alive and the weston it had been
talking to was a different, already-dead pid. `waydroid show-full-ui`
returns 0 and does nothing. The result is an empty Weston window and no
Android UI.

Handle the two states explicitly instead:

- Session RUNNING and weston alive: just re-show the UI and exit. Do not
  touch the compositor the session is attached to.
- Session RUNNING but no compositor (the stuck state above): stop the
  session first, then start cleanly, since it cannot be reattached.

Verified end to end on Ubuntu 24.04 / WSL2:

- from stopped: starts, "Android with user 0 is ready", session RUNNING
- second click while healthy: prints "Waydroid is already running;
  re-showing the UI", weston pid 8528 before and 8528 after
- click while stuck: prints "Stopping a session left without a compositor
  before restarting", tears down, comes back up on a fresh weston with
  "Android with user 0 is ready"
…ndow

The installer mounted a tmpfs at /mnt/shared_memory via a wsl.conf boot
hook, believing it fixed WSLg's "[WARN:COPY MODE]" black-window bug. It
causes that bug. The tmpfs shadows the shared-memory transport WSLg uses to
hand buffers to the Windows RDP client, so WSLg falls back to copy mode and
renders every window solid black.

Diagnosis on a real install:

- The Weston window's title read "[WARN:COPY MODE] West...", and Android
  inside it was black -- despite a fully healthy stack: binder OK, container
  and session RUNNING, hwcomposer connected (wl_display_connect returned a
  live handle, EGL init all EGL_SUCCESS), the /run/xdg/wayland-0 bind mount
  inode-identical to the host socket, SurfaceFlinger booted, 77 visible
  layers, launcher the top-resumed activity.
- Isolated it by running a plain weston-terminal directly against WSLg, with
  no Waydroid or nested Weston involved. It was black and in copy mode too,
  proving the fault was WSLg-wide rather than anything Waydroid does.
- Removing the boot hook and doing a full `wsl --shutdown` made that same
  terminal render correctly, and Waydroid's Android UI then displayed.

The hook could never have worked as intended anyway: it loses the race it
was written to win. Measured across two cold starts, WSLg's compositor came
up ~60ms before the mount landed:

    WSLg compositor: 19:15:45.178   shared_memory: 19:15:45.236
    WSLg compositor: 20:34:05.405   shared_memory: 20:34:05.468

Changes:

- Do not write the boot hook, and actively remove it when a previous run of
  this installer left one behind -- otherwise upgrading fixes nothing.
- README: correct the cause/fix table, which stated the relationship
  backwards, and rewrite the troubleshooting entry to tell people to test a
  plain GUI app first and to use `wsl --shutdown` rather than
  `wsl -t <distro>` (terminating one distro leaves the WSLg system distro
  running with the broken state, so the symptom persists and looks
  unfixable).
- The Start Menu .desktop mitigation kept, but its stated rationale no
  longer references a race that no longer exists; it now just keeps a dozen
  Android app icons out of the Start Menu.

Verified: the new logic strips a seeded stale hook while preserving
`systemd = true` and the `[user]` section, and the script parses clean.
Android booted with an IP address but no gateway, so nothing inside it could
reach the network:

    # ip route
    192.168.240.0/24 dev eth0 proto kernel scope link src 192.168.240.112

Host-side NAT was already correct -- waydroid0 up at 192.168.240.1,
xt_MASQUERADE loaded, and the MASQUERADE rule present in the legacy tables.
The gap is entirely inside Android: netd never finishes bringing the link up
because its BPF and xt_quota setup fails against the WSL kernel. Both show up
in logcat:

    E NetworkStats: Unable to swap active stats map:
                    Address family not supported by protocol (code 97)
    E BandwidthController: Updating quota globalAlert failed
                    fopen("/proc/net/xt_quota/globalAlert", "we") failed

DHCP still hands out a lease, so this presents as working networking -- an IP,
a route to its own subnet, no gateway. Adding the route by hand restored
connectivity immediately (8.8.8.8 at 15ms, google.com at 69ms, DNS included),
confirming nothing else was wrong.

Adds waydroid-fix-network.sh, run as a third step from Start-Waydroid.bat
after the session is up. Notes:

- Runs as root: "waydroid shell" refuses to run as a normal user, so this
  cannot live in waydroid-start-user.sh.
- Reads the gateway from the waydroid0 bridge instead of hardcoding
  192.168.240.1; waydroid picks the subnet at init time.
- Retries for up to 60s, since Android may still be coming up, and exits 0
  if it cannot help so it never blocks the launcher.

Verified end to end: the launcher prints "Added Android default route via
192.168.240.1" and google.com resolves and pings from inside Android.
The previous version added the default route with `ip route add`, which does
not survive and does not help apps:

- Android uses fwmark policy routing. App traffic resolves in netd's
  per-network table (`eth0`), not `main`, so a route in `main` is the wrong
  table -- `ip route show table eth0` stayed empty.
- netd reconciles `main` and deletes routes it does not own, so the route
  disappeared within about a minute and internet died again.

Go through `ndc` instead. netd then owns the routes, they persist, and app
DNS starts working because the resolver is bound to the same network. Order
matters: netd rejects the gateway route with "addRoute() failed (Network is
unreachable)" until the connected subnet route exists in that table first.

The netd network id is read from Android's own routing rules
(fwmark 0x1<netid>/0x1ffff) rather than assuming the usual 100, and the
gateway and subnet come from the waydroid0 bridge rather than being
hardcoded.

Verified by tearing the routes back down and re-running the script:

    eth0 table now: []
    ping: connect: Network is unreachable
    -> Android network configured: default via 192.168.240.1 on netd network 100.
    default via 192.168.240.1 dev eth0 proto static
    192.168.240.0/24 dev eth0 proto static scope link
    google.com: 2 received, 0% packet loss, rtt 72.2ms

Apps recovered too -- Play's "Unable to resolve host play.googleapis.com"
errors stopped and Finsky/PlayCommon uploads began succeeding.

Known remaining gap: ConnectivityService still reports "Active default
network: none" because EthernetService never registers a NetworkAgent (the
framework's INetdEventListener never comes up, so EthernetTracker never
learns eth0 exists). Sockets and DNS work, so browsers are fine, but apps
that gate on ConnectivityManager -- Google Play among them -- still refuse.
Any app crash took down the whole Android framework. ActivityManager tries to
show its "app has stopped" dialog, adding that window fails in this compositor
setup, and the exception lands on system_server's android.ui thread:

    E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: android.ui
    java.lang.RuntimeException: Adding window failed
      at android.view.ViewRootImpl.setView(ViewRootImpl.java:1316)
      at android.app.Dialog.show(Dialog.java:352)
      at com.android.server.am.ErrorDialogController...

system_server dies, the framework restarts, and the network goes with it. The
symptom is not "Android keeps restarting" -- it is intermittent internet:
things work for a minute, then stop. Browsers survive longest because they
hold open sockets, while Google Play re-queries ConnectivityManager and
reports no connection.

`settings put global hide_error_dialogs 1` removes the code path. The value
lives in /data and persists; re-applying it is harmless.

Renames waydroid-fix-network.sh to waydroid-post-boot.sh, since it now covers
two unrelated post-boot repairs, and keeps both idempotent so the launcher can
run it on every start.

Before: system_server restarting repeatedly, "Active default network: none".
After: same pid across 47 minutes, "Active default network: 100", zero
FATAL EXCEPTION IN SYSTEM PROCESS, and Google Play loads search results.

README also documents the image-type switch trap found alongside this:
`waydroid init -s GAPPS -f` swaps the system image but keeps /data, and the
mismatched package UIDs break com.android.networkstack, which blocks
EthernetService in awaitIpClientStart() and leaves ConnectivityService with no
default network.
The nested compositor was launched at a fixed 1280x800, so Android came up
landscape and, at the image's stock 180 density, reported ~1138dp of width --
firmly in tablet-layout territory. Default to 720x1280 portrait instead.

Width and height are now named variables at the top of waydroid-start-user.sh,
and waydroid-post-boot.sh derives the display density from the actual width so
Android lands near a phone's 360dp. Changing the geometry no longer means
hand-tuning a density to match.

Also rewrites waydroid-post-boot.sh, which had two bugs of my own making:

- The density step ran before Android could answer, and the routing step
  exited early on success, so the density step was skipped entirely on any
  boot where routing was already fine. There is now one wait for Android up
  front and no early exits, so every step runs on every start.
- The idempotency check read "Physical density", but "wm density" reports a
  manual setting as a separate "Override density" line and leaves the physical
  value alone -- so the check never matched and the script re-applied the
  density on every run. It now reads the override when one is present.

Verified: after a reset, run 1 prints "Set display density to 320 so a 720px
screen reports ~360dp (phone layout)" and run 2 is a silent no-op. Final state
is 720x1248 (Weston keeps a few rows for its panel) at override density 320,
which is exactly 360dp wide.

Note Waydroid restarts the session when the compositor window is resized, so
the size is fixed at launch rather than tracked live; that is called out in a
comment next to the variables.
Records what actually went wrong on an end-to-end install, separated into
bugs in this installer and behavior of Waydroid/WSL itself. Most of these
fail silently or report an error that points somewhere unhelpful, so the
symptom is written down alongside the cause.

Installer bugs: the PowerShell array-literal concatenation that truncated the
kernel config command; CRLF breaking every shell payload sent into WSL (both
the heredoc terminator and the launcher shebangs); weston never being
installed; the Start Menu mitigation running before the files it moves exist;
and the launcher killing the compositor out from under a running session on a
second click.

Waydroid/WSL behavior: Android getting a DHCP lease but no gateway because
netd's BPF/xt_quota setup fails on the WSL kernel and EthernetService blocks
in awaitIpClientStart(), including why the route has to go through ndc rather
than "ip route add"; and app-error dialogs killing system_server, which
presents as intermittent internet rather than as a crash loop.

New troubleshooting entries for ARM-only apps on an x86_64 image, the ~24h
Play certification cache that a checkin does not refresh (and why clearing
com.google.android.gsf is the wrong way to force it), session restarts on
window resize, and the persist.waydroid.width/height properties that
crash-loop the hwcomposer.

Also adds a Display size section covering WIDTH/HEIGHT and the derived
density, a Known limitations section (single API level, x86_64 only, software
rendering, fixed window size, "wsl -t" not restarting WSLg), and refreshes
Package contents, which had drifted -- waydroid-post-boot.sh and
.gitattributes were missing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant