Skip to content

Property Contexts

从 property_contexts 文本规则、Soong 构建与 namespace 校验,追踪到 property_info Trie、init 权限检查、值类型校验和属性文件访问控制。

基于android-17.0.0_r1
AndroidSELinuxproperty_contextsinitsystem_properties源码阅读

Property Contexts ​

本文面向已经读过 安全上下文、TE规则语法、M4预处理、file contexts 和 策略语法工作台 的读者。前文已经说明了 type、attribute 和普通文件标签;本篇专门回答另一个容易误判的问题:属性名怎样找到 target Context,谁在什么时候检查 set,值类型约束在哪里执行,属性值又怎样被读取。

本文不把 ro.*、persist.* 当作 SELinux 规则本身,也不把属性前缀分类表当作实现。Android 17 的真实实现包含三层不同数据:源码中的文本条目、构建生成的序列化 property_info Trie,以及内核保护的属性共享内存文件。读完后,读者应能沿着一个属性名从 property_contexts 走到 PropertyInfoArea::GetPropertyInfo()、init::CheckPermissions() 和最终的 property_service set 或 file read 判定,并能解释 namespace、类型、持久化和特殊 ctl.* 路径各自失败在哪里。

1. 三层数据 ​

1.1 文本规则 ​

property_contexts 是属性名到两个元数据的映射:SELinux Context 和可选的值类型字符串。它不是 allow 规则,也不直接保存当前属性值。下面几行来自 Android 17 平台文件:

源码文件:system/sepolicy/private/property_contexts

text
sys.                    u:object_r:system_prop:s0
sys.powerctl            u:object_r:powerctl_prop:s0
wrap.                   u:object_r:zygote_wrap_prop:s0 prefix string
suspend.max_sleep_time_millis u:object_r:suspend_prop:s0 exact uint
fastbootd.protocol      u:object_r:fastbootd_protocol_prop:s0 exact enum usb tcp
*                       u:object_r:default_prop:s0

第一列是匹配键,第二列是 Context,exact/prefix 和类型字段改变的是解析方式及值校验。最后的 * 是属性信息 Trie 的默认 Context,不等于“所有域都能设置”。能否设置仍由调用者 Context、target Context 和 policy 中的 property_service set 决定。

1.2 二进制Trie ​

init 和 libc 不在每次请求时重新扫描文本文件。构建工具把多份文本解析为内存布局稳定的 Trie,运行时从 /dev/__properties__/property_info 映射只读区域。该区域保存 Context、类型字符串、节点、前缀和精确匹配的偏移量。

源码文件:system/core/property_service/libpropertyinfoparser/include/property_info_parser/property_info_parser.h

cpp
struct PropertyEntry {
  uint32_t name_offset;
  uint32_t namelen;
  uint32_t context_index;
  uint32_t type_index;
};

struct TrieNodeInternal {
  uint32_t property_entry;
  uint32_t num_child_nodes;
  uint32_t child_nodes;
  uint32_t num_prefixes;
  uint32_t prefix_entries;
  uint32_t num_exact_matches;
  uint32_t exact_match_entries;
};

这里的 index 不是 SELinux type 数字 ID,而是序列化区域中的字符串索引。构建器会把重复 Context 和类型字符串放入集合,只在节点中保存偏移;因此运行时 lookup 得到的仍是字符串,随后由 init 交给 SELinux 检查。

1.3 属性存储 ​

属性值由 bionic 的 system properties 区域提供读写接口。init 是通常唯一拥有写入权限的进程,其他进程通过 __system_property_find() 和读取回调访问共享区域。SELinux 对读取的保护对象是属性文件或属性区域的 file type,而不是 property_contexts 文本本身。

源码文件:bionic/libc/include/sys/system_properties.h

c
const prop_info* __system_property_find(const char* __name);
int __system_property_read_callback(const prop_info* __pi,
                                    void (*__callback)(void* __cookie,
                                                       const char* __name,
                                                       const char* __value,
                                                       uint32_t __serial),
                                    void* __cookie);

/* 只有拥有 property area 写权限的进程通常调用这些接口。 */
int __system_property_add(const char* __name, unsigned int __name_length,
                          const char* __value, unsigned int __value_length);
int __system_property_update(prop_info* __pi, const char* __value,
                             unsigned int __value_length);

因此要区分两个消费者:init 用 property_info 得到 target Context 来检查设置请求;普通进程读取属性值时,内核通过属性区域文件的 SELinux label 决定它能否读映射页。前者解决“能否 set”,后者解决“能否 read”。

2. 文本格式 ​

2.1 字段解析 ​

Android 17 的解析器不把空白数量当作语义。它取一行的 property、Context、匹配操作和剩余类型字段;旧格式可以没有操作和类型,新格式要求 prefix 或 exact 时则会拒绝其他操作词。

源码文件:system/core/property_service/libpropertyinfoserializer/property_info_file.cpp

cpp
bool ParsePropertyInfoLine(const std::string& line, bool require_prefix_or_exact,
                           PropertyInfoEntry* out, std::string* error) {
  auto tokenizer = SpaceTokenizer(line);

  auto property = tokenizer.GetNext();
  if (property.empty()) {
    *error = "Did not find a property entry in '" + line + "'";
    return false;
  }

  auto context = tokenizer.GetNext();
  if (context.empty()) {
    *error = "Did not find a context entry in '" + line + "'";
    return false;
  }

  auto match_operation = tokenizer.GetNext();
  auto type_strings = std::vector<std::string>{};
  auto type = tokenizer.GetNext();
  while (!type.empty()) {
    type_strings.emplace_back(type);
    type = tokenizer.GetNext();
  }

  bool exact_match = false;
  if (match_operation == "exact") {
    exact_match = true;
  } else if (match_operation != "prefix" && match_operation != "" &&
             require_prefix_or_exact) {
    *error = "Match operation '" + match_operation +
             "' is not valid: must be either 'prefix' or 'exact'";
    return false;
  }

  if (!type_strings.empty() && !IsTypeValid(type_strings)) {
    *error = "Type '" + Join(type_strings, " ") + "' is not valid";
    return false;
  }

  *out = {property, context, Join(type_strings, " "), exact_match};
  return true;
}

prefix 本身不会设置 exact_match,而没有操作词的旧条目也按非 exact 处理。解析器只负责语法和字段归一化,不验证 Context 是否为 policy 中的 property_type;后一个责任由 property_info_checker 承担。

2.2 匹配词边界 ​

属性名存在三种容易混淆的形式:以点结尾的分段前缀、没有点结尾的非分段前缀,以及 exact 精确项。

源码文件:system/core/property_service/libpropertyinfoserializer/trie_builder.cpp

cpp
auto name_pieces = Split(name, ".");

bool ends_with_dot = false;
if (name_pieces.back().empty()) {
  ends_with_dot = true;
  name_pieces.pop_back();
}

while (name_pieces.size() > 1) {
  auto child = current_node->FindChild(name_pieces.front());
  if (child == nullptr) {
    child = current_node->AddChild(name_pieces.front());
  }
  current_node = child;
  name_pieces.erase(name_pieces.begin());
}

