Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog.d/fixed-v1-policy-cache-30f8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
_2026-10-10_

## English

Fixed shared-app-ID Java hook selection and policy-cache races. Temporary policy read failures retain the last successful policy and report an error; LSPosed telemetry keeps complete snapshots on write failure. Debug reports include the capture Dashboard, state before capture and stable issue reasons, and the export screen explains partial and failed results. Added accessible search labels, lifecycle-aware screen subscriptions and bounded agent HTTP requests.

## Русский

Исправлены выбор Java-хуков при общем app ID и гонки кеша политики. Временная ошибка чтения сохраняет последнюю успешную политику и отражается в диагностике; телеметрия LSPosed сохраняет полный снимок при ошибке записи. Отчёт включает Дашборд из данных захвата, состояние до сбора и стабильные причины предупреждений; экран экспорта объясняет частичный и неудачный результат. Добавлены доступные подписи поиска, подписки экранов с учётом lifecycle и лимиты HTTP-запросов агента.
23 changes: 22 additions & 1 deletion docs/debug-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ renders*, so the bundle can't disagree with what the user saw on screen.
| `activeBackend` | The **one** native backend in charge (`id` + `state`). Priority kmod > builtin > KPM > zygisk; a live `/proc/vpnhide_ctl` decides between the two kernel backends by the `backend` id it reports. |
| `ports` | The portshide module `ModuleState` (localhost port blocker). |
| `kmodLoadStatus` | Boot-time `.ko` load result (uname, kprobes/kretprobes ok, insmod exit, dmesg tail, `filesystemHiding`). The richest single "did the kernel module come up" field. |
| `dashboard` | Full live dashboard model. **`null` in file exports** (only the agent-bridge `getState` fills it). |
| `dashboard` | Dashboard derived from the capture root snapshot and one saved diagnostic presentation, shared with `diagnostics`. File exports and agent bridge use the same builder. A failed dashboard assembly leaves this null, appends a reason to `errors`, and preserves the rest of the archive. |
| `beforeCapture` | Optional available process state before enabling capture logging or starting checks: observation time, diagnostic summary, retained Dashboard protection/messages, pending operation IDs, recovery flag and last operation phases/failure. No fresh root read or diagnostic run is requested. It may be absent or use retained screen values; it is the state available at collection start, not proof of what the user previously saw. |
| `config` | The canonical desired-state config as structured JSON (`/data/system/vpnhide_config.json`) — answers "is app X even a target, with which roles". |
| `statistics` | Point-in-time hook-counter totals (per-uid/per-method hit counts). Always present in a debug export, even without forensics. |
| `rootShell` | Snapshot-shell self-diagnosis (§6). The "not verified vs inactive" discriminator. |
Expand Down Expand Up @@ -428,3 +429,23 @@ Rule of thumb: a **typed** top-level field → find where it's *derived*
| Was the capture even measurable? | `gate` (§4) |
| Is anything missing/cut off? | `errors`, `sections.debug_snapshot_truncated` |
| What was included in this capture? | `captureOptions`, `captureKind` |

### Export feedback and stable issue reasons

The Collect sheet shows complete, partial and failed outcomes. Partial archives
remain available for Save/Share; the section failures are shown alongside the
result. A failed export retains its reason and can be retried.

Each `dashboard.messages` entry keeps its existing severity, localized text,
action, download artifact and help article, and adds `reason` and
`presentationLanguage`. `reason.kind` is an explicit stable snake_case identifier;
`reason.parameters` is a map of named string values (versions, components, counts,
details; list-valued parameters use comma-separated stable package/hook names).
The same `DashboardIssue` that produces the UI message produces its reason. No
export-specific detection rules exist. For example `module_version_mismatch`
carries `component`, `moduleVersion` and `appVersion` regardless of RU/EN wording.

