Skip to content

perf + security: hot-path batching, action/source pins, local API token#115

Merged
hawkff merged 26 commits into
mainfrom
feat/pr2-perf-security
Jul 3, 2026
Merged

perf + security: hot-path batching, action/source pins, local API token#115
hawkff merged 26 commits into
mainfrom
feat/pr2-perf-security

Conversation

@hawkff

@hawkff hawkff commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Performance improvements on hot paths plus supply-chain and local-service-hardening
changes. Stacked on the bug-fix PR (merge that first).

Performance

  • Selector reload check: derive the selector group id via two DAO lookups instead
    of building a full throwaway configuration on every switch (mirrors the existing
    builder logic; a keep-in-sync comment references the source lines).
  • Test-result writes: persist connection-test results in one batch write instead
    of one write plus one listener round per profile; the existing full-group refresh
    that follows delivers the UI update.
  • Per-tick updates: add a batched callback so per-second updates are sent once per
    tick (one IPC + one main-thread hop + an O(1) lookup) instead of one call per
    profile with an O(n) scan. The single-item callback is retained for the
    selector-switch path; new interface methods have default bodies so existing
    implementers are unaffected.

Security / supply chain

  • Pin third-party CI actions: pin every third-party action across the CI and
    test workflows to a full commit SHA with a version comment (matching the one action
    already pinned this way). No job logic changes.
  • Pin native build inputs: pin the toolchain source (previously a moving branch)
    and the bundled sidecar source (previously a moving tag) to exact commits, matching
    the pinning the sibling sources already use. Fresh checkout dir + fail-fast on git
    errors so an interrupted prior run can’t build from stale state.
  • Prefer bundled binaries: resolve the app’s bundled sidecar binaries before any
    externally installed provider, so an external package can’t shadow a binary the app
    ships (external providers are still used for anything the app doesn’t bundle).
  • Local control API: require a per-install token for the optional local control
    API (off by default) and pre-authenticate the bundled dashboard for the local
    endpoint only. The token is added to the log-redaction list.

Testing

  • Reviewed as a diff (CodeRabbit CLI, findings addressed). Workflow YAML validated;
    build-script syntax checked. Compilation, lint, and unit gates run in CI.
  • The local control-API change requires an on-device check (dashboard authenticates;
    unauthenticated requests are rejected) before merge — to be verified on Android.

Follow-up (not in this PR)

  • Mirroring the two remaining external sources into the project’s own org and
    repointing is tracked separately (needs an org-mirror step + lockfile regeneration).

Greptile Summary

This PR delivers batched-IPC performance improvements on hot paths (traffic updates, test-result writes, selector-reload checks) and a suite of supply-chain / local-API security hardening changes on top of the bug-fix base branch. Previously identified issues — the per-install Clash API token leaking into exported configs (ConfigBuilder.kt) and the WebviewFragment URL-injection bugs (fragment-blinding, prefix ambiguity) — are fully addressed here.

  • Batched traffic callbacks: cbTrafficUpdateList is added to the AIDL interface with a default fallback body, so existing implementers are unaffected; TrafficLooper and ConfigurationFragment use the new batch path to reduce IPC round-trips per tick.
  • Supply-chain pins: every third-party CI action is pinned to an immutable commit SHA; mieru.sh and libcore/init.sh now fetch by commit hash instead of a mutable tag/branch, with set -e / explicit || exit 1 providing fail-fast guarantees.
  • Local Clash API token: a per-install UUID secret is generated lazily, stored in DataStore, redacted from logs via Commandline.kt, injected into configs only when not exporting, and pre-authenticated into the embedded dashboard URL via proper Uri.buildUpon() query construction.

Confidence Score: 5/5

Safe to merge after the on-device dashboard auth check noted in the PR description is confirmed.

All previously flagged issues are addressed: the Clash API secret is now guarded by !forExport in ConfigBuilder, and WebviewFragment uses Uri.buildUpon() with an exact host+port match to inject credentials safely. The new batch traffic path mirrors the pre-existing single-item pattern. Build-script changes are protected by set -e (mieru.sh) and explicit || exit 1 (libcore/init.sh). No new correctness or security issues were found in the diff.

No files require special attention beyond the on-device dashboard authentication verification called out in the PR description.

Important Files Changed

