Android properties look deceptively simple from a shell: getprop prints a value and setprop changes one. Underneath, the read path is optimized for many processes, while the write path crosses a protected service boundary. That asymmetry is the key to understanding why a property may be visible yet unwritable—and why adding one to a product is an API and SELinux design task, not merely editing build.prop.

Properties are strings with enforced contracts

  • The underlying store is a dictionary of property names and string values.

  • property_contexts can enforce bool, signed/unsigned integer, double, enum, or string syntax when values are set.

  • Generated Sysprop APIs convert the string representation to typed C++, Java, or Rust values.

  • A missing property, an empty value, and a default selected by an API are different states.

  • Properties are lightweight global configuration/state; misuse creates hidden coupling, compatibility failures, and security vulnerabilities.

Read path and write path

Android property data flowtext
Build / boot inputs
  partition build.prop + boot parameters + init rules
                 init property service
                  │ validate + SELinux
          ┌───────┴────────┐
          ▼                ▼
  property memory       persistent storage
  mapped by readers     for persist.* values
  libc / libbase / generated Sysprop getters
 
Writer process ──socket request──> init property service

The optimization is deliberately one-sided

  • Frequent reads avoid conventional IPC by consulting mapped property data.

  • Applications still need permission/visibility to read protected property contexts.

  • Writes are serialized through the property service so policy and mutation rules cannot be bypassed.

  • Property change notifications/triggers occur only after an accepted update.

  • Persistent properties require storage availability and special handling during boot.

The old Android source story no longer fits

  • Modern AOSP uses C++ init/property-service code, not the historical init.c and hard-coded path list shown in early Android releases.

  • Treble split platform and vendor partitions and made cross-partition property stability a compatibility concern.

  • Property files are generated/loaded per partition rather than treating /system/build.prop as the whole source of truth.

  • SELinux property contexts and neverallow rules govern who can read and write.

  • Android 10+ provides .sysprop schemas and generated APIs for stable cross-boundary access.

1. Inspect properties from an authorized device

Host with adb and authorized test devicebash
adb shell getprop ro.build.version.release
adb shell getprop ro.product.cpu.abilist
adb shell getprop | head -n 20
The first commands print selected read-only build/device values. The final command previews property names and values visible to the shell domain.

getprop is observation, not a public application API

  • adb shell executes under the device shell security domain, not with arbitrary app privileges.

  • ro.* conventionally identifies values set once during boot; do not assume every prefix alone defines policy.

  • Property availability and names vary by Android release, product, partition, and vendor implementation.

  • A property visible to shell may be hidden from untrusted apps or unsupported as a compatibility contract.

  • Property dumps can reveal build, radio, hardware, debugging, or operational data; sanitize logs before sharing.

2. Do not experiment with setprop on production devices

Disposable userdebug/eng test device onlybash
adb shell setprop debug.example.feature.enabled true
adb shell getprop debug.example.feature.enabled
If policy permits the shell domain to create/write this property, the read returns true. Production builds commonly reject undeclared or unauthorized writes.

Risk level: caution. Review the command before running it.

A successful shell write proves very little

  • setprop requests a runtime mutation; the property service makes the final decision.

  • Userdebug/eng policy and root access differ materially from production user builds.

  • An undeclared property may fail due to property-context mapping or SELinux denial.

  • debug.* is not persistent across reboot by default and must never carry secrets.

  • Never change security, radio, encryption, boot, or vendor properties on a user device merely to test an article.

Property naming communicates ownership and lifecycle

  • AOSP recommends structured snake-case names such as [{prefix}.]{group}[.{subgroup}]*.{name}[.{type}].

  • Omit a prefix for ordinary runtime properties, use ro for set-once data, or persist only when reboot persistence is justified.

  • Suffixes such as .enabled, .config, or .timeoutmillis clarify type/intent.

  • Choose established subsystem groups where possible instead of vague sys, system, or default buckets.

  • Property names become compatibility surface; renaming or changing meaning can break system/vendor/Mainline consumers.

Partition ownership after Treble

  • Platform properties belong to system-side components.

  • Vendor and ODM properties belong to their respective hardware/product partitions.

  • The process that sets a partition’s build properties runs under the corresponding init context, such as vendor_init for vendor/ODM inputs.

  • A property read across system/vendor or Mainline boundaries must remain stable.

  • Do not use a property as an undocumented side channel between a HAL and its clients; expose hardware-owned configuration through the appropriate stable HAL API.

3. Define a property context and type

system/sepolicy/.../property_contextstext
audio.demo.volume.level u:object_r:audio_demo_prop:s0 exact int
audio.demo.feature.enabled u:object_r:audio_demo_prop:s0 exact bool

Context mapping is both validation and security plumbing

  • The exact property name maps to the SELinux type audio_demo_prop.

  • exact prevents the entry from unintentionally matching a namespace prefix.

  • int and bool make the property service reject malformed writes.

  • Define the property type with the narrow platform/vendor accessibility macro appropriate to ownership.

  • Grant only required read/write rules and preserve neverallow constraints; never weaken SELinux globally to make a set succeed.

