Skip to content

Fix nine reported issues and defects found while auditing around them - #2303

Open
MOMO0302-02 wants to merge 9 commits into
chen08209:mainfrom
MOMO0302-02:fix/reported-issues
Open

Fix nine reported issues and defects found while auditing around them#2303
MOMO0302-02 wants to merge 9 commits into
chen08209:mainfrom
MOMO0302-02:fix/reported-issues

Conversation

@MOMO0302-02

@MOMO0302-02 MOMO0302-02 commented Aug 13, 2026

Copy link
Copy Markdown

Updated for the current main. The nine commits merged since this branch opened rewrote the Android service layer, the desktop core transport and the core message layer. Forty files conflicted; rather than replaying the fixes I re-checked each one against the new code and dropped every fix whose defect main has since resolved — see Already fixed on main below. The branch is now 85 files, +1496/−302 against main, down from 107.

This branch collects fixes for reported issues plus a set of defects found while auditing the surrounding code. It is deliberately submitted as one branch so the history stays reviewable; happy to split it into per-issue pull requests on request.

Please read the Verification and Behaviour changes sections before merging.

Already fixed on main — dropped from this branch

These were part of the original submission and are no longer here, because main now fixes the same defect, either independently or by removing the code that carried it. Listed so you do not go looking for them:

Reported issues still addressed here

Issue Cause
#2241 The export dialog was given the profile label, which never carries an extension.
#2242 flutter_js exports its prebuilt QuickJS bridge under a misspelled variable (flutter_qjs_bundled_libraries), so Flutter's plugin bundling never picks it up. Bundled explicitly in linux/CMakeLists.txt.
#2215 (partial) The app's geo auto-update setting is still never written into the generated config, so the subscription's value wins. The geox-url key half of this issue is already fixed on main.
#2286 Windows hard-codes checkPermission to "granted" while the WLAN API returns ERROR_ACCESS_DENIED, so exclude-SSID rules are inert with no hint. It also forwards raw 802.11 SSID octets, which the standard codec cannot decode when they are not valid UTF-8.
#2276 Windows sends WM_QUERYENDSESSION/WM_ENDSESSION at shutdown, not WM_CLOSE, so the Dart quit path never runs and the per-user WinINET proxy setting survives the reboot.

Other defects found while auditing

Security

  • Backup restore accepts archive entries with ../, an absolute path, or a drive qualifier (zip slip). A crafted or MITM'd backup could write anywhere the user can write, and a drive-qualified entry aborts the whole restore instead of skipping one file.
  • badCertificateCallback returns true for every host, so subscription downloads, WebDAV backups and update checks accept any forged certificate. Now restricted to loopback. See Behaviour changes.

Data loss and corruption

  • Rule.parse splits on commas with no awareness of logic rules, drops any segment merely containing src/no-resolve, aliases a two-segment rule's payload to its target, and throws a RangeError on single-segment input. Corrupted rules are then persisted by the v1→v2 migration and by quick-fill. Rewritten with a bracket-aware split and exact trailing-parameter matching; test/models/rule_test.dart added.
  • Restore with the default compatible strategy hard-replaces scripts, rules, links and proxy groups, silently deleting local entries the backup does not contain. Now merged, with order keys allocated after the existing ones so the two sequences cannot collide.
  • SQLite foreign keys are off by default and beforeOpen does not enable them, so the declared cascades never fire; profile dependents are now deleted explicitly.
  • excludeSSIDs is a top-level Config field and is dropped on restore.
  • Side-loading a provider overwrites the file on disk before the core validates it.
  • A new proxy group's generated order key is applied in memory but never persisted, so the row keeps a null order and later reordering computes keys from a null neighbour.

Crashes, hangs, leaks

  • A range error in the proxy list view when a search filter is active: the offsets are built from the filtered groups but indexed with a position taken from the unfiltered ones.
  • A FormatException in the port dialog after collapsing an emptied field — collapsed fields leave the form, so their validators never run.
  • The connections view writes to its ValueNotifier after dispose has released it.
  • The JavaScript runtime is never disposed.
  • The delete animation in SuperGrid captures a slot index, so a card deleted while the list changes removes the wrong one; and didUpdateWidget resets the controller mid-animation, so onDelete never fires at all.
  • eventListener is read from core goroutines while the platform thread replaces and releases it — a data race over a JNI global reference.

UI and logic

  • Access-control search is case-sensitive, so typing WeChat matches nothing.
  • The custom-rule sheet's delete button is empty and its no-resolve/src switches have no handler.
  • Renaming a proxy group to an existing name is accepted, producing duplicates the core rejects.
  • Deleting a group matches on the live-edited name, so it deletes nothing — or the wrong group.
  • The profile preview encodes an already-encoded YAML document.
  • Declining the disclaimer still records it as accepted.
  • Dismissing an options dialog writes null back, which resets the app language to "follow system"; the radios are toggleable, so re-tapping the current option does the same.
  • New profiles are inserted at the top of the list, then jump to the bottom on the next stream emission.
  • The tray "copy environment variable" command is invalid in both cmd and PowerShell.
  • An offline update check reports "already up to date".
  • A geo update failure announces a successful update first, then the error.
  • A desktop core crash leaves the app in the started state, so the system proxy stays pointed at a dead port.
  • Quitting skips the debounced config flush that the minimize path performs, losing whatever changed in the last debounce window.

Platform

  • The Android build is broken on Windows: run_build_tool.sh writes its own location into the generated runner pubspec, but under Git Bash pwd yields a POSIX path the native Dart SDK cannot resolve, so the build fails with "could not find package build_tool".
  • log.Errorln takes a format string, so log.Errorln("TUN:", err) renders the reason through fmt's surplus-operand marker as TUN:%!(EXTRA *fmt.wrapError=...). The reason was always there, just wrapped in that marker — this is a readability and go vet fix, not a functional one, and it resolves no reported TUN failure.
  • macOS applicationShouldTerminate always returns .terminateCancel, which aborts system logout and shutdown.
  • macOS stopProxy gives up on the first failing networksetup call, leaving the remaining services proxied; the default network service name is truncated at the first space, breaking Thunderbolt Bridge and similar.
  • checkIsAdmin shell-escapes an argv element, so any install path containing a space always re-prompts for credentials.
  • Linux: g_variant_iter_loop values leak on early exit, and the connman branch uses strings after the iterator has released them.

Verification

Re-run in full after the merge, against Flutter 3.44.9 / Dart 3.12.2:

  • flutter analyzeno issues.
  • flutter test740 of 741 pass. The one failure, task_test's makeRealProfileTask normalizes runtime config and added rules, fails identically on a pristine origin/main worktree under Windows: it asserts a POSIX prefix (/profiles/providers/7/proxies/) against a path built with package:path's platform-aware join. It is untouched by this branch, and it passes on your Linux CI. Happy to make that assertion platform-agnostic in a separate pull request if you want it.
  • plugins/proxy and plugins/wifi_ssid analysed and tested separately, since the root analysis_options.yaml excludes plugins/** — clean, 21 and 5 tests respectively.
  • go build ./... and go vet ./... pass for the core, both in the default build and cross-compiled for android/arm64 with CGO_ENABLED=1 through the NDK, which is what brings lib.go, bride.go, tun/ and platform/ into analysis at all.

Not compiled: the macOS Swift change needs a macOS host, the Linux C change needs a Linux one, and the Windows C++ changes need MSVC — none of which I have, so all three were reviewed by hand rather than built. build.yaml only triggers on v* tags, so opening this pull request runs no automated build either. Please treat those three with corresponding scepticism, and tell me if you would rather I drop them from this branch.

There are no longer any Kotlin or Rust changes in this branch, so the APK build and cargo check that the earlier revision relied on are no longer load-bearing.

Behaviour changes worth calling out

  1. TLS verification. Certificates are now verified for every non-loopback host, so a self-signed or private-CA WebDAV server or subscription URL fails where it previously succeeded. An Allow insecure certificates switch was added under Application settings, off by default, for users who need those servers back — so the default is safe and the escape hatch is explicit. Translations are included for all four bundled locales.
  2. Geo auto-update now follows the app setting instead of the subscription's value.
  3. Options dialogs deliver their result through a callback instead of the pop value, so a dismissal is no longer indistinguishable from picking null. Tapping the glyph of an already-selected radio now closes without changing anything; picking the null option is still reachable by tapping its row.

MOMO0302-02 and others added 8 commits August 13, 2026 02:13
…code

Reported issues
- Additional rules were emitted in reverse UI order, inverting their
  priority so they appeared to have no effect (chen08209#2256, chen08209#2251, chen08209#2255).
- Exported profiles had no .yaml extension (chen08209#2241).
- The run-time counter was clamped at 99:59:59 and could render
  impossible values such as 99:99:59 (chen08209#2214, chen08209#2191).
- The quick-settings tile showed the inverse state: start failures and
  failed binds were reported as the opposite of reality (chen08209#2260).
- Script-mode overwrites failed on Linux because the QuickJS bridge was
  never bundled (chen08209#2242).
- Reading the Wi-Fi SSID could block startup on macOS and silently failed
  on Windows, leaving exclude-SSID rules inert with no hint (chen08209#2188, chen08209#2286).
- Custom geox-url was dropped (wrong yaml keys) and geo auto-update ran
  even when disabled (chen08209#2215).
- Auto-start enabled the system proxy before the core was ready and never
  rolled back when setup failed (chen08209#2239).
- Shutting Windows down left a stale 127.0.0.1 system proxy behind (chen08209#2276).

Security
- Reject backup archive entries that escape the restore directory.
- Verify TLS certificates for non-loopback hosts. Note: self-signed
  WebDAV servers and subscriptions now fail instead of being accepted
  silently.

Correctness and data safety
- Rewrite rule parsing: logic rules, payloads containing "src" or
  "no-resolve", two-segment rules and single-segment input were corrupted.
- Restore with the default compatible strategy no longer replaces local
  scripts, rules, links and proxy groups; merged rows get order keys
  allocated after the existing ones.
- Delete profile dependents explicitly, since SQLite foreign keys are off.
- Preserve excludeSSIDs on restore and the WebDAV file name when editing
  credentials.
- Validate side-loaded providers before overwriting the file on disk.
- Fix connection list rendering only half its rows, case-sensitive app
  search, inverted permission and quick-fill conditions, dead delete
  button and inert rule switches, duplicate proxy group names, and the
  double-encoded profile preview.

Crashes, hangs and leaks
- Core: return owned strings from getTraffic/getTotalTraffic instead of
  freed memory, guard the event listener with a lock, apply allow-lan,
  and report side-load and config-validation errors.
- Rust IPC: release the state lock before joining, which deadlocked stop
  and restart whenever the core was connected.
- Report core start failures instead of a stale connected state, and time
  out the shutdown wait.
- Fix range errors in the proxy list and tab when a search filter is
  active, and a format exception in the port dialog.
- Cancel the previous poll timer on core restart, batch delay tests
  lazily, stop the dashboard memory poll after dispose, and dispose the
  JavaScript runtime after each evaluation.

Platform
- macOS: do not cancel system logout or shutdown; read the SSID off the
  platform thread; disable the proxy on every network service.
- Android: promote the service to foreground even with the screen off,
  make VPN start failures observable, and keep the SSID and package
  caches thread-safe.
- Linux: bundle the QuickJS bridge and fix GVariant lifetime handling in
  the SSID lookup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drop two redundant awaits in returns and use the null-aware element
marker in Rule.rawValue. Reported by flutter analyze.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The notes pointed at a .fvmrc that no longer exists and named a CI
version that does not match the workflow. Verified against
.github/workflows/build.yaml and by resolving dependencies: app_links
requires Dart 3.12, so the declared 3.8.0 lower bound in pubspec.yaml is
not the effective minimum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ith tests

Verifying certificates broke self-hosted WebDAV servers and subscription
URLs that present a self-signed or private-CA certificate, with no way to
keep using them. Add an application setting, off by default, that accepts
them again; loopback stays trusted unconditionally either way.

Also extract the archive entry check from the restore task so it can be
tested directly, and cover it, the order-key allocation a merge restore
depends on, and the options dialog's dismiss path. Drop the now-unused
Service.getRunTime wrapper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
run_build_tool.sh writes its own location into the generated runner
pubspec as a path dependency, but under Git Bash `pwd` yields a POSIX
path ("/f/..."), and the native Dart SDK that resolves it cannot. The
Android build therefore fails on Windows with "could not find package
build_tool". Convert the path with cygpath when it is available, which
leaves Linux and macOS untouched.

Found while building the APK to verify the Kotlin and cgo changes in this
branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mihomo's log.Errorln is printf-style (log.Errorln -> newLog ->
fmt.Sprintf), so `log.Errorln("TUN:", err)` rendered the reason through
fmt's surplus-operand marker as `TUN:%!(EXTRA *fmt.wrapError=...)`. The
reason was always in the log, just wrapped in that marker: use real
format verbs so the message reads cleanly and go vet's printf check
passes.

This is a readability and lint fix, not a functional one - it does not
resolve any reported TUN startup failure, so please do not backport it
as such.

Found by go vet once the Android toolchain made the cgo-only packages
analysable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were changed in this branch but had no coverage: the layout delegate
now handles a child larger than its viewport, where clamp used to be given
an upper bound below its lower one, and safeCopy reports a missing source
instead of creating an empty file in its place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extracting the entry check moved the join to a per-segment form, which
made `C:/evil.txt` slip through: it is not posix-absolute and carries no
`..`, so it became an ordinary segment and produced a path inside the
restore directory that Windows cannot create. Writing it threw an
uncaught FileSystemException and aborted the whole restore, where the
previous inline code simply skipped the entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream advanced nine commits (375 files, +27756/-10126) that rewrote the
Android service layer, the desktop core transport and the core message layer.
Forty files conflicted; each fix was re-checked against the new code rather
than replayed mechanically.

Dropped because upstream fixed the same defect, independently or by removing
the code that carried it:

- the whole Android tile/service set (State.kt, Service.kt, CommonService.kt,
  RemoteService.kt, ModuleLoader.kt) - ServiceState/ServiceController settle
  the run state from the actual start result, ServiceModules is synchronous,
  and ProxyService.start rethrows
- AppPlugin package enumeration (now PackageResolver, lazy + immutable) and
  the VPN consent path (denied consent now reports false)
- VpnService uid map, NotificationModule foreground promotion, core.cpp
  scoped_string, CoreLib withTimeout, external-provider lock, the traffic
  double-free, DAV ping generation guard, WebDAV file name preservation,
  proxy delay-test batching, the proxies tab/list index mismatch guard,
  GeoResource config keys, the loading widget, memory info lifecycle,
  start-button width measurement, macOS location permission mapping, and the
  Rust IPC deadlock (listener is now non-blocking, so the stop path joins)

Kept because the defect is still live on the new code, re-applied against the
new APIs:

- OptionsDialog reports a dismissed dialog as picking null, which resets the
  app language to "follow system"; the null delivered by a toggleable radio
  is now treated as no selection
- restoring a backup drops excludeSSIDs, a top-level Config field
- quitting skips the debounced config flush that the minimize path performs
- a desktop core crash leaves the system proxy pointed at a dead port
- deleting a profile orphans groups, links and rules: the declared cascade is
  inert because sqlite3 disables foreign keys and beforeOpen does not enable
  them
- proxy group delete by name deletes the wrong group mid-rename; renaming onto
  an existing name is not rejected; a new group's order key is not persisted
- the zip-slip guard on backup restore
- eventListener is read from core goroutines while the platform thread
  replaces and releases it
- macOS system-proxy teardown stops at the first failed network service
- Windows leaves the system proxy set across shutdown/logoff
- Windows reports Wi-Fi permission as granted regardless, and forwards
  non-UTF-8 SSIDs that the Dart side cannot decode
- a disposed connections view writes to its released notifier
- new profiles are inserted at the top and then jump to the bottom
- an offline update check is reported as "already up to date"
- a geo update error is announced as a successful update first

Verified: flutter analyze clean, 740 of 741 tests pass, go build and go vet
clean, cgo cross-build for android/arm64 clean. The one failing test,
task_test's makeRealProfileTask case, fails identically on pristine
origin/main under Windows because it asserts POSIX separators against a
path built with package:path join; it is untouched by this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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