Filename Overview
app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt Adds dashboardUrl() using Uri.buildUpon() + exact host/port check — properly fixes the prior fragment-blinding and prefix-ambiguity issues.
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt Guards the Clash API secret with !forExport, preventing it from leaking into exported/shared configs — addresses the previously flagged P1 finding.
app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt Resolves bundled binaries first via initNativeInternal before falling back to getPluginExternal, preventing external providers from shadowing shipped binaries.
app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt canReloadSelector() now derives selectorGroupId via two DAO lookups instead of building a throwaway ProxyInstance — optimization is logically equivalent to the old path.
app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt Switches from per-profile cbTrafficUpdate calls to a single cbTrafficUpdateList batch per tick, reducing IPC overhead proportional to profile count.
app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt Adds batch onUpdated(List) override and switches test-result persistence to updateProfileQuietly — follows the same ViewHolder lookup pattern as the pre-existing single-item override.
buildScript/lib/mieru.sh Pins mieru checkout to immutable commit SHA; set -e at the top provides fail-fast if any git step fails, consistent with the PR's stated design goal.
libcore/init.sh Pins gomobile toolchain to a commit SHA, adds rm -rf before git init to prevent stale state reuse, and adds explicit
app/src/main/java/io/nekohasekai/sagernet/utils/Commandline.kt Adds 'secret' to the JSON-field redaction list so the Clash API token is scrubbed from log output.
app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl Adds cbTrafficUpdateList to the oneway callback interface; appended at the end preserves backward-compatible ordinal numbering for existing methods.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant TL as TrafficLooper
    participant Binder as ISagerNetServiceCallback (AIDL)
    participant SC as SagerConnection
    participant MA as MainActivity
    participant PM as ProfileManager
    participant CF as ConfigurationFragment.Listener

    Note over TL: Per-tick (1 s)
    TL->>TL: Build ArrayList(traffic.values)
    TL->>Binder: cbTrafficUpdateList(batch)
    Binder->>SC: cbTrafficUpdateList(stats)
    SC->>SC: runOnMainDispatcher
    SC->>MA: cbTrafficUpdateList(data)
    MA->>MA: runOnDefaultDispatcher
    MA->>PM: postUpdate(data)
    PM->>CF: "onUpdated(List<TrafficData>)"
    CF->>CF: Build positions map (O(n))
    CF->>CF: Lookup visible holders (background)
    CF->>CF: onMainDispatcher
    CF->>CF: bind each holder (main thread)

    Note over TL: Selector-switch path (unchanged)
    TL->>Binder: cbTrafficUpdate(single)
    Binder->>SC: cbTrafficUpdate
    SC->>MA: cbTrafficUpdate
    MA->>PM: postUpdate(single)
    PM->>CF: onUpdated(TrafficData)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant TL as TrafficLooper
    participant Binder as ISagerNetServiceCallback (AIDL)
    participant SC as SagerConnection
    participant MA as MainActivity
    participant PM as ProfileManager
    participant CF as ConfigurationFragment.Listener

    Note over TL: Per-tick (1 s)
    TL->>TL: Build ArrayList(traffic.values)
    TL->>Binder: cbTrafficUpdateList(batch)
    Binder->>SC: cbTrafficUpdateList(stats)
    SC->>SC: runOnMainDispatcher
    SC->>MA: cbTrafficUpdateList(data)
    MA->>MA: runOnDefaultDispatcher
    MA->>PM: postUpdate(data)
    PM->>CF: "onUpdated(List<TrafficData>)"
    CF->>CF: Build positions map (O(n))
    CF->>CF: Lookup visible holders (background)
    CF->>CF: onMainDispatcher
    CF->>CF: bind each holder (main thread)

    Note over TL: Selector-switch path (unchanged)
    TL->>Binder: cbTrafficUpdate(single)
    Binder->>SC: cbTrafficUpdate
    SC->>MA: cbTrafficUpdate
    MA->>PM: postUpdate(single)
    PM->>CF: onUpdated(TrafficData)
Loading

Comments Outside Diff (1)

  1. app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt, line 239-244 (link)

    P1 security Per-install secret leaked into exported configs

    buildConfig is also called with forExport = true from ProxyEntity.exportConfig(), which serialises the result to a sharable JSON file. Because the DataStore.enableClashAPI block does not guard on forExport, the real per-install secret is written verbatim into every exported config. Anyone who receives the exported file (e.g. for troubleshooting) gains knowledge of the local Clash API token for that device. While the API is bound to loopback today, the token should be treated as a local capability credential and not exported.