if (exact) {
  if (!current_node->AddExactMatchContext(name_pieces.front(), context, type)) {
    *error = "Duplicate exact match detected for '" + name + "'";
    return false;
  }
} else if (!ends_with_dot) {
  if (!current_node->AddPrefixContext(name_pieces.front(), context, type)) {
    *error = "Duplicate prefix match detected for '" + name + "'";
    return false;
  }
} else {
  auto child = current_node->FindChild(name_pieces.front());
  if (child == nullptr) child = current_node->AddChild(name_pieces.front());
  child->set_context(context);
  child->set_type(type);
}

例如 persist.sys. 会沿 persist、sys 子节点匹配后代;persist.sys.safemode 没有点时是一个非分段前缀,它也可能匹配 persist.sys.safemode_extra;加上 exact 才只命中完整名字。这个差异不能用“最长前缀”四个字代替,因为点分段和非分段前缀在 Trie 中存储位置不同。

2.3 类型字段 ​

序列化器允许的类型集合是固定的:string、bool、int、uint、double、size,以及至少包含一个枚举值的 enum。

源码文件:system/core/property_service/libpropertyinfoserializer/property_info_file.cpp

cpp
bool IsTypeValid(const std::vector<std::string>& type_strings) {
  if (type_strings.empty()) return false;

  if (type_strings[0] == "enum") {
    return type_strings.size() > 1;
  }

  if (type_strings.size() != 1) return false;

  static const char* const no_parameter_types[] = {
      "string", "bool", "int", "uint", "double", "size"};
  for (const auto& type : no_parameter_types) {
    if (type_strings[0] == type) return true;
  }
  return false;
}

这一步只判断格式,例如 enum usb tcp 合法、enum 不合法;它还没有判断属性设置者是否有权限,更不会把字符串自动转换成整数。值的语义校验在 init 的 CheckType() 中再次执行。

3. 匹配算法 ​

3.1 节点遍历 ​

运行时 lookup 先按点分隔的 property pieces 在 Trie 中进行二分查找。每到一个节点,先保存该节点的默认 Context/type,再检查节点上登记的前缀;如果还能找到下一个点分隔子节点就继续,否则在叶节点检查 exact 列表,最后再检查非点分隔前缀。

源码文件:system/core/property_service/libpropertyinfoparser/property_info_parser.cpp

cpp
void PropertyInfoArea::GetPropertyInfoIndexes(const char* name,
                                              uint32_t* context_index,
                                              uint32_t* type_index) const {
  uint32_t return_context_index = ~0u;
  uint32_t return_type_index = ~0u;
  const char* remaining_name = name;
  auto trie_node = root_node();

  while (true) {
    const char* sep = strchr(remaining_name, '.');

    if (trie_node.context_index() != ~0u) {
      return_context_index = trie_node.context_index();
    }
    if (trie_node.type_index() != ~0u) {
      return_type_index = trie_node.type_index();
    }

    CheckPrefixMatch(remaining_name, trie_node,
                     &return_context_index, &return_type_index);

    if (sep == nullptr) break;

    const uint32_t substr_size = sep - remaining_name;
    TrieNode child_node;
    if (!trie_node.FindChildForString(remaining_name, substr_size, &child_node)) {
      break;
    }

    trie_node = child_node;
    remaining_name = sep + 1;
  }

  for (uint32_t i = 0; i < trie_node.num_exact_matches(); ++i) {
    if (!strcmp(c_string(trie_node.exact_match(i)->name_offset), remaining_name)) {
      if (context_index != nullptr) {
        *context_index = trie_node.exact_match(i)->context_index != ~0u
                ? trie_node.exact_match(i)->context_index : return_context_index;
      }
      if (type_index != nullptr) {
        *type_index = trie_node.exact_match(i)->type_index != ~0u
                ? trie_node.exact_match(i)->type_index : return_type_index;
      }
      return;
    }
  }

  CheckPrefixMatch(remaining_name, trie_node,
                   &return_context_index, &return_type_index);
  if (context_index != nullptr) *context_index = return_context_index;
  if (type_index != nullptr) *type_index = return_type_index;
}

源码中的 return_context_index 和 return_type_index 是沿途继承的状态。一个 exact 条目可以只覆盖 Context 或只覆盖 type,未覆盖的字段继续使用更早保存的值。因此“精确匹配优先”并不意味着 exact 条目必须重复写全所有元数据。

3.2 前缀顺序 ​

节点上的前缀数组按长度从长到短保存,CheckPrefixMatch() 顺序扫描,命中第一个后立即返回。

源码文件:system/core/property_service/libpropertyinfoparser/property_info_parser.cpp

cpp
void PropertyInfoArea::CheckPrefixMatch(const char* remaining_name,
                                        const TrieNode& trie_node,
                                        uint32_t* context_index,
                                        uint32_t* type_index) const {
  const uint32_t remaining_name_size = strlen(remaining_name);
  for (uint32_t i = 0; i < trie_node.num_prefixes(); ++i) {
    auto prefix_len = trie_node.prefix(i)->namelen;
    if (prefix_len > remaining_name_size) continue;

    if (!strncmp(c_string(trie_node.prefix(i)->name_offset),
                 remaining_name, prefix_len)) {
      if (trie_node.prefix(i)->context_index != ~0u) {
        *context_index = trie_node.prefix(i)->context_index;
      }
      if (trie_node.prefix(i)->type_index != ~0u) {
        *type_index = trie_node.prefix(i)->type_index;
      }
      return;
    }
  }
}

构建器必须先把“更具体”的同节点前缀排在前面,否则第一个命中会遮蔽后面的规则。点结尾前缀通常落在更深的节点,非点前缀则放在当前叶节点的 prefix 数组中;这也是测试同时覆盖 persist.dot_prefix. 和 persist.non_dot_prefix 的原因。

3.3 默认值 ​

BuildTrie() 接收 default Context 和 default type。Android 的检查器使用 u:object_r:default_prop:s0 与 \s* 作为默认值,因此没有显式条目的属性仍能得到一个可查询结果;但是否允许读取仍取决于 default_prop 的 policy 权限。

源码文件:system/core/property_service/property_info_checker/property_info_checker.cpp

cpp
if (!BuildTrie(property_info_entries,
               "u:object_r:default_prop:s0", "\\s*",
               &serialized_contexts, &build_trie_error)) {
  std::cerr << "Unable to serialize property contexts: "
            << build_trie_error << std::endl;
  return -1;
}

\s* 是检查器内部的默认 type 字符串,不是文本文件中必须写出的属性值类型。对平台构建生成的运行时区域,默认 type 会由调用方提供给 serializer;阅读代码时要同时看调用者和测试 fixture,不能根据一处默认值推断所有产物都相同。

4. 分区构建 ​

4.1 模块来源 ​

system/sepolicy/contexts/Android.bp 为平台、system_ext、product、vendor 和 odm 分别声明 property_contexts module。与 file contexts 不同,vendor/odm property contexts 不使用 fc_sort,而是保留规则输入给 property serializer 和 namespace checker。

源码文件:system/sepolicy/contexts/Android.bp

make
property_contexts {
    name: "plat_property_contexts",
    defaults: ["contexts_flags_defaults"],
    srcs: [":property_contexts_files{.plat_private}"],
}

property_contexts {
    name: "system_ext_property_contexts",
    defaults: ["contexts_flags_defaults"],
    srcs: [":property_contexts_files{.system_ext_private}"],
    system_ext_specific: true,
}

property_contexts {
    name: "vendor_property_contexts",
    defaults: ["contexts_flags_defaults"],
    srcs: [
        ":property_contexts_files{.plat_vendor}",
        ":property_contexts_files{.vendor}",
        ":property_contexts_files{.reqd_mask}",
    ],
    soc_specific: true,
}

property_contexts {
    name: "odm_property_contexts",
    defaults: ["contexts_flags_defaults"],
    srcs: [":property_contexts_files{.odm}"],
    device_specific: true,
}

同一组 module 还声明 recovery 变体,例如 vendor_property_contexts.recovery 使用相同 stem。分区 module 的安装属性决定哪些文件进入对应镜像;运行时读取哪些文件由 property area 初始化代码决定,不是 init 每次 set 时遍历五个目录。

4.2 M4展开 ​

所有 contexts module 共用 buildGeneralContexts():为每个输入插入 newline,调用带 --fatal-warnings -s 的 m4,按属性移除注释,最后仅在 module 配置开启时执行 fc_sort。property contexts 走同一通用函数,但默认不会设置 Fc_sort。

源码文件:system/sepolicy/build/soong/selinux_contexts.go

go
func (m *selinuxContextsModule) buildGeneralContexts(
        ctx android.ModuleContext, inputs android.Paths) android.Path {
    builtContext := pathForModuleOut(ctx, ctx.ModuleName()+"_m4out")
    rule := android.NewRuleBuilder(pctx, ctx)
    rule.SandboxDisabled()

    newlineFile := pathForModuleOut(ctx, "newline")
    rule.Command().Text("echo").FlagWithOutput("> ", newlineFile)
    rule.Temporary(newlineFile)

    var inputsWithNewline android.Paths
    for _, input := range inputs {
        inputsWithNewline = append(inputsWithNewline, input, newlineFile)
    }

    flags := m.getBuildFlags(ctx)
    rule.Command().
        Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
        Text("--fatal-warnings -s").
        FlagForEachArg("-D", ctx.DeviceConfig().SepolicyM4Defs()).
        Flag(boardApiLevelToM4Macro(ctx, m.properties.Board_api_level)).
        Flags(flagsToM4Macros(flags)).
        Inputs(inputsWithNewline).
        FlagWithOutput("> ", builtContext)

    if proptools.Bool(m.properties.Remove_comment) {
        rule.Temporary(builtContext)
        remove_comment_output := pathForModuleOut(
                ctx, ctx.ModuleName()+"_remove_comment")
        rule.Command().
            Text("sed -e 's/#.*$//' -e '/^$/d'").
            Input(builtContext).
            FlagWithOutput("> ", remove_comment_output)
        builtContext = remove_comment_output
    }

    /* fc_sort branch omitted: property_contexts modules leave it disabled. */
    rule.DeleteTemporaryFiles()
    rule.Build("selinux_contexts", "building contexts: "+m.Name())
    return builtContext
}

newline 文件的作用是把相邻输入文件隔开,避免上一个文件没有末尾换行时把下一文件第一条规则拼接到同一行。Remove_comment 的 sed 发生在 m4 之后,因此 m4 注释和展开输出都能被清理;这一步不会改变 property key 的匹配语义。

4.3 命名空间 ​

Android Q 及以后,vendor/odm module 在 Soong 中额外调用 check_prop_prefix。允许的 property prefix 与允许的 Context 名字前缀分别来自 VTS Treble sysprop 约定;shipping API 低于 R 时还保留 persist.camera. 兼容例外。

源码文件:system/sepolicy/build/soong/selinux_contexts.go

go
allowedPropertyPrefixes := []string{
    "ctl.odm.", "ctl.vendor.",
    "ctl.start$odm.", "ctl.start$vendor.",
    "ctl.stop$odm.", "ctl.stop$vendor.",
    "ro.boot.", "ro.hardware.", "ro.odm.", "ro.vendor.",
    "odm.", "persist.odm.", "persist.vendor.", "vendor.",
}

if shippingApiLevel.LessThanOrEqualTo(ApiLevelR) {
    allowedPropertyPrefixes = append(allowedPropertyPrefixes, "persist.camera.")
}

if shippingApiLevel.GreaterThanOrEqualTo(ApiLevelR) {
    allowedContextPrefixes = []string{"vendor_", "odm_"}
}

cmd := rule.Command().
    BuiltTool("check_prop_prefix").
    FlagWithInput("--property-contexts ", input).
    FlagForEachArg("--allowed-property-prefix ",
                   proptools.ShellEscapeList(allowedPropertyPrefixes)).
    FlagForEachArg("--allowed-context-prefix ", allowedContextPrefixes)

if !ctx.DeviceConfig().BuildBrokenVendorPropertyNamespace() {
    cmd.Flag("--strict")
}

namespace 校验只约束 vendor/odm 的命名空间和 Context 前缀,不能代替 SELinux policy 的读写权限。一个 vendor.foo. 条目即使通过 namespace check,仍需要 vendor_foo_prop 类型声明和允许的 get_prop/set_prop 规则。

4.4 Sysprop API ​

如果 module 依赖 sysprop_library 并生成了 API 文件,Soong 会调用 sysprop_type_checker 对 property contexts 的类型声明和 sysprop API 进行兼容检查。

源码文件:system/sepolicy/build/soong/selinux_contexts.go

go
if len(apiFiles) > 0 {
    out := pathForModuleOut(ctx, ctx.ModuleName()+"_api_checked")
    msg := `\n******************************\n` +
        `API of sysprop_library doesn't match with property_contexts\n` +
        `Please fix the breakage and rebuild.\n` +
        `******************************\n`

    rule.Command().
        Text("( ").
        BuiltTool("sysprop_type_checker").
        FlagForEachInput("--api ", apiFiles).
        FlagWithInput("--context ", builtCtxFile).
        Text(" || ( echo").Flag("-e").
        Flag(`"`+msg+`"`).
        Text("; exit 38) )")
}

这条检查证明的是 API 与 Context 中的 type 约定一致,不证明调用者有 MAC 权限,也不证明运行时 property area 已重新生成。发布镜像使用预编译 contexts 时,还要确认使用的是当前产品对应的产物。

5. 构建校验 ​

5.1 检查器入口 ​

property_contexts_test 不是 checkfc 的 file backend,而是 Soong 注册的 property_info_checker。它读取一个或多个文本文件,要求每行使用 prefix/exact 语法,构建临时 Trie,然后加载编译 policy 检查每个 Context。

源码文件:system/sepolicy/build/soong/selinux_contexts.go

go
// property_contexts_test tests given property_contexts files with property_info_checker.
func propertyContextsTestFactory() android.Module {
    m := &contextsTestModule{context: PropertyContext}
    m.AddProperties(&m.properties)
    android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibCommon)
    return m
}

func (m *contextsTestModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
    tool := "checkfc"
    if m.context == PropertyContext {
        tool = "property_info_checker"
    }
    /* src、sepolicy依赖和rule参数的其余分支省略。 */
}

Android.bp 中的平台测试只输入平台 module,后续分区测试逐层追加上游 module。这意味着 vendor_property_contexts_test 验证的是 platform + system_ext + product + vendor 的合并输入,而不是 vendor 文件单独自洽。

源码文件:system/sepolicy/contexts/Android.bp

make
property_contexts_test {
    name: "plat_property_contexts_test",
    srcs: [":plat_property_contexts"],
    sepolicy: ":precompiled_sepolicy",
}

property_contexts_test {
    name: "vendor_property_contexts_test",
    srcs: [
        ":plat_property_contexts",
        ":system_ext_property_contexts",
        ":product_property_contexts",
        ":vendor_property_contexts",
    ],
    sepolicy: ":precompiled_sepolicy",
}

5.2 Context属性 ​

检查器初始化时从 compiled policy 查找名为 property_type 的 attribute,并取得其 bitmap 位。每个 Context 先经过 sepol 语法检查,再确认其 type 存在、确实是 type 而非 attribute,并且属于 property_type。

源码文件:system/core/property_service/property_info_checker/property_info_checker.cpp

cpp
auto* attr = reinterpret_cast<type_datum*>(
    hashtab_search(policy_db_->p_types.table, "property_type"));
if (attr == nullptr || attr->flavor != TYPE_ATTRIB) {
    std::cerr << "'property_type' is not defined correctly." << std::endl;
    return false;
}
property_type_bit_ = attr->s.value - 1;

bool CheckContext(const char* context) {
    sepol_context_t* sepol_context_raw;
    if (sepol_context_from_string(sepol_handle_, context,
                                  &sepol_context_raw) < 0) {
        return false;
    }
    auto sepol_context = std::unique_ptr<sepol_context_t,
                                         decltype(&sepol_context_free)>{
        sepol_context_raw, sepol_context_free};

    if (sepol_context_check(sepol_handle_, sepol_policy_db_,
                            sepol_context.get()) < 0) {
        return false;
    }

    const char* context_type = sepol_context_get_type(sepol_context.get());
    auto* type = reinterpret_cast<type_datum*>(
        hashtab_search(policy_db_->p_types.table, context_type));
    if (type == nullptr || type->flavor != TYPE_TYPE) return false;

    return ebitmap_get_bit(&policy_db_->type_attr_map[type->s.value - 1],
                           property_type_bit_);
}

所以 u:object_r:system_prop:s0 是否合法,不由 property_contexts 文件名决定,而由 policy 中 system_prop 的 attribute membership 决定。把普通 file type 写进 property contexts 会在构建测试阶段失败。

5.3 语法与重复 ​

property_info_checker 的主函数会累积所有输入文件的 entries,再调用 BuildTrie()。解析错误、重复 exact、重复 prefix 或非法 type 都会使工具返回非零;这使跨分区覆盖必须显式设计,不能依赖“后一个文件自然覆盖前一个文件”。

源码文件:system/core/property_service/property_info_checker/property_info_checker.cpp

cpp
for (int i = 2; i < argc; ++i) {
    auto filename = argv[i];
    auto file_contents = std::string{};
    if (!ReadFileToString(filename, &file_contents)) {
        std::cerr << "Could not read properties from '" << filename << "'\n";
        return -1;
    }

    auto errors = std::vector<std::string>{};
    ParsePropertyInfoFile(file_contents, true,
                          &property_info_entries, &errors);
    if (!errors.empty()) {
        for (const auto& error : errors) {
            std::cerr << "Could not read line from '" << filename
                      << "': " << error << std::endl;
        }
        return -1;
    }
}

if (!BuildTrie(property_info_entries,
               "u:object_r:default_prop:s0", "\\s*",
               &serialized_contexts, &build_trie_error)) {
    std::cerr << "Unable to serialize property contexts: "
              << build_trie_error << std::endl;
    return -1;
}

测试通过只能说明输入可解析、Context 属于 property policy、Trie 可构建;它不能证明设备上实际加载了该文件,也不能证明某个 domain 有 set 或 read 权限。

6. 序列化布局 ​

6.1 字符串去重 ​

TrieBuilder 把 Context 和 type 放入 std::set<std::string>,再保存指向集合元素的指针。相同 Context 在大量规则中只序列化一次,节点只保留 index。

源码文件:system/core/property_service/libpropertyinfoserializer/trie_builder.cpp

cpp
const std::string* TrieBuilder::StringPointerFromContainer(
        const std::string& string, std::set<std::string>* container) {
    // Get a pointer to the string in a given set, such that we only ever
    // serialize each string once.
    auto [iterator, _] = container->emplace(string);
    return &(*iterator);
}

TrieBuilder::TrieBuilder(const std::string& default_context,
                         const std::string& default_type)
    : builder_root_("root") {
    auto* context_pointer = StringPointerFromContainer(
            default_context, &contexts_);
    builder_root_.set_context(context_pointer);
    auto* type_pointer = StringPointerFromContainer(
            default_type, &types_);
    builder_root_.set_type(type_pointer);
}

6.2 只读映射 ​

运行时 parser 打开 property info 文件后检查 owner、group、可写位和文件大小,再用 mmap(PROT_READ, MAP_SHARED) 映射。版本号和序列化大小不符合预期时,它会卸载映射并返回失败。

源码文件:system/core/property_service/libpropertyinfoparser/property_info_parser.cpp

cpp
bool PropertyInfoAreaFile::LoadPath(const char* filename) {
  int fd = open(filename, O_CLOEXEC | O_NOFOLLOW | O_RDONLY);

  struct stat fd_stat;
  if (fstat(fd, &fd_stat) < 0) {
    close(fd);
    return false;
  }

  if ((fd_stat.st_uid != 0) || (fd_stat.st_gid != 0) ||
      ((fd_stat.st_mode & (S_IWGRP | S_IWOTH)) != 0) ||
      (fd_stat.st_size < static_cast<off_t>(sizeof(PropertyInfoArea)))) {
    close(fd);
    return false;
  }

  auto mmap_size = fd_stat.st_size;
  void* map_result = mmap(nullptr, mmap_size, PROT_READ,
                          MAP_SHARED, fd, 0);
  if (map_result == MAP_FAILED) {
    close(fd);
    return false;
  }

  auto property_info_area = reinterpret_cast<PropertyInfoArea*>(map_result);
  if (property_info_area->minimum_supported_version() > 1 ||
      property_info_area->size() != mmap_size) {
    munmap(map_result, mmap_size);
    close(fd);
    return false;
  }

  close(fd);
  mmap_base_ = map_result;
  mmap_size_ = mmap_size;
  return true;
}

owner 和 mode 检查是防篡改边界;版本检查是 ABI 边界。它们失败时 init 没有一个“降级到任意文本规则”的隐式路径,后续 property_info_area->GetPropertyInfo() 也不能安全继续。

6.3 结构访问 ​

PropertyInfoArea 通过偏移量访问 header、Context 字符串数组、type 字符串数组和 root node。数组和字符串访问器只接受小于等于序列化区域 size 的 offset,避免把损坏文件中的任意整数直接当作地址。

源码文件:system/core/property_service/libpropertyinfoparser/include/property_info_parser/property_info_parser.h

cpp
const char* c_string(uint32_t offset) const {
    if (offset != 0 && offset > size()) return nullptr;
    return static_cast<const char*>(data_base_ + offset);
}

const uint32_t* uint32_array(uint32_t offset) const {
    if (offset != 0 && offset > size()) return nullptr;
    return reinterpret_cast<const uint32_t*>(data_base_ + offset);
}

TrieNode root_node() const { return trie(header()->root_offset); }

这里的边界检查是格式解析器的最低保证,不是 SELinux 访问控制。即使序列化区域完整,返回的 Context 仍须经过 CheckMacPerms();即使 Context 合法,调用者也可能被 neverallow 或 allow 缺失拒绝。

7. 运行时加载 ​

7.1 属性区域初始化 ​

bionic 的 ContextsSplit::InitializeProperties() 负责加载 platform 和 vendor property contexts,兼容旧设备上根目录的单文件路径;Recovery 中 vendor 未挂载时,platform 文件加载成功即可继续。

源码文件:bionic/libc/system_properties/contexts_split.cpp

cpp
bool ContextsSplit::InitializeProperties() {
  if (InitializePropertiesFromFile("/property_contexts")) {
    return true;
  }

  if (access("/system/etc/selinux/plat_property_contexts", R_OK) != -1) {
    if (!InitializePropertiesFromFile(
            "/system/etc/selinux/plat_property_contexts")) {
      return false;
    }

    // Recovery may not have mounted vendor yet.
    if (access("/vendor/etc/selinux/vendor_property_contexts", R_OK) != -1) {
      InitializePropertiesFromFile(
              "/vendor/etc/selinux/vendor_property_contexts");
    }
  } else {
    if (!InitializePropertiesFromFile("/plat_property_contexts")) {
      return false;
    }
    if (access("/vendor_property_contexts", R_OK) != -1) {
      InitializePropertiesFromFile("/vendor_property_contexts");
    }
  }
  return true;
}

这段代码属于属性共享区域的初始化,不是 init 的 MAC 检查。Android 17 的 init 使用自己的 PropertyInfoAreaFile 读取 /dev/__properties__/property_info;两条路径都必须使用与当前分区策略一致的 contexts 数据,否则 setter 和 reader 可能观察到不同的 Context 集合。

7.2 Init缓存 ​

init 在 property_service.cpp 中保留一个静态 PropertyInfoAreaFile,启动时加载默认路径并在后续请求中复用。复用的是只读 mmap,不是每个属性的 Context 字符串缓存。

源码文件:system/core/init/property_service.cpp

cpp
[[clang::no_destroy]] static PropertyInfoAreaFile property_info_area;

bool CanReadProperty(const std::string& source_context,
                     const std::string& name) {
    const char* target_context = nullptr;
    property_info_area->GetPropertyInfo(name.c_str(),
                                        &target_context, nullptr);
    PropertyAuditData audit_data;
    audit_data.name = name.c_str();
    ucred cr = {.pid = 0, .uid = 0, .gid = 0};
    audit_data.cr = &cr;

    auto lock = std::lock_guard{selinux_check_access_lock};
    return selinux_check_access(source_context.c_str(), target_context,
                                "file", "read", &audit_data) == 0;
}

CanReadProperty() 通过 target Context 做 file/read 检查;它不调用 property_service set。这正是 property type 的两种 policy 权限被分开的源码证据。

7.3 加载失败 ​

如果 PropertyInfoAreaFile::LoadPath() 失败,问题可能来自文件不存在、owner/group 不为 root、文件可被 group/other 写、版本过高或 size 不一致。不要只查看某一条属性是否存在;先确认 init 实际映射的是哪一个 property_info 文件以及该文件是否与 policy 产物配套。

8. 设置入口 ​

8.1 Unix socket ​

属性设置请求通过 init 的 property service socket 进入。对于 PROP_MSG_SETPROP2,init 从 socket 读取 name/value,使用 SO_PEERCRED 获取 pid/uid/gid,再使用 getpeercon() 获取 source Context,最后调用 HandlePropertySet()。

源码文件:system/core/init/property_service.cpp

cpp
case PROP_MSG_SETPROP2: {
    std::string name;
    std::string value;
    if (!socket.RecvString(&name, &timeout_ms) ||
        !socket.RecvString(&value, &timeout_ms)) {
        socket.SendUint32(PROP_ERROR_READ_DATA);
        return;
    }

    std::string source_context;
    if (!socket.GetSourceContext(&source_context)) {
        socket.SendUint32(PROP_ERROR_PERMISSION_DENIED);
        return;
    }

    const auto& cr = socket.cred();
    std::string error;
    auto result = HandlePropertySet(name, value, source_context,
                                    cr, &socket, &error);
    if (!result) {
        // 异步请求会在完成后发送响应。
        return;
    }
    socket.SendUint32(*result);
    break;
}

source Context 来自 peer,而不是由 property name 推断;target Context 才来自 property_info Trie。这个 owner/消费者分离决定了日志中同时出现 source_context、target_context 和 property name。

8.2 权限顺序 ​

非 ctl.* 属性经过 CheckPermissions():先检查合法名称,再 lookup target Context/type,随后检查 MAC,最后检查 value type。MAC 失败时不会继续做类型校验,也不会进入 PropertySet()。

源码文件:system/core/init/property_service.cpp

cpp
uint32_t CheckPermissions(const std::string& name, const std::string& value,
                          const std::string& source_context,
                          const ucred& cr, std::string* error) {
    if (!IsLegalPropertyName(name)) {
        *error = "Illegal property name";
        return PROP_ERROR_INVALID_NAME;
    }

    if (StartsWith(name, "ctl.")) {
        if (!CheckControlPropertyPerms(name, value, source_context, cr)) {
            *error = StringPrintf("Invalid permissions to perform '%s' on '%s'",
                                  name.c_str() + 4, value.c_str());
            return PROP_ERROR_HANDLE_CONTROL_MESSAGE;
        }
        return PROP_SUCCESS;
    }

    const char* target_context = nullptr;
    const char* type = nullptr;
    property_info_area->GetPropertyInfo(name.c_str(),
                                         &target_context, &type);

    if (!CheckMacPerms(name, target_context,
                       source_context.c_str(), cr)) {
        *error = StringPrintf(
                "SELinux permission check failed "
                "(source_context=%s, target_context=%s)",
                source_context.c_str(), target_context ?: "(null)");
        return PROP_ERROR_PERMISSION_DENIED;
    }

    if (!CheckType(type, value)) {
        *error = StringPrintf(
                "Property type check failed, value doesn't match expected type '%s'",
                type ?: "(null)");
        return PROP_ERROR_INVALID_VALUE;
    }

    return PROP_SUCCESS;
}

property_contexts 只提供 target Context 和可选 type;CheckMacPerms() 才把它变成一次 selinux_check_access(source, target, "property_service", "set")。因此增加一行 contexts 规则不会自动授予设置权限。

8.3 MAC检查 ​

CheckMacPerms() 对每次设置持有 selinux_check_access_lock,把 name、pid、uid、gid 放进 audit data。锁保护的是 libselinux access check 调用,而不是属性值写入的并发队列。

源码文件:system/core/init/property_service.cpp

cpp
static bool CheckMacPerms(const std::string& name,
                          const char* target_context,
                          const char* source_context,
                          const ucred& cr) {
    if (!target_context || !source_context) return false;

    PropertyAuditData audit_data;
    audit_data.name = name.c_str();
    audit_data.cr = &cr;

    auto lock = std::lock_guard{selinux_check_access_lock};
    return selinux_check_access(source_context, target_context,
                                "property_service", "set",
                                &audit_data) == 0;
}

失败消费者是调用方收到的 PROP_ERROR_PERMISSION_DENIED 和 init 日志,内核 audit 记录则由 libselinux/SELinux policy 路径产生。若 target_context 为 null,连一次有效的 SELinux check 都不会执行。

9. 值类型 ​

9.1 清空语义 ​

CheckType() 对空值总是返回 true,使调用者可以清空属性,让“未设置时的默认值”重新生效。这是一个容易漏掉的恢复路径:即使属性声明为 uint 或 enum,空字符串也不会因类型检查被拒绝;它是否能清空还要先通过 MAC 检查。

源码文件:system/core/init/property_type.cpp

cpp
bool CheckType(const std::string& type_string,
               const std::string& value) {
    // Always allow clearing a property such that the default value
    // when it is not set takes over.
    if (value.empty()) return true;

    auto type_strings = Split(type_string, " ");
    if (type_strings.empty()) return false;
    auto type = type_strings[0];

    if (type == "string") return true;
    if (type == "bool") {
        return value == "true" || value == "false" ||
               value == "1" || value == "0";
    }

9.2 数值类型 ​

int 使用 ParseInt 解析有符号 64 位值;uint 先拒绝以 - 开头的值,再用 ParseUint;double 调用 ParseDouble。这些检查发生在属性已经通过 SELinux set 权限之后。

源码文件:system/core/init/property_type.cpp

cpp
    if (type == "int") {
        int64_t parsed;
        return ParseInt(value, &parsed);
    }
    if (type == "uint") {
        uint64_t parsed;
        if (value.empty() || value.front() == '-') return false;
        return ParseUint(value, &parsed);
    }
    if (type == "double") {
        double parsed;
        return ParseDouble(value.c_str(), &parsed);
    }

9.3 Size与枚举 ​

size 不是通用整数,它要求数字后面恰好跟一个 g、k 或 m;enum 则把 type 字符串按空格拆分,从第二项开始逐个比较。Android 17 的实现不接受大写单位,也不把枚举值做大小写折叠。

源码文件:system/core/init/property_type.cpp

cpp
    if (type == "size") {
        auto it = value.begin();
        while (it != value.end() && isdigit(*it)) it++;
        if (it == value.begin() || it == value.end() ||
            (*it != 'g' && *it != 'k' && *it != 'm')) {
            return false;
        }
        it++;
        return it == value.end();
    }
    if (type == "enum") {
        for (auto it = std::next(type_strings.begin());
             it != type_strings.end(); ++it) {
            if (*it == value) return true;
        }
    }
    return false;
}

property_type_test.cpp 对空值、边界整数、负 uint、单位后缀和枚举成员分别断言。测试覆盖的是 CheckType() 的纯函数行为,不覆盖 socket、source Context 或 property_info lookup。

10. 特殊属性 ​

10.1 ctl属性 ​

ctl.* 不是普通属性。ctl.start foo 的 value 是服务名,旧权限模型按 ctl.foo 构造假名;新模型按 ctl.start$foo 构造完整键。两个 lookup 都通过 property_info Trie,只有至少一个 CheckMacPerms() 成功才继续发送控制消息。

源码文件:system/core/init/property_service.cpp

cpp
bool CheckControlPropertyPerms(const std::string& name,
                               const std::string& value,
                               const std::string& source_context,
                               const ucred& cr) {
    if (name == "ctl.start" || name == "ctl.stop" ||
        name == "ctl.restart") {
        auto control_string_legacy = "ctl." + value;
        const char* target_context_legacy = nullptr;
        const char* type_legacy = nullptr;
        property_info_area->GetPropertyInfo(
                control_string_legacy.c_str(),
                &target_context_legacy, &type_legacy);
        if (CheckMacPerms(control_string_legacy, target_context_legacy,
                          source_context.c_str(), cr)) {
            return true;
        }
    }

    auto control_string_full = name + "$" + value;
    const char* target_context_full = nullptr;
    const char* type_full = nullptr;
    property_info_area->GetPropertyInfo(control_string_full.c_str(),
                                        &target_context_full, &type_full);
    return CheckMacPerms(control_string_full, target_context_full,
                         source_context.c_str(), cr);
}

新条目形如 ctl.start$adbd,其中 $ 是 property_info key 的普通字符,不是 shell 展开。把 ctl.start$adbd 写成 ctl.start.adbd 会走完全不同的 Trie 路径。

源码文件:system/sepolicy/private/property_contexts

text
ctl.start$adbd             u:object_r:ctl_adbd_prop:s0
ctl.stop$adbd              u:object_r:ctl_adbd_prop:s0
ctl.restart$adbd           u:object_r:ctl_adbd_prop:s0
ctl.                       u:object_r:ctl_default_prop:s0

10.2 restorecon属性 ​

selinux.restorecon_recursive 使用普通 property set 权限进入 HandlePropertySet(),但成功后不直接写普通属性值,而是触发异步 restorecon 线程。init 对非 PID 1 的请求把路径放进队列并立即返回成功,线程完成后再把同一 property 写回去通知等待者。

源码文件:system/core/init/property_service.cpp

cpp
class AsyncRestorecon {
  public:
    void TriggerRestorecon(const std::string& path) {
        auto guard = std::lock_guard{mutex_};
        paths_.emplace(path);
        if (!thread_started_) {
            thread_started_ = true;
            std::thread{&AsyncRestorecon::ThreadFunction, this}.detach();
        }
    }

  private:
    void ThreadFunction() {
        auto lock = std::unique_lock{mutex_};
        while (!paths_.empty()) {
            auto path = paths_.front();
            paths_.pop();
            lock.unlock();
            if (selinux_android_restorecon(
                    path.c_str(), SELINUX_ANDROID_RESTORECON_RECURSE) != 0) {
                LOG(ERROR) << "Asynchronous restorecon of '"
                           << path << "' failed'";
            }
            android::base::SetProperty(kRestoreconProperty, path);
            lock.lock();
        }
        thread_started_ = false;
    }
};

这个例子说明 property_contexts 的 target Context 是入口授权,不是最终业务动作。若异步 restorecon 失败,属性 set 请求仍可能先收到成功;应继续检查 init 的异步错误日志和目标路径标签。

10.3 powerctl属性 ​

sys.powerctl 先经过 CheckPermissions(),随后在 HandlePropertySet() 中记录发起者 pid/cmdline。reboot,userspace 在 Android 17 被判为 deprecated invalid value;其他合法值继续由 property change 消费者触发关机或重启。

源码文件:system/core/init/property_service.cpp

cpp
if (name == "sys.powerctl") {
    std::string cmdline_path = StringPrintf("proc/%d/cmdline", cr.pid);
    std::string process_cmdline;
    if (ReadFileToString(cmdline_path, &process_cmdline)) {
        process_log_string = StringPrintf(" (%s)", process_cmdline.c_str());
    }
    LOG(INFO) << "Received sys.powerctl='" << value
              << "' from pid: " << cr.pid << process_log_string;
    if (value == "reboot,userspace") {
        *error = "Userspace reboot is deprecated.";
        return {PROP_ERROR_INVALID_VALUE};
    }
}

11. 属性写入 ​

11.1 内存更新 ​

PropertySet() 在再次检查合法名称和值格式后,查找现有 prop_info。已存在的 ro.* 只能写一次;其他属性更新现有 slot 或通过 __system_property_add() 新建 slot。

源码文件:system/core/init/property_service.cpp

cpp
static std::optional<uint32_t> PropertySet(
        const std::string& name, const std::string& value,
        SocketConnection* socket, std::string* error) {
    if (!IsLegalPropertyName(name)) {
        *error = "Illegal property name";
        return {PROP_ERROR_INVALID_NAME};
    }

    if (auto result = IsLegalPropertyValue(name, value); !result.ok()) {
        *error = result.error().message();
        return {PROP_ERROR_INVALID_VALUE};
    }

    prop_info* pi = (prop_info*)__system_property_find(name.c_str());
    if (pi != nullptr) {
        if (StartsWith(name, "ro.")) {
            *error = "Read-only property was already set";
            return {PROP_ERROR_READ_ONLY_PROPERTY};
        }
        __system_property_update(pi, value.c_str(), value.size());
    } else {
        int rc = __system_property_add(name.c_str(), name.size(),
                                       value.c_str(), value.size());
        if (rc < 0) {
            *error = "__system_property_add failed";
            return {PROP_ERROR_SET_FAILED};
        }
    }

CheckPermissions() 的 MAC/type 检查和 PropertySet() 的 property area 更新是两个阶段。第二阶段失败时,调用者已经通过 SELinux;因此排查 PROP_ERROR_SET_FAILED 时要看 property area 容量和合法值,而不是继续添加 allow。

11.2 持久化队列 ​

写入成功后,persist.* 和 next_boot.* 被标记为需要落盘。启动早期如果 persistent properties 尚未加载,init 先只更新内存,避免默认属性加载顺序被持久化写入打断;加载完成后才把写请求交给 PersistWriteThread 或同步写盘。

源码文件:system/core/init/property_service.cpp

cpp
bool need_persist = StartsWith(name, "persist.") ||
                    StartsWith(name, "next_boot.");
if (socket && persistent_properties_loaded && need_persist) {
    if (persist_write_thread) {
        persist_write_thread->Write(name, value, std::move(*socket));
        return {};
    }
    WritePersistentProperty(name, value);
}

NotifyPropertyChange(name, value);
return {PROP_SUCCESS};

返回空的 std::optional 表示 socket 响应会由异步持久化线程稍后发送;这与 AsyncRestorecon 的“先返回,再执行”是相似的时序,但消费者和持久化对象不同。

11.3 属性文件加载 ​

从 build.prop、vendor.prop 等文件加载属性时,init 仍调用 CheckPermissions()。LoadProperties() 会按文件路径把 source Context 设为 init 或 vendor_init,过滤 ctl.*、sys.powerctl 和 restorecon 特殊属性,避免静态文件伪造控制消息。

源码文件:system/core/init/property_service.cpp

cpp
const char* context = kInitContext;
if (SelinuxGetVendorAndroidVersion() >= __ANDROID_API_P__) {
    for (const auto& vendor_path_prefix : kVendorPathPrefixes) {
        if (StartsWith(filename, vendor_path_prefix)) {
            context = kVendorContext;
        }
    }
}

if (StartsWith(key, "ctl.") || key == "sys.powerctl"s ||
    std::string{key} == kRestoreconProperty) {
    LOG(ERROR) << "Ignoring disallowed property '" << key
               << "' with special meaning in prop file '" << filename << "'";
    continue;
}

if (CheckPermissions(key, *expanded_value, context, cr, &error)
        == PROP_SUCCESS) {
    (*properties)[key] = std::move(*expanded_value);
}

同一个 key 从多个文件出现时,LoadProperties() 在 map 中比较旧值并记录 override warning。读取属性文件并不绕过 property_contexts;它依然需要 target Context 和 source Context 的组合满足 policy。

12. 读取权限 ​

12.1 Policy类型 ​

属性 Context 中的 type 必须带 property_type attribute。Android policy 又把很多 type 放进 system_property_type、vendor_property_type、system_public_prop() 或 system_internal_prop() 等层次,这些宏决定可见性和写入边界。

源码文件:system/sepolicy/public/property.te

text
type adbd_prop, property_type;
type debug_prop, property_type;
type default_prop, property_type;
type vendor_default_prop, property_type;

typeattribute system_prop core_property_type;
typeattribute radio_prop core_property_type;
typeattribute debug_prop core_property_type;

源码文件:system/sepolicy/private/property.te

text
system_internal_prop(adb_timeout_prop)
system_restricted_prop(dumpstate_last_id_prop)
system_vendor_config_prop(sensors_hal_prop)

typeattribute system_prop core_property_type;

neverallow { domain -coredomain } {
  system_property_type
  -system_public_property_type
}:property_service set;

attribute 层次是 policy 编译时的对象集合;property_info 的 Context 字符串只是把名字映射到其中一个具体 type。不能从 system_prop 这个名字推断“所有 system 进程可写”,也不能从 vendor_default_prop 推断“所有 vendor 进程可读”。

12.2 get/set宏 ​

策略宏分别为属性文件读取和 property_service 设置生成规则。典型调用如下:

源码文件:system/sepolicy/public/te_macros

text
define(`get_prop', `
  allow $1 $2:file { getattr open read map };
')

define(`set_prop', `
  unix_socket_connect($1, property, init)
  allow $1 $2:property_service set;
  get_prop($1, $2)
')

在真实 policy 中,宏定义和兼容性封装会比这段核心展开更复杂;关键关系是 get_prop(domain, type) 面向 file 类,set_prop(domain, type) 还需要连接 init 的 property socket 和 property_service set。因此“set 成功但 get denied”和“get 成功但 set denied”都是合法的独立状态。

12.3 neverallow边界 ​

Android 17 的 private policy 明确限制非 coredomain 写系统属性、限制 coredomain 写 vendor 属性,并禁止 property type 文件的 ioctl/lock。neverallow 是编译期约束,不是运行时 fallback。

源码文件:system/sepolicy/private/property.te

text
neverallow { domain -coredomain } {
  system_property_type
  -system_public_property_type
}:property_service set;

neverallow { coredomain -init } {
  vendor_property_type
  -vendor_public_property_type
}:property_service set;

neverallow domain property_type:file { ioctl lock };

当新增属性 type 时,应同时决定它属于哪个公开/内部/受限集合,并为读写消费者提供最小规则。只修改 property_contexts 而不修改 property.te,可能得到合法 Context 却在 policy 编译或运行时访问阶段失败。

13. 设备闭环 ​

13.1 查询三件事 ​

排查一个属性时至少分别查询:当前值、property_info 解析出来的 Context/type、调用者 domain。getprop -Z 的可用性因设备工具版本而异,不能把它当成唯一证据。

源码文件:system/core/property_service/libpropertyinfoparser/include/property_info_parser/property_info_parser.h

cpp
void GetPropertyInfo(const char* property,
                     const char** context,
                     const char** type) const;

设备侧可以用以下命令建立可观察链路:

bash
# 当前属性值;空值也可能是合法的“已清空”状态。
adb shell getprop persist.sys.safemode

# 当前设备工具若支持 -Z,查看属性对应的 SELinux Context。
adb shell getprop -Z persist.sys.safemode

# 查看当前 shell 调用者 domain,和 init 日志中的 source_context 对照。
adb shell id -Z

这些命令不能证明当前 shell 具有 property_service set 权限,也不能证明源码中的新条目已经进入 /dev/__properties__/property_info;构建产物和 init 日志仍需单独检查。

13.2 只读预演 ​

setprop 本身会改变状态。诊断时先用一个不会影响业务的测试属性和错误值观察返回码,或只读取 init 日志中的 SELinux permission check failed / Property type check failed 文本。不要用 audit2allow 直接把失败变成 allow,它可能掩盖 namespace、type 或值格式问题。

13.3 失败分层 ​

13.4 日志证据 ​

源码文件:system/core/init/property_service.cpp

cpp
*error = StringPrintf(
        "SELinux permission check failed "
        "(source_context=%s, target_context=%s)",
        source_context.c_str(), target_context ?: "(null)");

*error = StringPrintf(
        "Property type check failed, value doesn't match expected type '%s'",
        (type ?: "(null)"));

第一类日志说明 lookup 已返回 target 或至少暴露了 null target,失败点在 MAC;第二类说明 MAC 已通过,失败点在 type。若只看到客户端的通用 failed to set property,应回到 init 日志按这两个字符串区分路径。

14. 反向验证 ​

14.1 Trie单元测试 ​

property_info_serializer_test.cpp 用人工 entries 构建 Trie,并验证默认值、点前缀、非点前缀、exact 覆盖和长属性名。测试输入与断言能直接证明 lookup 算法的分支,而不是只证明文件能编译。

源码文件:system/core/property_service/libpropertyinfoserializer/property_info_serializer_test.cpp

cpp
auto property_info = std::vector<PropertyInfoEntry>{
    {"test.", "1st", "1st", false},
    {"test.test", "2nd", "2nd", false},
    {"test.test", "5th", "5th", true},
    {"test.test1", "3rd", "3rd", true},
    {"testwordprefix", "9th", "9th", false},
};

ASSERT_TRUE(BuildTrie(property_info, "default", "default",
                      &serialized_trie, &build_trie_error));
auto property_info_area = reinterpret_cast<const PropertyInfoArea*>(
        serialized_trie.data());

property_info_area->GetPropertyInfo("test.test", &context, &type);
EXPECT_STREQ("5th", context);
EXPECT_STREQ("5th", type);

property_info_area->GetPropertyInfo("test.testa", &context, &type);
EXPECT_STREQ("2nd", context);
EXPECT_STREQ("2nd", type);

property_info_area->GetPropertyInfo("testwordprefixblah", &context, &type);
EXPECT_STREQ("9th", context);
EXPECT_STREQ("9th", type);

test.test 证明 exact 覆盖同名非 exact prefix;test.testa 证明较短前缀继承;testwordprefixblah 证明没有点结尾的 prefix 会匹配后续字符。测试没有证明多分区 Soong 合并或 SELinux policy 权限。

14.2 类型单元测试 ​

源码文件:system/core/init/property_type_test.cpp

cpp
TEST(property_type, CheckType_uint) {
    EXPECT_TRUE(CheckType("uint", ""));
    EXPECT_FALSE(CheckType("uint", "abc"));
    EXPECT_TRUE(CheckType("uint", "0"));
    EXPECT_FALSE(CheckType("uint", "-123"));
}

TEST(property_type, CheckType_size) {
    EXPECT_TRUE(CheckType("size", "512g"));
    EXPECT_TRUE(CheckType("size", "512k"));
    EXPECT_TRUE(CheckType("size", "512m"));
    EXPECT_FALSE(CheckType("size", "512gggg"));
}

TEST(property_type, CheckType_enum) {
    EXPECT_TRUE(CheckType("enum 123 456 789", "456"));
    EXPECT_FALSE(CheckType("enum 123 456 789", "0"));
}

这些测试的 arrange 是传入 type 字符串和值,action 是调用纯函数,assert 是布尔结果;它们能证明值解析分支和清空语义,但不能证明 property_contexts 中的 type 字段经过了正确 lookup。两类测试必须结合使用。

14.3 解析实验 ​

在源码树中可以用小型 fixture 调用 serializer 测试边界。下面命令运行的是 Android 17 已有的 gtest/host 测试目标,目标名是否出现在当前产品构建图中取决于 lunch 配置。

bash
# 初始化 Android 构建环境并选择 userdebug 产品。
source build/envsetup.sh
lunch PRODUCT-userdebug

# 构建 property_info serializer 与 init 类型测试目标。
m propertyinfoserializer_tests property_type_test

# 运行 serializer 的 exact/prefix 断言。
atest propertyinfoserializer_tests

# 运行 CheckType 的 bool/int/uint/double/size/enum 断言。
atest property_type_test

propertyinfoserializer_tests 通过只能说明内存 Trie 的构建和查询逻辑;property_type_test 通过只能说明值格式;它们都不替代设备上 property area、policy 和 source domain 的闭环检查。

14.4 构建产物 ​

bash
# 生成分区 property contexts 与对应测试模块。
source build/envsetup.sh
lunch PRODUCT-userdebug
m plat_property_contexts plat_property_contexts_test \
  vendor_property_contexts vendor_property_contexts_test \
  odm_property_contexts odm_property_contexts_test

检查输出文件时,应同时确认文本条目、namespace checker 输出和 precompiled policy 是同一次构建产生的。只拿旧设备中的 /system/etc/selinux/plat_property_contexts 与新源码比较,会把“规则未加载”和“规则本身错误”混为一谈。

15. 源码导航 ​

要回答的问题首选源码关键符号
一行 property_contexts 怎样解析system/core/property_service/libpropertyinfoserializer/property_info_file.cppParsePropertyInfoLine、IsTypeValid
exact、prefix 如何放进 Triesystem/core/property_service/libpropertyinfoserializer/trie_builder.cppAddToTrie
运行时怎样遍历 Triesystem/core/property_service/libpropertyinfoparser/property_info_parser.cppGetPropertyInfoIndexes、CheckPrefixMatch
二进制区域如何加载system/core/property_service/libpropertyinfoparser/property_info_parser.cppPropertyInfoAreaFile::LoadPath
Context 是否属于 property_typesystem/core/property_service/property_info_checker/property_info_checker.cppContextChecker::CheckContext
分区 module 怎样生成system/sepolicy/contexts/Android.bpplat_property_contexts、vendor_property_contexts
M4、namespace、API 检查在哪里system/sepolicy/build/soong/selinux_contexts.gobuildGeneralContexts、checkVendorPropertyNamespace、buildPropertyContexts
设置请求的入口system/core/init/property_service.cpphandle_property_set_fd、HandlePropertySet
target Context 怎样参与 MACsystem/core/init/property_service.cppCheckPermissions、CheckMacPerms
值类型怎样校验system/core/init/property_type.cppCheckType
ctl 属性如何按服务名授权system/core/init/property_service.cppCheckControlPropertyPerms
persist 属性何时写盘system/core/init/property_service.cppPropertySet、PersistWriteThread
读取权限落到哪里bionic/libc/system_properties/、system/sepolicy/private/property.teproperty area、get_prop、neverallow

从 persist.sys.safemode 复述完整路径时,应先指出文本条目和 Context type,再说明 Soong 如何把 platform 输入生成产物,init 如何从 property_info Trie 取出 target Context,CheckMacPerms() 如何执行 property_service:set,CheckType() 如何处理值,最后说明 persist. 如何进入持久化队列。若这条链路中任一步失败,后续消费者都不会以为属性已经正常设置;而读取路径还需要独立检查属性区域 file/read 权限。