Skip to content

fix(install): reject unsupported Android portable installs - #2245

Merged
DeusData merged 6 commits into
DeusData:mainfrom
lorenzozanee:fix/termux-portable-guard
Nov 2, 2026
Merged

DeusData merged 6 commits into
DeusData:mainfrom
lorenzozanee:fix/termux-portable-guard

Conversation

@lorenzozanee

@lorenzozanee lorenzozanee commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

What does PR do?

On Android/Termux, install.sh previously selected and downloaded an incompatible Linux portable binary. Detect Android/Termux, report the supported alternatives, and stop before download. Add a regression test that verifies the downloader is not invoked.

Refs #1202

Checklist

  • Every commit signed off (git commit -s) — required, CI rejects unsigned commits (DCO, see CONTRIBUTING.md)
  • Tests pass locally (make -f Makefile.cbm test)
  • Lint passes (make -f Makefile.cbm lint-ci)
  • New behavior is covered by test (reproduce-first bug fixes)

Signed-off-by: lorenzozanee <wyz0707@proton.me>
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you — this is the right fix and the right size. On Termux the installer currently downloads the portable asset, which then dies with "Bad system call" — it is statically linked glibc, and it makes system calls Android's seccomp filter rejects; stopping before the download, with a message that says why, is exactly what should happen. Detecting on three independent signals (uname -o, TERMUX_VERSION, the $PREFIX path) is thorough, guarding it behind OS = linux keeps every other platform untouched, and the test is nicely built — stubbing curl to exit 99 proves "rejected before download" rather than merely "rejected".

Three small things, and then I would like to merge it:

1. The test will never run as it stands. scripts/test.sh does not glob the tests/ directory — it lists each shell contract test explicitly (bash "$ROOT/tests/test_build_dir_safety.sh" and so on, around lines 200–270), and every one of them is named test_*.sh with underscores. tests/test-install-android-guard.sh is neither listed nor named to the convention, so no lane would ever execute it, and a guard nobody runs protects nothing. Please rename it to tests/test_install_android_guard.sh and add one line for it next to the other contract tests in scripts/test.sh. (It would be the first installer test in that list — a good precedent.)

2. Fixes #1202 → Refs #1202, please. #1202 is much wider than this: it also covers the gaps in building from source under Termux and an ENOENT on the temporary database that is still unexplained. Merging with Fixes would auto-close all of that. This PR resolves the first failure mode in that issue — cleanly — and should say so without closing the rest.

3. One sentence in the error message promises slightly more than we can. "Build from source with scripts/build.sh in Termux" reads as a supported path; the reporter on #1202 got it mostly working, but with open problems, and Termux support is an undecided question here. Something like

error: Android/Termux cannot run the Linux portable binary (static glibc; Android kills it with "Bad system call").
Building from source in Termux may work but is not a supported platform yet - see issue #1202.

tells the user the truth and gives them the thread where the workarounds are collected. Your test greps for Android/Termux, so it keeps passing.