Reviews (6): Last reviewed commit: "security(config): omit the local API tok..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main performance, supply-chain, and local API security changes.
Description check ✅ Passed The description clearly matches the changeset and covers the performance, pinning, and local API hardening work.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@hawkff hawkff force-pushed the feat/pr2-perf-security branch from cdfdb76 to b561ec5 Compare July 3, 2026 01:42
Comment thread app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt Outdated
Comment thread app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt Outdated
Comment thread app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
@hawkff hawkff force-pushed the feat/pr2-perf-security branch from b561ec5 to 846f0f8 Compare July 3, 2026 01:52
@hawkff hawkff force-pushed the feat/pr2-perf-security branch from 846f0f8 to 7d97488 Compare July 3, 2026 02:01
@hawkff hawkff force-pushed the feat/pr2-perf-security branch from 7d97488 to d225538 Compare July 3, 2026 09:09
@hawkff

hawkff commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Fixed the dashboard-URL findings: it's now built via Uri so the token lands in the query component (not inside a #fragment) and the local-controller check matches host+port exactly instead of a broad startsWith.

Declining the keep-in-sync line-number note: the comment is a review aid pointing reviewers at the builder's derivation; the exact line numbers drifting is acceptable and cheaper than removing the pointer entirely.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
.github/workflows/release.yml (1)

23-24: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add persist-credentials: false to checkout steps.

zizmor flags these actions/checkout steps for credential persistence (artipacked): the default GitHub token is left in the local git config for the rest of the job, so any subsequent script (including third-party build tooling now being fetched/executed in this same PR) could exfiltrate it. Since this PR's stated purpose is security hardening, this is a good complementary fix.

🔒 Proposed fix
       - name: Checkout
-        uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
+        uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
+        with:
+          persist-credentials: false

This applies to every checkout step in this workflow (and likely other .github/workflows/*.yml files sharing the same pattern).

Also applies to: 55-56, 95-102, 154-156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 23 - 24, Add persist-credentials:
false to every actions/checkout step in this workflow so the GitHub token is not
left in git config for later steps. Update each Checkout job block in
release.yml that uses actions/checkout (the repeated checkout invocations in the
workflow) to include this setting alongside the existing uses reference.

Source: Linters/SAST tools

.github/workflows/ci.yml (1)

19-19: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider persist-credentials: false on checkout steps.

Same as flagged in build.yml — these checkout steps persist the git token in local config for the rest of the job. Worth disabling given the security-hardening intent of this cohort.

🔒 Suggested addition
       - name: Checkout
         uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
+        with:
+          persist-credentials: false

Also applies to: 126-126

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 19, The checkout steps in the CI workflow
still persist the git token in local config, which conflicts with the hardening
intent. Update each actions/checkout usage in the workflow to disable credential
persistence by setting persist-credentials to false, including the additional
checkout instance referenced in the same job.

Source: Linters/SAST tools

.github/workflows/build.yml (1)

21-22: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider persist-credentials: false on checkout steps.

Static analysis flags these actions/checkout steps for retaining the git credential in local config for the rest of the job. Since this PR is hardening CI supply-chain security (SHA pinning), also disabling credential persistence on checkout reduces the blast radius if a later step in the job is compromised.

🔒 Suggested addition
       - name: Checkout
         uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
+        with:
+          persist-credentials: false

Also applies to: 90-91

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml around lines 21 - 22, The Checkout step in the
workflow is still persisting git credentials, which weakens the CI hardening.
Update the `actions/checkout` usages in the workflow to set
`persist-credentials: false` so credentials are not left in local git config for
later steps. Apply this to the relevant Checkout steps identified by the
`actions/checkout` entries in `.github/workflows/build.yml`.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt`:
- Around line 187-195: The clashApiSecret getter in DataStore currently
generates and stores a UUID under `@Synchronized`, but that only protects one
process and can race with other processes reading the shared configurationStore.
Replace the read-then-generate flow with a single atomic insert-if-absent or
transactional claim that returns the already-stored value if present, and only
creates one new secret when missing. Use the existing clashApiSecret property
and configurationStore access as the place to centralize this cross-process-safe
claim.

In `@app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt`:
- Around line 1719-1739: The batch traffic update lookup is happening off the
main thread in onUpdated, but it reads configurationIdList and touches
layoutManager.findViewByPosition() and
configurationListView.getChildViewHolder(), which must be done on the UI thread.
Refactor ConfigurationFragment.onUpdated so the index resolution and holder
lookup happen inside onMainDispatcher { ... } or configurationListView.post {
... }, then bind the collected ConfigurationHolder/TrafficData pairs there to
avoid stale positions or recycled holders.

In `@buildScript/lib/mieru.sh`:
- Around line 22-24: The fetch logic in mieru.sh still uses the raw MIERU_COMMIT
SHA, which can fail when the commit is not advertised or the tag moves; update
the checkout flow around the git fetch call to fetch and use the release tag or
another stable ref instead of the bare SHA. Keep MIERU_COMMIT as the pinned
revision source if needed, but make the actual fetch in the MIERU version
handling code resolve via the tag/ref so the build does not depend on GitHub
accepting raw-SHA fetches.

---

Nitpick comments:
In @.github/workflows/build.yml:
- Around line 21-22: The Checkout step in the workflow is still persisting git
credentials, which weakens the CI hardening. Update the `actions/checkout`
usages in the workflow to set `persist-credentials: false` so credentials are
not left in local git config for later steps. Apply this to the relevant
Checkout steps identified by the `actions/checkout` entries in
`.github/workflows/build.yml`.

In @.github/workflows/ci.yml:
- Line 19: The checkout steps in the CI workflow still persist the git token in
local config, which conflicts with the hardening intent. Update each
actions/checkout usage in the workflow to disable credential persistence by
setting persist-credentials to false, including the additional checkout instance
referenced in the same job.

In @.github/workflows/release.yml:
- Around line 23-24: Add persist-credentials: false to every actions/checkout
step in this workflow so the GitHub token is not left in git config for later
steps. Update each Checkout job block in release.yml that uses actions/checkout
(the repeated checkout invocations in the workflow) to include this setting
alongside the existing uses reference.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ceb6d937-1eb6-46b4-a963-97ca24f98f40

📥 Commits

Reviewing files that changed from the base of the PR and between 4609c29 and 722a1ea.

📒 Files selected for processing (24)
  • .depot/workflows/android-instrumented.yml
  • .depot/workflows/build-apk.yml
  • .depot/workflows/guard.yml
  • .depot/workflows/lint.yml
  • .depot/workflows/unit-tests.yml
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl
  • app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
  • app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt
  • app/src/main/java/io/nekohasekai/sagernet/utils/Commandline.kt
  • buildScript/lib/mieru.sh
  • libcore/init.sh

Comment on lines +187 to +195
val clashApiSecret: String
@Synchronized get() {
var s = configurationStore.getString(Key.CLASH_API_SECRET)
if (s.isNullOrEmpty()) {
s = java.util.UUID.randomUUID().toString().replace("-", "")
configurationStore.putString(Key.CLASH_API_SECRET, s)
}
return s
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files and usages first
git ls-files | rg '^(app/src/main/java/io/nekohasekai/sagernet/database/DataStore\.kt|app/src/main/java/io/nekohasekai/sagernet/.*(ConfigBuilder|WebviewFragment)|.*configurationStore.*|.*Key\.kt)$' || true

echo '--- DataStore.kt ---'
sed -n '1,260p' app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

echo '--- configurationStore usages ---'
rg -n "configurationStore|CLASH_API_SECRET|clashApiSecret" app/src/main/java -g '!**/build/**'

echo '--- Key definition ---'
rg -n "CLASH_API_SECRET" app/src/main/java

echo '--- storage backend clues ---'
rg -n "SharedPreferences|DataStore|MMKV|Room|ObjectBox|Preferences" app/src/main/java

Repository: hawkff/NekoBoxForAndroid

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- RoomPreferenceDataStore.kt (outline) ---'
ast-grep outline app/src/main/java/io/nekohasekai/sagernet/database/preference/RoomPreferenceDataStore.kt --view expanded || true

echo '--- RoomPreferenceDataStore.kt (relevant slices) ---'
sed -n '1,220p' app/src/main/java/io/nekohasekai/sagernet/database/preference/RoomPreferenceDataStore.kt
echo '---'
sed -n '220,520p' app/src/main/java/io/nekohasekai/sagernet/database/preference/RoomPreferenceDataStore.kt

echo '--- KeyValuePair.kt (DAO / conflict strategy) ---'
sed -n '1,220p' app/src/main/java/io/nekohasekai/sagernet/database/preference/KeyValuePair.kt

echo '--- startup / secret consumers ---'
sed -n '1,160p' app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt
echo '---'
sed -n '220,280p' app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
echo '---'
sed -n '60,120p' app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt

Repository: hawkff/NekoBoxForAndroid

Length of output: 39838


Make the initial Clash API secret claim atomic across processes. @Synchronized only serializes one process, but configurationStore is shared and refreshed asynchronously. On a fresh install, main and bg can both generate different UUIDs before either sees the other's write, so the core and dashboard can end up using different secrets. Use a single insert-if-absent/transactional claim that returns the stored value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt` around lines
187 - 195, The clashApiSecret getter in DataStore currently generates and stores
a UUID under `@Synchronized`, but that only protects one process and can race with
other processes reading the shared configurationStore. Replace the
read-then-generate flow with a single atomic insert-if-absent or transactional
claim that returns the already-stored value if present, and only creates one new
secret when missing. Use the existing clashApiSecret property and
configurationStore access as the place to centralize this cross-process-safe
claim.

Comment on lines +1719 to +1739
override suspend fun onUpdated(data: List<TrafficData>) {
try {
val positions = HashMap<Long, Int>(configurationIdList.size)
configurationIdList.forEachIndexed { index, id -> positions[id] = index }
val updates = ArrayList<Pair<ConfigurationHolder, TrafficData>>()
for (item in data) {
val index = positions[item.id] ?: continue
val holder = layoutManager.findViewByPosition(index)
?.let { configurationListView.getChildViewHolder(it) } as ConfigurationHolder?
if (holder != null) updates.add(holder to item)
}
if (updates.isNotEmpty()) {
onMainDispatcher {
for ((holder, item) in updates) holder.bind(holder.entity, item)
}
}
} catch (e: Exception) {
Logs.w(e)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt"

echo "== File outline =="
ast-grep outline "$FILE" --view expanded || true

echo
echo "== Relevant line ranges =="
sed -n '1650,1765p' "$FILE" | cat -n

echo
echo "== Search for configurationIdList mutations and traffic update callbacks =="
rg -n "configurationIdList|cbTrafficUpdateList|postUpdate|runOnDefaultDispatcher|onUpdated\(data: List<TrafficData>\)|onUpdated\(data: TrafficData\)" app/src/main/java -S

Repository: hawkff/NekoBoxForAndroid

Length of output: 30999


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Async dispatcher helpers =="
sed -n '1,120p' app/src/main/java/io/nekohasekai/sagernet/ktx/Asyncs.kt | cat -n

echo
echo "== ProfileManager traffic update dispatch =="
sed -n '1,240p' app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt | cat -n

echo
echo "== MainActivity traffic callbacks =="
sed -n '410,450p' app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt | cat -n

echo
echo "== ConfigurationFragment mutations around the reported area =="
sed -n '1550,1755p' app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt | cat -n

Repository: hawkff/NekoBoxForAndroid

Length of output: 22847


Move the batch traffic update lookup onto the main thread.
onUpdated(List<TrafficData>) runs on Dispatchers.Default via cbTrafficUpdateList -> ProfileManager.postUpdate, but it reads configurationIdList and calls layoutManager.findViewByPosition() / getChildViewHolder(). Those accesses should stay on the main thread; otherwise a concurrent post {} mutation can produce a stale index or recycled holder. Move the lookup into onMainDispatcher { ... } or configurationListView.post { ... } before binding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt` around
lines 1719 - 1739, The batch traffic update lookup is happening off the main
thread in onUpdated, but it reads configurationIdList and touches
layoutManager.findViewByPosition() and
configurationListView.getChildViewHolder(), which must be done on the UI thread.
Refactor ConfigurationFragment.onUpdated so the index resolution and holder
lookup happen inside onMainDispatcher { ... } or configurationListView.post {
... }, then bind the collected ConfigurationHolder/TrafficData pairs there to
avoid stale positions or recycled holders.

Comment thread buildScript/lib/mieru.sh
Comment on lines +22 to +24
# Immutable commit that MIERU_VERSION points to (pinned for reproducible builds;
# update together with MIERU_VERSION on any bump).
MIERU_COMMIT="${MIERU_COMMIT:-1532c85cc8ca08dff469326f35a3f027697c6950}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does GitHub reliably support git fetch --depth 1 origin for a commit that is not the tip of any branch or tag?

💡 Result:

No, GitHub does not reliably support git fetch --depth 1 origin for commits that are not advertised as branch tips or tags [1][2]. In the Git protocol, servers generally only allow clients to fetch objects that are reachable from advertised references (like branches and tags) to prevent the exposure of non-public history and to manage performance costs [3][4][2]. While the underlying Git software supports configuration flags—such as uploadpack.allowReachableSHA1InWant—that could theoretically enable this, GitHub does not expose such arbitrary commit fetching via the Git protocol [1][4][2]. If you attempt to run git fetch --depth 1 origin on GitHub for a non-advertised commit, the command will typically fail [1][2]. To obtain a specific commit from a GitHub repository, recommended alternatives include: 1. Using the GitHub REST API or GraphQL API: You can use the GitHub API to download a tarball or zipball of a specific commit SHA, which bypasses the Git protocol entirely [5][2]. 2. Progressive Deepening: If you have a shallow clone of the repository, you can perform a shallow clone of the default branch and then use git fetch --deepen repeatedly to pull more history until the desired commit is reached [6][2]. 3. Full Clone: If the repository size permits, a full git clone remains the most reliable way to access any arbitrary commit in the history [2].

Citations:


🌐 Web query:

GitHub git fetch by raw commit SHA shallow clone non-tip commit documented support uploadpack.allowReachableSHA1InWant

💡 Result:

The ability to fetch a specific commit SHA that is not at the tip of a branch (a non-tip commit) via a shallow clone is controlled by the server-side configuration uploadpack.allowReachableSHA1InWant [1][2][3]. When this setting is enabled on a Git server, it permits the server to accept a fetch request for any object (such as a specific commit SHA) that is reachable from any of the repository's ref tips, even if that commit is not explicitly advertised [2][4][5]. This functionality was introduced to allow clients to fetch specific commits—useful for submodules or CI/CD workflows—without needing to clone or fetch the entire repository history [2][5]. Key points regarding this feature include: 1. Server-Side Requirement: The Git server must explicitly set uploadpack.allowReachableSHA1InWant to true [2][5]. By default, this value is false [1][3]. Whether this is enabled on platforms like GitHub can vary depending on the specific infrastructure or configuration (e.g., GitHub Enterprise instances may default to disabled) [6]. 2. Functionality: When enabled, a client can perform a shallow fetch (e.g., git fetch --depth 1 origin <SHA>) for a commit that is not a branch or tag tip [2][7]. The server performs a reachability check to ensure the requested SHA is part of the repository history, which carries a computational cost [1][2][3]. 3. Documentation: This configuration is officially documented in the Git configuration documentation for upload-pack [1][3][8]. It is separate from uploadpack.allowTipSHA1InWant, which only allows fetching objects that are currently at the tips of refs [1][5]. 4. Client Usage: While modern Git clients support requesting these SHAs, the successful execution of such a request depends entirely on the server's policy [2][9]. If the server does not have this setting enabled, a fetch request for a non-advertised SHA will typically be rejected, forcing clients to fall back to fetching more history or the entire repository [4][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files buildScript/lib/mieru.sh
wc -l buildScript/lib/mieru.sh
cat -n buildScript/lib/mieru.sh | sed -n '1,120p'

Repository: hawkff/NekoBoxForAndroid

Length of output: 3529


Fetch the release tag instead of the raw commit SHA in buildScript/lib/mieru.sh:46-58 git fetch --depth 1 origin "$MIERU_COMMIT" still depends on GitHub accepting raw-SHA fetches, which is not guaranteed for non-tip commits. Even with this pin on a release-tag commit, the build can break if the tag is moved or the SHA is no longer advertised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@buildScript/lib/mieru.sh` around lines 22 - 24, The fetch logic in mieru.sh
still uses the raw MIERU_COMMIT SHA, which can fail when the commit is not
advertised or the tag moves; update the checkout flow around the git fetch call
to fetch and use the release tag or another stable ref instead of the bare SHA.
Keep MIERU_COMMIT as the pinned revision source if needed, but make the actual
fetch in the MIERU version handling code resolve via the tag/ref so the build
does not depend on GitHub accepting raw-SHA fetches.

The per-install token is for the local controller only; a config exported
for sharing must not embed it. Mirrors the existing export gating in the
selector path.
@hawkff hawkff deleted the branch main July 3, 2026 11:10
@hawkff hawkff closed this Jul 3, 2026
@hawkff hawkff reopened this Jul 3, 2026
@hawkff hawkff changed the base branch from fix/pr1-runtime-bugs to main July 3, 2026 11:13
@hawkff hawkff merged commit b677423 into main Jul 3, 2026
7 checks passed
@hawkff hawkff deleted the feat/pr2-perf-security branch July 3, 2026 11:23
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