Exact and prefix mappings

property_contexts matching examplestext
audio.demo.status u:object_r:audio_demo_status_prop:s0 exact enum on off unknown
audio.demo.debug. u:object_r:audio_demo_debug_prop:s0 prefix string

Exact beats prefix when both match

  • The status property accepts only the declared enum values.

  • The prefix entry covers properties beginning with audio.demo.debug..

  • Broad prefixes expand the number of names sharing policy; use them only for a coherent controlled family.

  • When an exact and prefix rule conflict, AOSP specifies that the exact entry takes precedence.

  • Run property/sepolicy build checks because text order alone is not a safe policy design.

4. Set build-time values through product configuration

device/<vendor>/<product>/device.mkmakefile
PRODUCT_SYSTEM_PROPERTIES += \
    audio.demo.feature.enabled=true
 
PRODUCT_VENDOR_PROPERTIES += \
    ro.audio.demo.hardware.config=codec_v2

The partition variable chooses where the value is built

  • Product property assignments are emitted into the selected partition’s build property data.

  • Use = for a definitive assignment; AOSP also supports ?= defaults with defined precedence behavior.

  • Place a property in the partition that owns it and matches its SELinux/property contract.

  • A read-only ro.* value is intended to be established during boot/build flow, not changed later.

  • Duplicate conflicting assignments should fail review/build rather than depend on accidental ordering.

5. React to properties in init scripts

init.audio-demo.rctext
on property:audio.demo.feature.enabled=true
    start audio_demo
 
on property:audio.demo.feature.enabled=false
    stop audio_demo

Property triggers translate state into init actions

  • The action runs when the property transition matches the trigger and during applicable boot property-trigger evaluation.

  • start and stop address a service declared in init configuration.

  • The property writer still needs SELinux set permission and the service/action needs its own correct domain policy.

  • Design idempotent transitions and avoid loops where actions repeatedly rewrite their own trigger property.

  • Use explicit service lifecycle APIs where a property-triggered global state machine would be fragile.

Substitution and defaults inside init

init.audio-demo.rctext
on property:audio.demo.feature.enabled=true
    write /sys/devices/platform/demo/level ${audio.demo.volume.level:-50}

The default applies when the property is empty/unset

  • ${property} substitutes a property value in supported init contexts.

  • ${property:-default} supplies a default when no usable value is present.

  • Writing sysfs requires correct node ownership, lifecycle, range checking, and SELinux access.

  • A typed property prevents non-integer text but does not enforce a device-specific numeric range by itself.

  • A dedicated service/HAL is preferable for complex validation, errors, transactions, or hardware ownership.

6. Define stable properties with a Sysprop schema

AudioDemoProperties.sysproptext
owner: Platform
module: "android.sysprop.AudioDemoProperties"
 
prop {
    api_name: "feature_enabled"
    type: Boolean
    prop_name: "audio.demo.feature.enabled"
    scope: Public
    access: ReadWrite
}
 
prop {
    api_name: "hardware_config"
    type: String
    prop_name: "ro.audio.demo.hardware.config"
    scope: Public
    access: Readonly
}

The schema replaces magic strings at call sites

  • owner records whether Platform, Vendor, or ODM owns the contract.

  • module becomes a Java class and C++ namespace; Rust bindings are generated for supported consumers.

  • type produces typed optional values instead of manual string parsing.

  • Readonly emits a getter; writable access emits getter/setter APIs, subject to runtime property policy.

  • ro.* properties cannot be declared read-write.

  • Public scope and generated API stability do not grant every process SELinux permission automatically.

7. Build the generated API library

Android.bptext
sysprop_library {
    name: "AudioDemoProperties",
    srcs: ["AudioDemoProperties.sysprop"],
    property_owner: "Platform",
}

Soong generates language-specific clients

  • The source schema is compiled into Java, C++, and Rust-facing library variants as supported.

  • property_owner must agree with the schema and partition design.

  • Consumers depend on the generated library rather than importing a private header or reflecting on hidden Java APIs.

  • Build-time API checks detect incompatible stable-sysprop changes.

  • Generated functions still use the underlying property mechanism and enforce its runtime access policy.

8. Read through generated C++ APIs

audio_demo_main.cppcpp
#include <AudioDemoProperties.sysprop.h>
 
using android::sysprop::AudioDemoProperties;
 
bool enabled = AudioDemoProperties::feature_enabled().value_or(false);
std::string config =
    AudioDemoProperties::hardware_config().value_or("unknown");

Optional values force a missing-value decision

  • The generated getter returns an optional typed value.

  • value_or(false) and value_or("unknown") define explicit local defaults.

  • No property-name string is duplicated in the consumer.

  • The header/library name is generated by the sysprop build and must be linked in the Soong module.

  • Defaults should be safe and compatible—not silently mask a required factory configuration.