Two things that are ours, not yours:

  • The red test-msan you saw earlier was daemon_bootstrap_fails_fast_when_daemon_dies_at_publication — a daemon test that has nothing to do with install.sh. I re-ran it on the identical commit and it passed, which makes it a nondeterministic test on our side; it is recorded as such and will not be held against this PR.
  • lint / lint is currently red on main itself for an unrelated reason, and a fix is in flight (#2257). If you rebase after that lands you will see a clean run.

A note on process, since you are new here: force-pushing your own PR branch is completely fine, and keep the Signed-off-by on any amended commit. Thanks again — small, careful fixes to the installer are some of the most valuable contributions this project gets, because that script is the first thing every new user runs.

DeusData added a commit to htarnacki/codebase-memory-mcp that referenced this pull request Sep 25, 2026
… its verdict

daemon_bootstrap_fails_fast_when_daemon_dies_at_publication went red on
the MSan lane of three PRs that touch no daemon code at all (DeusData#2245,
DeusData#2158, DeusData#2140) with

    tests/test_daemon_bootstrap.c:1116: ASSERT(daemon_named_cause)

and green on a rerun of the identical commit.

Cause, attributed 2026-09-20. The test forks a real daemon host whose
record publication fails with ENOSPC, and asserts that the client names
that cause instead of waiting out its 30 s startup deadline. But before
it listens, the host SHA-256s its own executable image, and the test
runner is ~450 MB: ~3 s natively, 20-32 s under MSan (48 CI samples,
median 25.8 s). The spawn override returned as soon as it had forked, so
the client's 30 s clock raced the host's hash. On a slow run the
deadline fired first, the reaper SIGKILLed a host that had not failed
yet, and the log had no listen_failed line to find. Nothing was wrong
with the product: with a 28 s artificial delay the client does learn the
recorded ENOSPC cause.

The spawn override now reaps the forked host before it returns. The
host writes its start-failure record and only then releases its lifetime
reservation and exits, so "record on disk, reservation released" is a
stable state pinned by construction when the client takes its first look
-- and that first look is guaranteed: the client's post-spawn wait loop
runs at least once even past the deadline, and its first failure check
after a spawn is unthrottled. How long the host takes no longer matters.
No production code changes; the assertions are unchanged.

A regression to the pre-DeusData#1828 behaviour (a client that ignores the
record) still fails the unchanged assertions: its message no longer
names the failure, the errno and the path.

Proof, one build with temporary env-switched instrumentation that is
not part of this commit (a sleep in the forked host before it starts,
and a switch back to the old fire-and-forget spawn):

  spawn          forked host delayed   daemon_bootstrap suite
  synchronous    no                    28 passed
  old (async)    31 s                  27 passed, 1 failed:
                                       ASSERT(log_read)
  synchronous    31 s                  28 passed

The old spawn fails under the delay one assertion earlier than CI did
(log_read, not daemon_named_cause): the injected sleep sits before the
host opens its log, whereas in CI the host was killed mid-hash with the
log already open. Same mechanism -- the client's deadline beating the
host -- and the 2026-09-20 attribution reproduced the CI message itself
with a delay placed after the log is opened.

The final tree, without the instrumentation: daemon_bootstrap 28 passed,
three runs in a row.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
daidaiJ pushed a commit to daidaiJ/codebase-memory-mcp that referenced this pull request Sep 29, 2026
… its verdict

daemon_bootstrap_fails_fast_when_daemon_dies_at_publication went red on
the MSan lane of three PRs that touch no daemon code at all (DeusData#2245,
DeusData#2158, DeusData#2140) with

    tests/test_daemon_bootstrap.c:1116: ASSERT(daemon_named_cause)

and green on a rerun of the identical commit.

Cause, attributed 2026-09-20. The test forks a real daemon host whose
record publication fails with ENOSPC, and asserts that the client names
that cause instead of waiting out its 30 s startup deadline. But before
it listens, the host SHA-256s its own executable image, and the test
runner is ~450 MB: ~3 s natively, 20-32 s under MSan (48 CI samples,
median 25.8 s). The spawn override returned as soon as it had forked, so
the client's 30 s clock raced the host's hash. On a slow run the
deadline fired first, the reaper SIGKILLed a host that had not failed
yet, and the log had no listen_failed line to find. Nothing was wrong
with the product: with a 28 s artificial delay the client does learn the
recorded ENOSPC cause.

The spawn override now reaps the forked host before it returns. The
host writes its start-failure record and only then releases its lifetime
reservation and exits, so "record on disk, reservation released" is a
stable state pinned by construction when the client takes its first look
-- and that first look is guaranteed: the client's post-spawn wait loop
runs at least once even past the deadline, and its first failure check
after a spawn is unthrottled. How long the host takes no longer matters.
No production code changes; the assertions are unchanged.

A regression to the pre-DeusData#1828 behaviour (a client that ignores the
record) still fails the unchanged assertions: its message no longer
names the failure, the errno and the path.

Proof, one build with temporary env-switched instrumentation that is
not part of this commit (a sleep in the forked host before it starts,
and a switch back to the old fire-and-forget spawn):

  spawn          forked host delayed   daemon_bootstrap suite
  synchronous    no                    28 passed
  old (async)    31 s                  27 passed, 1 failed:
                                       ASSERT(log_read)
  synchronous    31 s                  28 passed

The old spawn fails under the delay one assertion earlier than CI did
(log_read, not daemon_named_cause): the injected sleep sits before the
host opens its log, whereas in CI the host was killed mid-hash with the
log already open. Same mechanism -- the client's deadline beating the
host -- and the 2026-09-20 attribution reproduced the CI message itself
with a delay placed after the log is opened.

The final tree, without the instrumentation: daemon_bootstrap 28 passed,
three runs in a row.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
daidaiJ pushed a commit to daidaiJ/codebase-memory-mcp that referenced this pull request Sep 29, 2026
… its verdict

daemon_bootstrap_fails_fast_when_daemon_dies_at_publication went red on
the MSan lane of three PRs that touch no daemon code at all (DeusData#2245,
DeusData#2158, DeusData#2140) with

    tests/test_daemon_bootstrap.c:1116: ASSERT(daemon_named_cause)

and green on a rerun of the identical commit.

Cause, attributed 2026-09-20. The test forks a real daemon host whose
record publication fails with ENOSPC, and asserts that the client names
that cause instead of waiting out its 30 s startup deadline. But before
it listens, the host SHA-256s its own executable image, and the test
runner is ~450 MB: ~3 s natively, 20-32 s under MSan (48 CI samples,
median 25.8 s). The spawn override returned as soon as it had forked, so
the client's 30 s clock raced the host's hash. On a slow run the
deadline fired first, the reaper SIGKILLed a host that had not failed
yet, and the log had no listen_failed line to find. Nothing was wrong
with the product: with a 28 s artificial delay the client does learn the
recorded ENOSPC cause.

The spawn override now reaps the forked host before it returns. The
host writes its start-failure record and only then releases its lifetime
reservation and exits, so "record on disk, reservation released" is a
stable state pinned by construction when the client takes its first look
-- and that first look is guaranteed: the client's post-spawn wait loop
runs at least once even past the deadline, and its first failure check
after a spawn is unthrottled. How long the host takes no longer matters.
No production code changes; the assertions are unchanged.

A regression to the pre-DeusData#1828 behaviour (a client that ignores the
record) still fails the unchanged assertions: its message no longer
names the failure, the errno and the path.

Proof, one build with temporary env-switched instrumentation that is
not part of this commit (a sleep in the forked host before it starts,
and a switch back to the old fire-and-forget spawn):

  spawn          forked host delayed   daemon_bootstrap suite
  synchronous    no                    28 passed
  old (async)    31 s                  27 passed, 1 failed:
                                       ASSERT(log_read)
  synchronous    31 s                  28 passed

The old spawn fails under the delay one assertion earlier than CI did
(log_read, not daemon_named_cause): the injected sleep sits before the
host opens its log, whereas in CI the host was killed mid-hash with the
log already open. Same mechanism -- the client's deadline beating the
host -- and the 2026-09-20 attribution reproduced the CI message itself
with a delay placed after the log is opened.

The final tree, without the instrumentation: daemon_bootstrap 28 passed,
three runs in a row.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData added bug Something isn't working editor/integration Editor compatibility and CLI integration ux/behavior Display bugs, docs, adoption UX priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Oct 1, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thanks again for the focused Android guard. Following up on the existing review: the test needs registration in the shell runner, the issue link should avoid auto-closing the broader #1202, and the error text should describe Termux source builds as unsupported. Those are the outstanding requested changes in this thread.

@DeusData

Copy link
Copy Markdown
Owner

Thanks again for the Android install guard. The recorded next revision still needs shell-runner registration, a non-closing link to the broader #1202, and error wording that calls Termux source builds unsupported. Please let us know when that is ready.

Every shell contract test under tests/ is named test_*.sh with
underscores. A pure rename, so history and blame follow the file.

Refs DeusData#1202

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
scripts/test.sh lists every shell contract test explicitly and does
not glob tests/, so an unlisted test never ran on any leg. Register
tests/test_install_android_guard.sh as Step 0x2, right after Step 0x,
in the same two-line shape as its neighbours.

Give the two PATH stubs (uname, curl) the same shebang every other
stub under tests/ uses, so they are proper executables on every leg
instead of relying on the shell fallback for files without one.

Refs DeusData#1202

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The guard told users to build from source with scripts/build.sh in
Termux, which reads as a supported path. Source builds under Termux
still have open problems tracked in DeusData#1202, so the message now says
why the portable binary cannot run there and that a source build may
work but is not supported yet, pointing at DeusData#1202.

The test still matches Android/Termux in the new message.

Refs DeusData#1202

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Brings the branch up to current main (it was 2061 commits behind) so
the new Step 0x2 runs inside the current scripts/test.sh. No conflicts.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Point CBM_DOWNLOAD_URL, the documented download-base override in
install.sh, at a closed loopback port. If a curl stub were ever missed
on some leg, the download would then fail locally instead of fetching
a real release.

Check that the uname stub is the one PATH resolves before running
install.sh. Otherwise the test would quietly exercise the real host
instead of an Android one and prove nothing.

Refs DeusData#1202

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

DeusData commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Thank you, @lorenzozanee - this is exactly the right fix for the first failure in #1202, and it was right in substance from the start. Stopping before the download, instead of letting Termux users fetch a binary that Android then kills with "Bad system call", is the behaviour we want. Detecting on three independent signals (uname -o, TERMUX_VERSION and the Termux PREFIX path) makes it robust, keeping it behind OS = linux leaves every other platform untouched, and stubbing curl to exit 99 is what lets the test prove "rejected before download" rather than just "rejected".

You allowed maintainer edits, so we finished the small points from our review ourselves and you do not need to do anything more. What each commit on top of yours does:

  1. 17415f6 test(install): rename the Android guard test - a pure rename to tests/test_install_android_guard.sh, so it follows the test_*.sh naming of every other shell contract test. History and blame follow the file.
  2. 1fc70c2 test(install): run the Android guard test as Step 0x2 - scripts/test.sh lists each contract test explicitly instead of globbing tests/, so until now the test never ran. It now runs as Step 0x2, right after Step 0x, on every leg. Your two PATH stubs (uname and curl) also get the #!/usr/bin/env bash line every other stub under tests/ has, so they are real executables everywhere instead of relying on the shell fallback for files without one.
  3. 8b2b0d7 fix(install): say that Termux is not a supported platform yet - the message now explains why the portable binary cannot run on Android and says that a source build may work but is not supported yet, pointing at Termux/Portable Linux 64 Execution Issues + How It Sort Of Works Now #1202, instead of recommending scripts/build.sh. Your test still matches "Android/Termux", so it passes unchanged.
  4. 79f674e Merge origin/main - the branch was far behind main; the merge had no conflicts and lets the new step run inside the current scripts/test.sh.
  5. 7a7ecb7 test(install): keep the Android guard test off the network - the test now points CBM_DOWNLOAD_URL at a closed local port and first checks that your uname stub is the one PATH finds, so even if a stub were ever missed on some platform, the test fails on the spot instead of reaching the network or quietly testing the real host.

We also changed "Fixes #1202" to "Refs #1202" in the description: #1202 also covers source builds under Termux, which stay open, and this PR cleanly resolves the first failure mode in it.

We checked it locally: the test passes, and with the guard disabled it fails at Step 0x2 because the installer reaches the (stubbed) download.

If you push to this branch again, please pull first, since it now carries these commits on top of yours. Thanks again - the installer is the first thing every new user runs, so careful fixes like this one matter a lot.

@DeusData
DeusData merged commit 9c19b5d into DeusData:main Nov 2, 2026
41 checks passed
@DeusData

DeusData commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Merged in 9c19b5d. Thank you again, @lorenzozanee! Termux users now get a clear message before anything is downloaded, instead of a binary that Android kills with "Bad system call", and the guard test runs on every leg as Step 0x2. #1202 stays open for source builds under Termux, which this PR cleanly separates out. Much appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working editor/integration Editor compatibility and CLI integration priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. ux/behavior Display bugs, docs, adoption UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants