Skip to content

Commit 76ab0dd

Browse files
authored
Merge pull request #896 from mendixlabs/fix/890-chisel-linux-only
fix: ship the embedded tunnel in Linux builds only (closes #890)
2 parents 67fbf1a + b59b67b commit 76ab0dd

26 files changed

Lines changed: 893 additions & 192 deletions

.claude/skills/fix-issue.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,3 +515,4 @@ extracting `OffsetExpression`/`LimitExpression`.
515515
| `DESCRIBE PAGE` output will not re-parse for any page carrying a **pluggable widget**: `mxcli check` on it fails with `extraneous input ':'` / `extraneous input '('` from the first widget onward. Separately, a boolean property the author set is missing from the description entirely | Two independent defects in the same path. **Emit**: explicit properties were written with a raw `%s`, so every string lost its quotes — a JSON `spec: {"a": 1}` then broke the parse at its first brace. **Read**: `extractExplicitProperties` skipped any value of `"true"`/`"false"` as a "common default", so booleans never reached the output | `mdl/executor/cmd_pages_describe_output.go` (`explicitPropValue`, `isBareLiteral`), `mdl/executor/cmd_pages_describe_pluggable.go` (`buildPropertyValueTypeMap`, `extractExplicitProperties`), `mdl/executor/cmd_pages_describe.go` (`rawExplicitProp.ValueType`) | **Fixing one half alone is worse than the bug.** Quote without emitting booleans and the description re-parses cleanly while silently dropping a property — a wrong page that validates. Both halves ship together or neither. **Quote by the DECLARED type, never the value's shape**: `ValueType.Type` sits in the widget's `Type.ObjectType.PropertyTypes`, the same array `buildPropertyTypeKeyMap` already walks for `PropertyKey` and throws away; a String property holding `"30"` or `"true"` is indistinguishable from a number once it is a string in BSON, and must still come back quoted. Where no type is declared, fall back to the value's shape and quote anything not plainly numeric or boolean — quoting is the safe direction, since an unquoted arbitrary string may not parse at all. **The round trip is the test, not the output**: describe → `check` → `exec` → describe must be byte-identical and leave `mx check` at 0 errors. Tests `mdl/executor/cmd_pages_describe_pluggable_roundtrip_test.go`; example `mdl-examples/bug-tests/pluggable-describe-roundtrip.mdl`, verified end to end on 11.12.1. **Uncovered while verifying, NOT fixed**: giving a property in a conditionally shown group a non-default value writes a widget Mendix rejects with CE0463 — ProgressCircle's `showLabel: true` and `labelType: 'percentage'` both do it with no DESCRIBE involved, while the same widget's General-group properties take non-default values happily. Reported in mxcli-ledger FINDINGS #104 |
516516
| `DESCRIBE PAGE` output will not re-parse for any page carrying a **pluggable widget**: `mxcli check` on it fails with `extraneous input ':'` / `extraneous input '('` from the first widget onward. Separately, a boolean property the author set is missing from the description entirely | Two independent defects in the same path. **Emit**: explicit properties were written with a raw `%s`, so every string lost its quotes — a JSON `spec: {"a": 1}` then broke the parse at its first brace. **Read**: `extractExplicitProperties` skipped any value of `"true"`/`"false"` as a "common default", so booleans never reached the output | `mdl/executor/cmd_pages_describe_output.go` (`explicitPropValue`, `isBareLiteral`), `mdl/executor/cmd_pages_describe_pluggable.go` (`buildPropertyValueTypeMap`, `extractExplicitProperties`), `mdl/executor/cmd_pages_describe.go` (`rawExplicitProp.ValueType`) | **Fixing one half alone is worse than the bug.** Quote without emitting booleans and the description re-parses cleanly while silently dropping a property — a wrong page that validates. Both halves ship together or neither. **Quote by the DECLARED type, never the value's shape**: `ValueType.Type` sits in the widget's `Type.ObjectType.PropertyTypes`, the same array `buildPropertyTypeKeyMap` already walks for `PropertyKey` and throws away; a String property holding `"30"` or `"true"` is indistinguishable from a number once it is a string in BSON, and must still come back quoted. Where no type is declared, fall back to the value's shape and quote anything not plainly numeric or boolean — quoting is the safe direction, since an unquoted arbitrary string may not parse at all. **The round trip is the test, not the output**: describe → `check` → `exec` → describe must be byte-identical and leave `mx check` at 0 errors. Tests `mdl/executor/cmd_pages_describe_pluggable_roundtrip_test.go`; example `mdl-examples/bug-tests/pluggable-describe-roundtrip.mdl`, verified end to end on 11.12.1. Reported in mxcli-ledger FINDINGS #104 |
517517
| Authoring a pluggable widget property that lives in a **conditionally shown group** writes a widget Mendix rejects with **CE0463**, while the same widget's other properties take non-default values happily. On ProgressCircle both `showLabel: true` and `labelType: 'percentage'` do it; `showLabel: false` and General-group properties are clean. No DESCRIBE involved | Two gaps on the same axis. **Serialization**: #574 nulls the TextTemplate of a HIDDEN conditional property, but left a VISIBLE one null — and Mendix stores an empty `Forms$ClientTemplate` there. Null and empty are each invalid in the other's state, so nulling hidden ones was only half the rule. **Extraction**: the editorConfig reader did not understand a ternary's ELSE branch (`cond ? (…) : hidePropertiesIn([…])`), so the `showLabel` gate was never seen and `labelText` read as visible whenever `labelType` was `"text"` — its default | `mdl/backend/widgetobj/builder.go` (`ApplyVisibilityRules`, `bsonFieldIsNil`), `mdl/executor/editorconfig_extract.go` (`parseGuard` `:` case, `ternaryCondition`, `trailingExpr`) | **Fixing one gap alone inverts the bug rather than closing it** — filling visible templates without the missing gate made `showLabel: false` fail where `true` had, because mxcli still thought labelText was visible. Measured both ways round before and after; a fix that moves which case fails is not a fix. **Let Mendix say what the shape should be**: `mx update-widgets` on a COPY of the failing project reconciles the widget, and diffing that against mxcli's output named the single meaningful path (`Object/Properties[N]/Value/TextTemplate` null vs `Forms$ClientTemplate`) out of 969. **The good/bad control does the isolation for free**: authoring the same widget with the boolean both ways gave two documents differing in exactly ONE path, so no other candidate needed testing. **Only CONDITIONAL properties are filled** — Studio Pro's convention for an unset TextTemplate is not uniform (a DataGrid custom-content column stores null for `tooltip` and an empty template for `exportValue`, per `emptyClientTemplateRules`), so filling every unset one would trade this bug for its mirror image. **The extractor's preamble matters**: the ternary is preceded by a whole `switch`, and walking back past the `?` to the function start yields a fragment with an unbalanced `}` that parses to nothing and looks like "unsupported shape" — hence `trailingExpr`. Regression signal: the widgetdemo showcase applies with **0 CE0463** (its 4 CE1613 are a pre-existing attribute reference). Tests `mdl/backend/widgetobj/widget_visibility_test.go`, `mdl/executor/editorconfig_extract_test.go`; example `mdl-examples/bug-tests/pluggable-describe-roundtrip.mdl` now exercises the group. Reported in mxcli-ledger FINDINGS #104 follow-on |
518+
| Windows Defender flags the mxcli **Windows** release binary as `Trojan:Script/Sabsik.EN.A!ml`; enterprise EDR (Defender for Endpoint, CrowdStrike, SentinelOne) blocks it harder. Not the generic unsigned-Go-binary false positive of #185 | The binary genuinely embedded **chisel**, a dual-use tunnelling/pivoting tool (SSH over WebSocket), on every platform — although the tunnel only ever runs inside a Linux container. `run --hub` linked `chisel/client`, `tunnel-hub` linked `chisel/server`, so windows/darwin carried 32 packages incl. the whole `x/crypto/ssh` stack for a feature they cannot use | `cmd/mxcli/docker/tunnel_linux.go` + `tunnel_other.go` (client seam), `cmd/mxcli/tunnelhub/control_linux.go` + `control_other.go` (server seam), `scripts/check-tunnel-deps.sh` (guard) | **Never obfuscate, pack or rename to dodge the scanner** — attacker tradecraft, and it makes the binary less trustworthy, not more. **Code signing does not fix this class**: a signed binary containing chisel is still flagged behaviourally; signing only addresses #185's generic false positive. The fix is to stop shipping the capability where it is unused: one interface per seam, `_linux.go` impl + `!linux` stub, commands still registered everywhere but failing with an actionable message. **Prove absence three ways, and know that `go tool nm` is not one of them** — release ldflags `-s -w` strip the symbol table, so nm reports "no symbols" whether or not the code is linked and would give a false pass; use `go list -deps`, `go version -m`, and `strings` (nm only on a deliberately unstripped build). **Guard against the transitive path, not the name**: match the module list (`x/crypto/ssh`, `gorilla/websocket`, `armon/go-socks5`, `jpillora/*`) so re-entry without the word "chisel" still trips it, and assert a **positive control** (chisel IS in the linux graph) so the check cannot pass vacuously. Verified by re-adding the import and watching the guard fail on all four windows/darwin targets. Result: -13.5 MB (-14.7%) on windows+darwin, linux unchanged. See ADR-0009 |

.claude/skills/mendix/bootstrap-app.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ drop the `./` if it came pre-installed on `PATH`.
121121
8. **(Optional) browser preview from a cloud session:**
122122
`./mxcli run --hub https://hub.mxcli.org -p <AppName>.mpr`, and report the preview
123123
URL it prints. Needs `MXCLI_HUB_KEY` on the environment; without it, continue as a
124-
normal local run.
124+
normal local run. `--hub` ships in the **Linux** build only (a cloud session is a
125+
Linux container, so it works there); on a native Windows/macOS mxcli it fails with
126+
an explanatory message — continue as a normal local run.
125127

126128
---
127129

.claude/skills/mendix/run-local.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,18 @@ which makes it look like a change you just made broke authentication.
322322
323323
## External browser preview (`--hub`)
324324
325+
> **Linux builds only.** `--hub` and `mxcli tunnel-hub` ship in the **Linux** build
326+
> only. The tunnel embeds a general-purpose tunnelling tool that gets the Windows
327+
> and macOS binaries flagged by Defender and enterprise EDR for a capability they
328+
> can never use, so it is left out of them. On Windows/macOS the commands exist and
329+
> show help, but fail with an explanatory message — run mxcli inside the project's
330+
> devcontainer (where the warm loop already runs) to use `--hub`. See
331+
> [ADR-0009](https://github.com/mendixlabs/mxcli/blob/main/docs/13-decisions/0009-tunnel-is-linux-only.md).
332+
325333
`--hub <url>` exposes the running app in a **browser at a public URL** without the app
326334
leaving this machine and without committing — for reviewing work-in-progress from a
327335
phone/tablet, or from an egress-only environment like Claude Code on the web. The app
328-
stays here; a **chisel reverse tunnel** dials *out* to a hub over 443 and the hub proxies
336+
stays here; a **reverse tunnel** dials *out* to a hub over 443 and the hub proxies
329337
browser requests back down it. Nothing is pushed — only live HTTP — and everything rides
330338
one 443 connection, so it works through an egress-only proxy.
331339

.github/workflows/push-test.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,51 @@ permissions:
66
contents: read
77

88
jobs:
9+
# The tunnel seam has a !linux half (stub + its tests) that the ubuntu job can
10+
# only compile, never run. This job actually executes it on real Windows and
11+
# macOS runners, so "--hub fails with an actionable message" is a tested claim
12+
# rather than a cross-compile that type-checked. See ADR-0009.
13+
#
14+
# Neither package depends on the generated ANTLR parser, so this needs no
15+
# grammar step and stays fast.
16+
tunnel-seam-cross-platform:
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
os: [windows-latest, macos-latest]
21+
runs-on: ${{ matrix.os }}
22+
steps:
23+
- uses: actions/checkout@v7
24+
- uses: actions/setup-go@v7
25+
with:
26+
go-version: '1.26.6'
27+
- name: Test the tunnel seam
28+
shell: bash
29+
# Scoped with -run to the seam's own tests. The full test binaries are still
30+
# COMPILED for this platform, so a Windows/macOS build break is still caught;
31+
# only the !linux stub behaviour is executed.
32+
#
33+
# Running the whole packages here fails on Windows for reasons that predate
34+
# this change and are unrelated to the tunnel: several tests assert POSIX file
35+
# modes (0600) that Windows does not implement — os.Chmod only toggles the
36+
# read-only bit, so Stat reports 666 — plus one path-separator assumption.
37+
# Tracked separately in #897; widening this job is that issue's job, not this
38+
# one's.
39+
#
40+
# -run can pass vacuously if the tests are renamed or deleted, so assert that
41+
# the expected number actually ran.
42+
run: |
43+
out=$(go test -v -count=1 -run 'Unsupported' ./cmd/mxcli/docker/... ./cmd/mxcli/tunnelhub/...)
44+
echo "$out"
45+
n=$(printf '%s\n' "$out" | grep -c '^--- PASS: Test.*Unsupported' || true)
46+
echo "seam tests executed: $n"
47+
if [ "$n" -lt 4 ]; then
48+
echo "FAIL: expected at least 4 tunnel-seam tests to run, -run matched $n."
49+
echo " The !linux stubs in cmd/mxcli/docker and cmd/mxcli/tunnelhub"
50+
echo " must each keep a test whose name contains 'Unsupported'."
51+
exit 1
52+
fi
53+
954
build-and-test:
1055
runs-on: ubuntu-latest
1156
steps:
@@ -28,6 +73,13 @@ jobs:
2873
run: make build
2974
- name: Test
3075
run: make test
76+
- name: Check tunnel stays Linux-only
77+
# The embedded tunnel (chisel) must never reach the Windows/macOS builds —
78+
# it gets mxcli flagged by Defender and enterprise EDR on managed corporate
79+
# endpoints, which is most of our audience. See ADR-0009. The script also
80+
# asserts a positive control (chisel IS in the linux graph) so it cannot
81+
# pass vacuously.
82+
run: ./scripts/check-tunnel-deps.sh
3183
- name: Check MDL example scripts
3284
# Single source of truth: `make check-mdl` covers BOTH doctype-tests/ and
3385
# bug-tests/ (skipping *.test.mdl, inverting *.fail.mdl negative tests, and

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
### Changed
1010

11+
- **The embedded tunnel now ships in the Linux build only** (`mxcli run --hub`, `mxcli tunnel-hub`). The tunnel embeds [chisel](https://github.com/jpillora/chisel), a dual-use tunnelling tool that appears in threat intelligence as a pivoting component. It only ever runs inside a Linux container, but every platform linked it — so Microsoft Defender flagged the Windows binary (`Trojan:Script/Sabsik.EN.A!ml`) and enterprise EDR flags this class of payload harder still, blocking mxcli on the managed corporate endpoints most Mendix developers use. Both chisel imports now sit behind a one-interface, Linux-only seam; a CI guard (`scripts/check-tunnel-deps.sh`, `make check-tunnel-deps`) fails the build if chisel or its SSH/websocket/socks dependencies reappear in a windows/darwin dependency graph. **Windows and macOS release binaries are 13.5 MB smaller (-14.7%)** and contain no tunnelling code. The Linux build is unchanged. On other platforms the two commands remain registered and documented but fail with an actionable message. Note that this is a *different* problem from the `Wacatac.C!ml` report in [#185](https://github.com/mendixlabs/mxcli/issues/185), which was a genuine generic Go-binary false positive: here the capability really was in the binary, and code signing would not have addressed it. We did not obfuscate or repack anything — the fix is not shipping the capability where it is unused. ([#890](https://github.com/mendixlabs/mxcli/issues/890), [ADR-0009](docs/13-decisions/0009-tunnel-is-linux-only.md))
12+
- **Breaking, narrow:** `mxcli tunnel-hub` can no longer be hosted on Windows or macOS — move the hub to a Linux host. Developers on native Windows/macOS installs must run mxcli inside the project's devcontainer to use `--hub`.
13+
- `tunnelhub.ServerOptions.ChiselAddr` is renamed to `ControlAddr` (it addresses the platform-agnostic control server).
14+
1115
- **Go toolchain 1.26.5 → 1.26.6** for GO-2026-6218 (`net/url`), GO-2026-6090 (`crypto/tls`), GO-2026-6089 (`net/http`), GO-2026-6088 (`encoding/xml`), GO-2026-5972 (`encoding/asn1`) and GO-2026-5026 (`net/http`, via `golang.org/x/net/idna`). All six are standard-library advisories fixed in go1.26.6; no mxcli code changed. Bumped in `go.mod` and in all three workflows (`push-test`, `release`, `nightly`) together, so released binaries are not still linked against the vulnerable standard library.
1216

1317
## [0.17.0] - 2026-08-10

CLAUDE.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,38 @@ disable identity preservation. **Any test asserting "nothing changed" must inclu
363363
the control run with it set** — otherwise the test passes against a build that
364364
never had the fix, which is exactly how PR #125 shipped green.
365365

366+
### The Tunnel Is Linux-Only, On Purpose — Do Not "Restore" It
367+
368+
`mxcli run --hub` and `mxcli tunnel-hub` embed [chisel](https://github.com/jpillora/chisel),
369+
a dual-use tunnelling tool that appears in threat intelligence as a pivoting
370+
component. Shipping it in the Windows and macOS binaries — where the tunnel can
371+
never run — got them flagged by Defender (`Trojan:Script/Sabsik.EN.A!ml`) and
372+
denied by enterprise EDR, which blocks mxcli for corporate Mendix developers on
373+
managed endpoints. It is now built **for Linux only**. See
374+
[ADR-0009](docs/13-decisions/0009-tunnel-is-linux-only.md).
375+
376+
This looks like a portability gap and is not one. Making the tunnel cross-platform
377+
again re-introduces the detection for the large majority of downloads.
378+
379+
- **All chisel imports live behind two seams**, one interface each:
380+
`tunnelConn` / `startTunnel` (`cmd/mxcli/docker/tunnel_linux.go` + `tunnel_other.go`)
381+
and `controlServer` / `newControlServer` (`cmd/mxcli/tunnelhub/control_linux.go`
382+
+ `control_other.go`). Adding a chisel import anywhere else is the mistake the
383+
guard exists to catch.
384+
- **`scripts/check-tunnel-deps.sh` (CI, and `make check-tunnel-deps`) fails the
385+
build** if chisel or its tunnelling-specific dependencies — the SSH/websocket/
386+
socks stack included, which is how it would come back without the word "chisel"
387+
appearing — reach a windows/darwin dependency graph. It asserts a positive
388+
control first (chisel *is* in the linux graph), so it cannot pass vacuously.
389+
- **The hub seam is at `Start`, not construction**, so the portable front
390+
(registry, API, auth, routing) stays testable on every platform.
391+
- **Never obfuscate, pack, or rename to evade detection.** That is attacker
392+
tradecraft and makes things strictly worse. The only legitimate fix is not
393+
shipping the capability where it is unused. Code signing does **not** substitute:
394+
a signed binary containing chisel is still flagged behaviourally.
395+
- Do not conflate this with #185 (`Wacatac.C!ml`), which was a genuine generic
396+
Go-binary false positive with a different remedy.
397+
366398
### Theme Files: Where SCSS Actually Compiles
367399

368400
Styling written to the wrong place fails **silently** — the build succeeds and the

0 commit comments

Comments
 (0)