New reason identifiers and optional parameters are additive. Do not rename a
published identifier, repurpose a parameter or infer it from localized text;
changes of meaning follow the existing schema-bump rule. These fields and
`beforeCapture` are additive within schema 2 and are covered by the bundle golden.
6 changes: 6 additions & 0 deletions docs/help/en/configure-hiding.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ custom per-hook settings.

Ready? [Open the Hiding tab](vpnhide://hiding) and pick the app to configure.

Packages sharing an Android app ID also share the effective Java settings in
all profiles: enabled Java hooks from each package are combined. Turning Java
off for one package does not disable hooks selected by another package in the
group. To disable a hook for the group, deselect it in every package. The Java
hook dialog shows a notice when the app inventory identifies a shared app ID.

## Row, role chips and custom hooks

Tapping an unselected app row enables Java and Apps, plus Native and Ports when
Expand Down
6 changes: 6 additions & 0 deletions docs/help/ru/configure-hiding.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ split tunneling.

Готовы? [Откройте вкладку «Скрытие»](vpnhide://hiding) и выберите приложение.

Пакеты с общим Android app ID используют общие действующие настройки Java во
всех профилях: включённые хуки каждого пакета объединяются. Отключение Java у
одного пакета не отключает хуки, выбранные у другого пакета группы. Чтобы
отключить хук для всей группы, снимите его у каждого пакета. Диалог Java-хуков
показывает пояснение, когда список приложений выявляет общий app ID.

## Строка приложения, плашки ролей и отдельные хуки

Нажатие на невыбранную строку включает Java и Apps, а также Native и Ports, если
Expand Down
5 changes: 5 additions & 0 deletions docs/help/zh/configure-hiding.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ VPN Hide 会自动识别;你也可以手动隐藏。

准备好了?[打开「隐藏」标签页](vpnhide://hiding)并选择要配置的应用。

共享 Android 应用 ID 的软件包在所有用户配置中使用相同的有效 Java 设置:每个软件包启用的钩子会合并。
关闭其中一个软件包的 Java 角色不会禁用其他软件包选择的钩子。
要为整个组禁用某个钩子,请在每个软件包中取消选择它。
应用列表识别到共享应用 ID 时,Java 钩子对话框会显示提示。

## 应用行、角色按钮与单独钩子

点击未选择的应用行会开启 Java 和 Apps,并在对应组件已安装时开启 Native 和 Ports。
Expand Down
36 changes: 36 additions & 0 deletions docs/notes/v1-improvements-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# V1 improvement checklist

Work branch: `fix/v1-policy-cache`, based on `02bc7379ffd9240cc96a7b2b80b6c39e2170c3dd`.
The updated task is implemented in one branch. Only v1 is in scope.

## Implementation and review

- [x] **P1-01** — Enabled Java hook masks are unioned by appId at the Binder boundary. Disabled roles add no bits; every profile of a shared appId gets the same union. Package intent remains in canonical JSON. Inventory-detected sharing is explained in the Java dialog and all three offline-guide languages. Tests cover order, equal/different masks, disabled/empty/default roles, missing packages and profiles.
- [x] **P1-02** — Both canonical JSON and `packages.list` are fingerprinted before/after reads (device, inode, size, nanosecond mtime/ctime). Cache entries carry an atomic invalidation generation. Two attempts maximum; unstable reads retain a coherent prior value and retry on the next call. Tests drive file replacement, missed watcher events, invalidation during I/O and immediately before publication, and continuous changes. A watcher-thread test verifies invalidation does not wait for the reader lock.
- [x] **P2-03** — Typed read results distinguish errors from a successful empty policy. Errors retain last-good (empty at startup), publish an LSPosed metadata error and retry after one second without requiring a metadata change. Valid empty JSON or a removed canonical file clears policy. Recovery clears the error, including stat recovery without changed metadata. Tests cover these cache transitions; actual Android file-permission/SELinux scenarios remain device checks.
- [x] **P2-04** — Close-search and clear-query buttons have EN/RU/ZH accessible names. Resource compilation and lint pass; TalkBack focus/navigation remain device checks.
- [x] **P2-05** — Telemetry uses atomic same-directory rename only. Failure preserves the old complete file and in-memory counters, then schedules another flush. Tests verify a failed replacement/recovery and an open reader retaining the complete old inode across replacement. Actual target permissions/SELinux remain device checks.
- [x] **P3-06** — Screen subscription audit completed. Screen state collection uses lifecycle-aware APIs; the existing RESUMED-owned app-VPN poller and process-owned observation/config/diagnostic coordinators remain intact. Operation feedback subscriptions remain active to handle results of accepted work. Statistics-session polling intentionally continues in the background while the user exercises another app. Activity recreation/tab/background scenarios remain device checks.
- [x] **P3-07** — Import scans recurse through hook and app packages. Reviewed shared sources are checked for UI imports, unreviewed project imports and references to app-only declarations. Nested-source fixtures cover the recursive scan. Shared config parsing/model and hook-mask vocabulary are separated from app storage/report helpers. This is a source-level guard, not a separate Kotlin compile module or proof of reflective/third-party dependency isolation.
- [x] **P3-08** — Responsibilities reviewed. The boundary-driven extraction into `CanonicalConfigData` and `HookMaskData` has a concrete benefit; large screens and existing classifier tables were not split solely for line counts. Architecture guidance was updated in `lsposed/AGENTS.md`.
- [ ] **P3-09** — Device visual check pending: small display, landscape, large font, keyboard/bottom-sheet actions and reduced animations. No Android device was attached (`adb devices -l` returned an empty list). No density defect is claimed from source inspection.
- [x] **P3-10** — HTTP receives at most 32 KiB of request-head bytes, 64 headers, 8 KiB per line and 256 KiB of body, with one five-second monotonic deadline shared by head/body. Socket timeout follows the remaining budget. Receiving ends before operation dispatch, so this does not cancel an accepted root operation. Tests cover repeated/oversized headers, carriage-return inflation, slow reads, body deadline and a subsequent normal request.
- [x] **P2-11** — File exports derive Dashboard from the capture root snapshot and one saved diagnostic presentation, also used by the summary. Capture-owned gate, evidence and run ID remain unchanged. Dashboard assembly failure appends a section error and leaves the rest of the bundle available. Compilation, existing run/outcome tests and bundle golden validate contracts; full root capture remains a device check.
- [x] **P2-12** — The export sheet retains the full outcome and explains complete, partial and failed results in EN/RU/ZH. Partial archives keep Save/Share; failures retain their reason and retry path. Resource compilation/lint pass; interaction checks remain pending on device.
- [x] **P3-13** — Best-effort `beforeCapture` records available process state before logging/probes: timestamp, diagnostic summary, retained protection/messages, pending operation IDs and last phase/failure state. It performs no fresh root read and does not claim to be a previously viewed screen. It never blocks collection. Bundle golden covers the structure.
- [x] **P3-14** — Dashboard messages carry an explicit stable reason/parameter DTO and presentation language alongside existing text/severity/actions/help. Both file export and agent bridge consume the same Dashboard builder/classifier. Tests pin version/component parameters independent of translated text. Contract and golden are updated; additions remain within schema 2 under the existing compatibility policy.

## Verification

Run from the repository root with the configured Android/JDK/Rust environment:

```sh
ktlint 'lsposed/**/*.kt'
./lsposed/gradlew -p lsposed :app:testDebugUnitTest :app:detekt cpdCheck \
:app:lintDebug :app:assembleDebug -PvpnhideWarningsAsErrors=true
```

All checks above passed; 844 JVM tests completed with zero failures or skips.

Runtime claims are limited to JVM tests and successful APK compilation/packaging.
No APK, TalkBack, LSPosed or root/SELinux scenario was executed on a device.
25 changes: 23 additions & 2 deletions docs/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,29 @@ sanitizer for that app; `java: ["hook_name", ...]` enables only the named
LSPosed Java hooks for the app. App-hiding package visibility is controlled by
`appHiding`, not by the Java VPN hook list.

Inotify watches `/data/system/vpnhide_config.json`; package reinstall UID/appId
changes are picked up by a periodic fingerprint of `/data/system/packages.list`.
Packages sharing an appId use the **union** of their enabled Java hook lists.
A disabled Java role (or empty list) contributes no hooks; it does not veto
another package's selection. Binder cannot distinguish those packages at the
UID boundary, so the union applies to the whole group in every profile. Canonical
JSON still stores each package's intent. The Java hook dialog explains this when
the package inventory identifies a shared appId; the offline guide describes it
regardless of inventory availability.

Inotify invalidates the cache on canonical-config changes. Independently, a
one-second poll fingerprints **both** canonical JSON and `packages.list`, including
device/inode, size and nanosecond modification/change times. A load compares both
fingerprints before and after reading, and publishes only under the same
invalidation generation. A late invalidation cannot be overwritten by a reader.
Each call attempts at most two reads. If the sources keep changing (or cannot be
statted), it returns the previous coherent snapshot, or an empty fail-open policy
at startup; it leaves the cache expired so the next call retries. A stable missing
canonical file or explicit empty config replaces the previous policy. Read/parse errors retain the last successful policy (or fail open at startup)
and trigger another read after one second even if metadata did not change. A
blank, malformed or unsupported JSON file is an error; intentional reset is a
valid empty canonical object or removal of the canonical file. LSPosed telemetry
exports `policy_read_status=error` and `policy_read_error` until reading succeeds.
Telemetry uses same-directory atomic rename only; replacement failure preserves
the previous complete file and schedules another write without clearing counters.

---

Expand Down
6 changes: 3 additions & 3 deletions lsposed/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ model together:
| `ui/`, `checks/`, `generated/` | design system, the JNI probe binding, codegen |

What stays in the root package is the shared vocabulary both sides use —
`StorageConfig`, `ShellUtils`, `RootSnapshotCache`, `HookRegistry`,
`CanonicalConfigData`, `StorageConfig`, `ShellUtils`, `RootSnapshotCache`, `HookMaskData`, `HookRegistry`,
`DashboardData`, the agent bridge. Moving those buys import churn and nothing
else; they belong to no single feature.

Expand Down Expand Up @@ -54,7 +54,7 @@ The single most important thing about this module: `hook/` is loaded by LSPosed
**into `system_server`**; everything else runs in the app process. So `hook/`
carries no Compose, no Activity, no app resources — and nothing outside it may
touch `de.robv.android.xposed` (absent in the app process). The shared vocabulary
they both use (canonical-config parsing, `HookRegistry`, `LsposedStats`,
they both use (canonical-config parsing in `CanonicalConfigData`, mask vocabulary in `HookMaskData`, `LsposedStats`,
`LogTags`) stays in the root package. `HookPackageBoundaryTest` enforces both
directions, since `internal` is module-wide and the compiler will not.

Expand Down Expand Up @@ -132,7 +132,7 @@ directions, since `internal` is module-wide and the compiler will not.
or a pure transform of the fresh config; never save a cached full snapshot or
call a config/activation shell through `suExec`. Root outcome can be unknown:
preserve phase evidence and let the coordinator reconcile it.
- **`StorageConfig` / `ShellCommandBuilders`** — canonical JSON schema,
- **`CanonicalConfigData` / `StorageConfig` / `ShellCommandBuilders`** — shared canonical JSON models/parsing, app-side storage builders,
migration helpers, and root-safe file writes (`buildCanonicalConfigWriteCommand`
in `StorageConfig` wraps the generic `buildAtomicSystemDataRawWriteCommand` in
`ShellCommandBuilders`). The canonical JSON is the persistent
Expand Down
Loading
Loading