refactor: generalize optional feature settings

This commit is contained in:
okhsunrog 2026-08-13 22:30:15 +03:00
parent 8400a27bfb
commit e712e3b3ec
15 changed files with 47 additions and 49 deletions

View file

@ -106,20 +106,20 @@ pub fn read_canonical() -> Result<String> {
}
}
/// Read one reboot-gated kernel feature without waiting for PackageManager.
/// Boot loaders query features before loading their backend, so disabled
/// features never register their probes at all.
pub(crate) fn kernel_boot_feature_enabled(feature: &str) -> Result<bool> {
/// Read one optional feature without waiting for PackageManager. Kernel boot
/// loaders query their relevant features before loading the backend, so a
/// disabled load-time feature never registers its probes at all.
pub(crate) fn optional_feature_enabled(feature: &str) -> Result<bool> {
let config = parse_canonical(&read_canonical()?)?;
Ok(config.settings.kernel_boot_features.contains(feature))
Ok(config.settings.optional_features.contains(feature))
}
/// Optional boot hardening is never a prerequisite for the baseline backend.
/// A corrupt desired-state file therefore falls back to the disabled default;
/// late-start config delivery still reports the parse error and refuses a
/// partial snapshot.
pub(crate) fn kernel_boot_feature_enabled_or_default(feature: &str) -> bool {
match kernel_boot_feature_enabled(feature) {
pub(crate) fn optional_feature_enabled_or_default(feature: &str) -> bool {
match optional_feature_enabled(feature) {
Ok(enabled) => enabled,
Err(err) => {
eprintln!(
@ -251,7 +251,7 @@ pub(crate) fn load_kpm_boot() -> Result<KpmBootReport> {
));
}
let filesystem_hiding =
kernel_boot_feature_enabled_or_default(KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS);
optional_feature_enabled_or_default(OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS);
let client = match KpmClient::detect_outcome()? {
KpmClientDetection::Ready(client) => client,
KpmClientDetection::AwaitingAuthentication(_) => {
@ -310,8 +310,8 @@ fn activate_kpm_with_pm_wait(wait: PmReadyWait, conflict_is_error: bool) -> Resu
let canonical = read_canonical()?;
let filesystem_hiding = parse_canonical(&canonical)?
.settings
.kernel_boot_features
.contains(KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS);
.optional_features
.contains(OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS);
let wire = project_native_with_pm_wait(&canonical, NativeHookFamily::Kpm, wait)?;
// Re-check after the (possibly long) PackageManager wait: the .ko may have
// been loaded meanwhile, in which case we must not configure the KPM.

View file

@ -8,8 +8,8 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::{
APATCH_DIR, KPM_CTL_LOCK, KpmBootOutcome, KpmBootReport, PORTS_CHAIN4, PORTS_CHAIN6,
PORTS_STATUS_DIR, Result, activate_kmod_boot, activate_kpm_boot, activate_ports_recorded,
activate_zygisk_boot, kernel_boot_feature_enabled, kernel_boot_feature_enabled_or_default,
kmod_backend_present, load_kpm_boot, write_atomic,
activate_zygisk_boot, kmod_backend_present, load_kpm_boot, optional_feature_enabled,
optional_feature_enabled_or_default, write_atomic,
};
const KMOD_STATUS_DIR: &str = "/data/adb/vpnhide_kmod";
@ -198,7 +198,7 @@ pub fn boot_load_kmod() -> Result<()> {
let module_prop = read_module_properties(&module_dir.join("module.prop"));
let (kprobes, kretprobes) = read_kprobe_config();
let (filesystem_hiding, filesystem_config_exit, filesystem_config_error) =
match kernel_boot_feature_enabled(FILESYSTEM_BOOT_FEATURE) {
match optional_feature_enabled(FILESYSTEM_BOOT_FEATURE) {
Ok(enabled) => (enabled, i32::from(!enabled), String::new()),
Err(err) => (false, 2, err.to_string()),
};
@ -332,9 +332,7 @@ pub fn boot_load_kpm() -> Result<()> {
}
if Path::new(APATCH_DIR).is_dir() {
let filesystem_hiding = Some(kernel_boot_feature_enabled_or_default(
FILESYSTEM_BOOT_FEATURE,
));
let filesystem_hiding = Some(optional_feature_enabled_or_default(FILESYSTEM_BOOT_FEATURE));
log_android(
"vpnhide",
"kpm: APatch/FolkPatch runtime — deferring load to service activator",

View file

@ -14,7 +14,7 @@ use crate::{
is_app_uid, pm_list_packages, pm_list_users, wait_for_pm_ready,
};
pub const KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS: &str = Hook::FilesystemIfacePaths.name();
pub const OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS: &str = Hook::FilesystemIfacePaths.name();
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
@ -35,7 +35,7 @@ pub struct Settings {
#[serde(default)]
pub remember_superkey: bool,
#[serde(default)]
pub kernel_boot_features: BTreeSet<String>,
pub optional_features: BTreeSet<String>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]

View file

@ -172,7 +172,7 @@ fn kmod_projection_includes_the_optional_filesystem_hook() {
let cfg = parse_canonical(
r#"{
"version": 1,
"settings": { "kernelBootFeatures": ["filesystem_iface_paths"] },
"settings": { "optionalFeatures": ["filesystem_iface_paths"] },
"apps": { "com.example.full": { "native": true } }
}"#,
)
@ -181,8 +181,8 @@ fn kmod_projection_includes_the_optional_filesystem_hook() {
assert!(
cfg.settings
.kernel_boot_features
.contains(KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS)
.optional_features
.contains(OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS)
);
assert_eq!(
project_native_with_resolver_for_family(&cfg, &resolver, NativeHookFamily::Kmod),

View file

@ -41,7 +41,7 @@ This document defines four things:
It does **not** define the canonical JSON schema, package-to-UID projection,
native-backend selection, boot order, APatch credential persistence, or the
arguments used to load `.ko`/KPM artifacts. In particular,
`settings.kernelBootFeatures` and the `filesystem_hiding=1` loader argument are
`settings.optionalFeatures` and the `filesystem_hiding=1` loader argument are
not control-v2 records. See [storage.md](storage.md) for desired state and
activation, and [state.md](state.md) for paths, lifetimes, and boot sequencing.

View file

@ -20,7 +20,7 @@ The single managed desired-state file.
- Format: JSON object, `version: 1`, `debug: Boolean`, `apps: { package ->
roles }`, `settings.rememberSuperkey: Boolean`,
`settings.kernelBootFeatures: [feature name]`,
`settings.optionalFeatures: [feature name]`,
`settings.autoHideVpnServices: Boolean`, `settings.autoHideVpnName: Boolean`,
`settings.autoHiddenPackages: [package]`.
- Roles per package: `java`, `native` (`Boolean` or hook-name array),

View file

@ -97,7 +97,7 @@ Shape (illustrative):
},
"settings": {
"rememberSuperkey": false,
"kernelBootFeatures": []
"optionalFeatures": []
}
}
```
@ -140,12 +140,12 @@ selection; the native wire carries the resolved `hookmask`.
The `rememberSuperkey` boolean lives here (it is a preference, not a secret). The
superkey itself does **not** (§6).
### 2.2 Optional kernel boot features
### 2.2 Optional features
`settings.kernelBootFeatures` is a canonical **desired-state field**, not a
control-v2 record. It names expensive or experimental kernel capabilities that
must be selected before a backend installs its global hooks. Unknown names are
preserved so newer versions can extend the set without a schema migration.
`settings.optionalFeatures` is a canonical **desired-state field**, not a
control-v2 record. It names expensive or experimental capabilities whose
backend-specific activation is defined here. Unknown names are preserved so
newer versions can extend the set without a schema migration.
The current projection is:

View file

@ -147,7 +147,7 @@ or KPatch-Next `kpatch kpm ctl0`.
### Optional filesystem hook loader contract
Optional boot features are outside control v2. When canonical
`settings.kernelBootFeatures` contains `filesystem_iface_paths`, the production
`settings.optionalFeatures` contains `filesystem_iface_paths`, the production
activator passes the exact KPM load argument `filesystem_hiding=1`. When it is
disabled, the activator passes a null/absent argument; the explicit
`filesystem_hiding=0` spelling is also accepted as disabled but is not emitted by

View file

@ -100,7 +100,7 @@ static int nr_targets;
static uint32_t default_hookmask;
static uint32_t active_hook_mask;
/* Locked at module load: these globally hot hooks cannot be added or removed by
* a later ctl0 config. The canonical kernelBootFeatures setting is projected
* a later ctl0 config. The canonical optionalFeatures setting is projected
* into KPM load args by the activator. */
static bool filesystem_hiding_requested;

View file

@ -174,10 +174,10 @@ internal fun AppPickerScreen(
nativeBackendId = targets.displayNativeBackendId,
nativeHookFamily = nativeHookFamily,
filesystemHidingEnabled =
KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS in
OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS in
targets.canonicalConfig
?.settings
?.kernelBootFeatures
?.optionalFeatures
.orEmpty(),
portsInstalled = targets.portsModuleInstalled,
onToggle = { layer ->

View file

@ -1576,10 +1576,10 @@ internal suspend fun loadDashboardState(
filesystemHidingDashboardMessage(
desiredEnabled =
KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS in
OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS in
targetsSnapshot.canonicalConfig
?.settings
?.kernelBootFeatures
?.optionalFeatures
.orEmpty(),
sections = shellSnapshot,
res = res,

View file

@ -40,9 +40,9 @@ internal fun FilesystemHidingSettingsSection() {
targets
?.canonicalConfig
?.settings
?.kernelBootFeatures
?.optionalFeatures
.orEmpty()
val enabled = KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS in enabledFeatures
val enabled = OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS in enabledFeatures
val runtimeState =
remember(enabled, rootSnapshot) {
resolveFilesystemHidingState(
@ -167,11 +167,11 @@ private fun writeFilesystemHidingSetting(enabled: Boolean): Int {
base.copy(
settings =
base.settings.copy(
kernelBootFeatures =
optionalFeatures =
if (enabled) {
base.settings.kernelBootFeatures + KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS
base.settings.optionalFeatures + OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS
} else {
base.settings.kernelBootFeatures - KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS
base.settings.optionalFeatures - OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS
},
),
)

View file

@ -6,7 +6,7 @@ import org.json.JSONObject
internal const val CANONICAL_CONFIG_FILE = "/data/system/vpnhide_config.json"
internal const val SUPERKEY_FILE = "/data/adb/vpnhide/superkey"
internal val KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS = HookIds.Hook.FILESYSTEM_IFACE_PATHS.hookName
internal val OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS = HookIds.Hook.FILESYSTEM_IFACE_PATHS.hookName
internal data class CanonicalConfig(
val version: Int = 1,
@ -18,7 +18,7 @@ internal data class CanonicalConfig(
internal data class CanonicalSettings(
val rememberSuperkey: Boolean = false,
val kernelBootFeatures: Set<String> = emptySet(),
val optionalFeatures: Set<String> = emptySet(),
val autoHideVpnServices: Boolean = true,
val autoHideVpnName: Boolean = false,
val autoHideExcludedPackages: Set<String> = emptySet(),
@ -146,7 +146,7 @@ internal fun parseCanonicalConfig(raw: String): CanonicalConfig? {
settings =
CanonicalSettings(
rememberSuperkey = settingsJson?.optBoolean("rememberSuperkey", defaultSettings.rememberSuperkey) == true,
kernelBootFeatures = parseStringSet(settingsJson?.optJSONArray("kernelBootFeatures")),
optionalFeatures = parseStringSet(settingsJson?.optJSONArray("optionalFeatures")),
autoHideVpnServices =
settingsJson?.optBoolean("autoHideVpnServices", defaultSettings.autoHideVpnServices)
?: defaultSettings.autoHideVpnServices,
@ -404,8 +404,8 @@ internal fun canonicalConfigJson(config: CanonicalConfig): String =
append(" \"rememberSuperkey\": ")
append(config.settings.rememberSuperkey)
append(",\n")
append(" \"kernelBootFeatures\": [")
config.settings.kernelBootFeatures.toSortedSet().forEachIndexed { index, feature ->
append(" \"optionalFeatures\": [")
config.settings.optionalFeatures.toSortedSet().forEachIndexed { index, feature ->
if (index != 0) append(", ")
appendJsonString(feature)
}

View file

@ -9,8 +9,8 @@ class FilesystemHidingDataTest {
@Test
fun `filesystem hiding remains opt in by default`() {
assertFalse(
KERNEL_BOOT_FEATURE_FILESYSTEM_IFACE_PATHS in
CanonicalSettings().kernelBootFeatures,
OPTIONAL_FEATURE_FILESYSTEM_IFACE_PATHS in
CanonicalSettings().optionalFeatures,
)
}

View file

@ -64,7 +64,7 @@ class StorageConfigTest {
},
"settings": {
"rememberSuperkey": true,
"kernelBootFeatures": ["filesystem_iface_paths", "future_feature"],
"optionalFeatures": ["filesystem_iface_paths", "future_feature"],
"autoHideVpnServices": false,
"autoHideVpnName": true,
"autoHideExcludedPackages": ["com.vpn.false_positive"],
@ -79,7 +79,7 @@ class StorageConfigTest {
assertEquals(
CanonicalSettings(
rememberSuperkey = true,
kernelBootFeatures = setOf("filesystem_iface_paths", "future_feature"),
optionalFeatures = setOf("filesystem_iface_paths", "future_feature"),
autoHideVpnServices = false,
autoHideVpnName = true,
autoHideExcludedPackages = setOf("com.vpn.false_positive"),