Low-level native APIs are fallback tools

legacy_native_reader.cppcpp
#include <android-base/properties.h>
 
const bool enabled =
    android::base::GetBoolProperty("audio.demo.feature.enabled", false);
const std::string config = android::base::GetProperty(
    "ro.audio.demo.hardware.config", "unknown");

libbase is safer than raw buffers, but still stringly named

  • GetBoolProperty parses supported boolean encodings and applies an explicit default.

  • GetProperty returns a string/default without manually allocating a PROP_VALUE_MAX buffer.

  • AOSP recommends generated Sysprop APIs where possible, especially across stable boundaries.

  • Host binaries can use appropriate libbase property wrappers in supported build contexts.

  • Never let untrusted input select arbitrary property names or treat properties as secret storage.

Java and ordinary applications

  • Platform Java code can consume generated Sysprop classes when its build dependency and API scope permit.

  • android.os.SystemProperties exists for platform/internal Java code but is not an ordinary public Android SDK API.

  • Third-party apps must not depend on reflection into hidden property APIs.

  • Use Build fields and public manager APIs for supported device/platform facts.

  • Use app-owned storage/configuration for app state, respecting backup, multi-user, direct-boot, privacy, and lifecycle semantics.

Persistent properties are special

  • persist.* indicates that accepted values should survive reboot.

  • Persistent data loads only after the relevant data storage is available.

  • A write can cause durable storage I/O; do not use persistent properties as a high-frequency telemetry channel.

  • Define ownership, default, migration, corruption/reset behavior, privacy, and factory-reset expectations.

  • Persist only when reboot survival is necessary; prefer the owning service’s structured storage for complex state.

Property value/name constraints

  • Use generated or libbase APIs instead of hard-coding Bionic buffer limits.

  • Bionic retains PROP_VALUE_MAX; historical and current behavior around long read-only values differs, so validate against the target branch/API.

  • Property names and values are not arbitrary databases or blobs.

  • Large, structured, frequently changing, transactional, or per-user data belongs elsewhere.

  • Types in property_contexts validate representation, not business meaning, units, dependencies, or ranges.

Watch a property during debugging

Host with adb and authorized test devicebash
adb shell getprop -T audio.demo.feature.enabled
adb shell getprop audio.demo.feature.enabled
adb shell dmesg | grep -i "avc:.*property"
Where supported, `getprop -T` prints the declared type; the value query prints current state. Kernel logs may show SELinux denials only when the device/build permits access.

A denial is evidence, not a request to disable SELinux

  • Command options vary by Android/toybox release; check adb shell getprop --help on the target.

  • Production devices may restrict dmesg, and audit records may appear through other authorized logging paths.

  • Capture source/target context, property type, operation, build fingerprint, and policy version.

  • Fix property ownership/context and the narrow allow rule; never switch SELinux permissive in a production solution.

  • Do not publish full property or audit dumps without removing device/user/vendor-sensitive values.

When a property write fails

  • Permission denied / SELinux denial: caller lacks set permission for the mapped property type.

  • Invalid argument/value: name, length, declared bool/int/enum syntax, or other validation failed.

  • Read-only property: an ro.*/write-once value was already established or schema disallows setters.

  • Property has no context: the name is undeclared/mapped to an unsuitable default and modern policy rejects it.

  • Works on userdebug, fails on user: engineering-only policy/root masked a production access violation.

  • Value changes but service does not react: trigger spelling/value, init import, boot ordering, or service definition is wrong.

  • Vendor can read but upgrade breaks: cross-partition magic-string contract was not defined/stabilized as a Sysprop API.

  • Persisted value disappears: storage/boot load, property namespace, policy, factory reset, or write timing is wrong.

Choose another configuration mechanism when appropriate

  • Use a stable AIDL HAL method when hardware is the source of truth.

  • Use system config XML for static structured configuration with a schema.

  • Use resource overlays for product-specific framework/resource values.

  • Use DeviceConfig or the appropriate platform configuration service for supported dynamically managed flags.

  • Use files/databases owned by a service for structured, transactional, high-volume, sensitive, or per-user state.

  • Use Binder/AIDL callbacks when clients need authenticated operations, errors, subscriptions, and lifecycle semantics.

Production design checklist

  • The property is genuinely the right mechanism and has one documented owner.

  • Name, prefix/lifecycle, partition, type, default, units, range, read/write actors, and persistence are specified.

  • SELinux property type, exact/prefix context, allow and neverallow policy use least privilege.

  • Cross-partition/Mainline access is defined through a stable generated Sysprop API.

  • Build-time assignments land in the owning partition and runtime setters are authorized.

  • Init triggers are idempotent, loop-free, and tested across boot/order/restart/factory-reset paths.

  • User and userdebug builds, OTA compatibility, missing/invalid values, and security/privacy logging are tested.

  • No third-party app, HAL side channel, secret, per-user state, or high-frequency data abuses the global property store.

Primary references