Android是如何使用selinux来保护系统属性的

尝试获取Android设备的序列号SerialNo

因为业务需求, 我们需要获取到Android设备的序列号(SerialNo)。首先看一下Android提供的用于获取序列号的Api以及Api实现。

1 Build.SERIAL


    /**
     * A hardware serial number, if available. Alphanumeric only, case-insensitive.
     * This field is always set to {@link Build#UNKNOWN}.
     *
     * @deprecated Use {@link #getSerial()} instead.
     **/
    @Deprecated
    // IMPORTANT: This field should be initialized via a function call to
    // prevent its value being inlined in the app during compilation because
    // we will later set it to the value based on the app's target SDK.
    public static final String SERIAL = getString("no.such.thing");

早期的Android版本中,可以直接通过调用Build.SERIAL来获取序列号,在高版本中,为了保护个人隐私, 不让第三方应用轻易获取序列号。所以该Api已经过时, 并且它的值也被设置成了"unknown"。

2 Build.getSerial()

先看一下该Api的源码, 该源码实现在frameworks/base/core/java/android/os/Build.java

    @SuppressAutoDoc // No support for device / profile owner.
    @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
    public static String getSerial() {
        IDeviceIdentifiersPolicyService service = IDeviceIdentifiersPolicyService.Stub
                .asInterface(ServiceManager.getService(Context.DEVICE_IDENTIFIERS_SERVICE));
        try {
            Application application = ActivityThread.currentApplication();
            String callingPackage = application != null ? application.getPackageName() : null;
            return service.getSerialForPackage(callingPackage);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
        return UNKNOWN;
    }

调用该Api需要READ_PRIVILEGED_PHONE_STATE权限, 该权限是一个protectLevel为signature的系统级权限,只有系统签名的系统App才能获取该权限, 第三方App是无法获取的。

该权限定义在frameworks/base/core/res/AndroidManifest.xml中:

    <!-- @SystemApi Allows read access to privileged phone state.
         @hide Used internally. -->
    <permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
        android:protectionLevel="signature|privileged" />

所以第三方应用无法通过调用getSerial() 来获取序列号。

接下来, 我们看一下DeviceIdentifiersPolicyService中getSerial()的实现。 代码位置在:

frameworks/base/services/core/java/com/android/server/os/DeviceIdentifiersPolicyService.java

getSerial方法的实现为:

        @Override
        public @Nullable String getSerial() throws RemoteException {
            // Since this invocation is on the server side a null value is used for the
            // callingPackage as the server's package name (typically android) should not be used
            // for any device / profile owner checks. The majority of requests for the serial number
            // should use the getSerialForPackage method with the calling package specified.
            if (!TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mContext,
                    /* callingPackage */ null, "getSerial")) {
                return Build.UNKNOWN;
            }
            return SystemProperties.get("ro.serialno", Build.UNKNOWN);
        }

        @Override
        public @Nullable String getSerialForPackage(String callingPackage) throws RemoteException {
            if (!TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mContext,
                    callingPackage, "getSerial")) {
                return Build.UNKNOWN;
            }
            return SystemProperties.get("ro.serialno", Build.UNKNOWN);
        }

TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers即是校验调用方的权限,没有权限的话,会直接返回"unknown"。

从这句代码return SystemProperties.get("ro.serialno", Build.UNKNOWN);来看,DeviceIdentifiersPolicyService也是通过读取系统属性ro.serialno来获取序列号的。这里调用SystemProperties.get的进程为system_server。所以我们想要验证一下, 普通App是否也可以直接读取到这个属性。

3 SystemProperties.get

因为SystemProperties是隐藏Api, 所以我们通过反射来调用。代码如下:

    Class<?> clazz = null;
    try {
        Class<?> clazz = Class.forName("android.os.SystemProperties");
        Method m = clazz.getDeclaredMethod("get", String.class);
        return (String)m.invoke(null, "ro.serialno");
    } catch (Exception e) {
        e.printStackTrace();
    }

但是执行该代码后,发现无法获取ro.serialno的值,并且会打印如下日志:

E/libc: Access denied finding property "ro.serialno"

该日志说明,我们读取ro.serialno属性的操作被拒绝了。


App读取属性流程分析

下面我们深入解析一下属性相关的源码。我们从读取属性的流程来跟踪源码,也会解析一下App中属性的初始化以及关键数据结构的创建。

跟踪读取属性的代码流程

接下来理一下读取系统属性的流程。

首先看一下入口SystemProperties.get方法,代码所在文件为frameworks/base/core/java/android/os/SystemProperties.java

    /**
     * Get the String value for the given {@code key}.
     *
     * @param key the key to lookup
     * @return an empty string if the {@code key} isn't found
     * @hide
     */
    @NonNull
    @SystemApi
    @TestApi
    public static String get(@NonNull String key) {
        if (TRACK_KEY_ACCESS) onKeyAccess(key);
        return native_get(key);
    }


    @UnsupportedAppUsage
    private static native String native_get(String key);

接下来我们找到native_get的native实现的入口函数,经过搜索, 发现它实现在frameworks/base/core/jni/android_os_SystemProperties.cpp中:

int register_android_os_SystemProperties(JNIEnv *env)
{
    const JNINativeMethod method_table[] = {
        { "native_get", "(Ljava/lang/String;)Ljava/lang/String;",
          (void*) SystemProperties_getS },
        { "native_get",
          "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
          (void*) SystemProperties_getSS },
        { "native_get_int", "(Ljava/lang/String;I)I",
          (void*) SystemProperties_get_integral<jint> },
        { "native_get_long", "(Ljava/lang/String;J)J",
          (void*) SystemProperties_get_integral<jlong> },
        { "native_get_boolean", "(Ljava/lang/String;Z)Z",
          (void*) SystemProperties_get_boolean },
        { "native_set", "(Ljava/lang/String;Ljava/lang/String;)V",
          (void*) SystemProperties_set },
        { "native_add_change_callback", "()V",
          (void*) SystemProperties_add_change_callback },
        { "native_report_sysprop_change", "()V",
          (void*) SystemProperties_report_sysprop_change },
    };
    return RegisterMethodsOrDie(env, "android/os/SystemProperties",
                                method_table, NELEM(method_table));
}


jstring SystemProperties_getSS(JNIEnv *env, jclass clazz, jstring keyJ,
                               jstring defJ)
{
    // Using ConvertKeyAndForward is sub-optimal for copying the key string,
    // but improves reuse and reasoning over code.
    auto handler = [&](const std::string& key, jstring defJ) {
        std::string prop_val = android::base::GetProperty(key, "");
        if (!prop_val.empty()) {
            return env->NewStringUTF(prop_val.c_str());
        };
        if (defJ != nullptr) {
            return defJ;
        }
        // This function is specified to never return null (or have an
        // exception pending).
        return env->NewStringUTF("");
    };
    return ConvertKeyAndForward(env, keyJ, defJ, handler);
}

jstring SystemProperties_getS(JNIEnv *env, jclass clazz, jstring keyJ)
{
    return SystemProperties_getSS(env, clazz, keyJ, nullptr);
}

SystemProperties_getSS方法中,真正执行获取属性的代码为android::base::GetProperty(key, ""),该方法实现在system/core/base/properties.cpp中:

std::string GetProperty(const std::string& key, const std::string& default_value) {
  std::string property_value;
#if defined(__BIONIC__)
  const prop_info* pi = __system_property_find(key.c_str());
  if (pi == nullptr) return default_value;

  __system_property_read_callback(pi,
                                  [](void* cookie, const char*, const char* value, unsigned) {
                                    auto property_value = reinterpret_cast<std::string*>(cookie);
                                    *property_value = value;
                                  },
                                  &property_value);
#else
  auto it = g_properties.find(key);
  if (it == g_properties.end()) return default_value;
  property_value = it->second;
#endif
  // If the property exists but is empty, also return the default value.
  // Since we can't remove system properties, "empty" is traditionally
  // the same as "missing" (this was true for cutils' property_get).
  return property_value.empty() ? default_value : property_value;
}

GetProperty函数的内部逻辑,会根据__BIONIC__这个宏是否定义,分两个不同的逻辑。可以看到如果该宏已定义的话,会继续调用__system_property_find, 如果该宏未定义,则从全局变量g_properties中读取。

从逻辑的复杂度来看,g_properties 逻辑比较简单,并且也没有什么保活措施,属性应该是随意读取的,应该是兼容的旧版本。g_properties的值也没有被初始化。

并且经过追踪__BIONIC__的定义,发现system/core/base/properties.cpp引用了头文件#include "android-base/properties.h", 这个头文件又引用了#include <sys/cdefs.h>,这个头文件中定义了__BIONIC__

#define __BIONIC__ 1

所以GetProperty会继续调用__system_property_find。__system_property_find实现在bionic/libc/bionic/system_property_api.cpp中:

__BIONIC_WEAK_FOR_NATIVE_BRIDGE
const prop_info* __system_property_find(const char* name) {
  return system_properties.Find(name);
}

system_properties是一个全局对象,对应的类为SystemProperties:

static SystemProperties system_properties;
static_assert(__is_trivially_constructible(SystemProperties),
              "System Properties must be trivially constructable");

SystemProperties类定义在bionic/libc/system_properties/include/system_properties/system_properties.h中,实现在bionic/libc/system_properties/system_properties.cpp中, 实现代码比较多, 我们直接看Find方法的实现:

const prop_info* SystemProperties::Find(const char* name) {
  if (!initialized_) {
    return nullptr;
  }

  prop_area* pa = contexts_->GetPropAreaForName(name);
  if (!pa) {
    async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Access denied finding property \"%s\"", name);
    return nullptr;
  }

  return pa->find(name);
}

Find方法调用成员变量contexts_的GetPropAreaForName方法来获取一个prop_area,如果无法获取,则打印Access denied finding property日志。 这跟我们上面通过反射SystemProperties.get方法时,打印出来的日志是一致的,这也证明我们跟踪代码的方向是正确的。

所以可以确定,Android拦截读取属性的逻辑,是在contexts_->GetPropAreaForName里面实现的。


相关数据结构的初始化

我们接着上面的流程继续深入分析。首先看一下contexts_是如何被初始化的。 该成员变量的初始化,在成员函数Init中:

bool SystemProperties::Init(const char* filename) {
  // This is called from __libc_init_common, and should leave errno at 0 (http://b/37248982).
  ErrnoRestorer errno_restorer;

  if (initialized_) {
    contexts_->ResetAccess();
    return true;
  }

  if (strlen(filename) >= PROP_FILENAME_MAX) {
    return false;
  }
  strcpy(property_filename_, filename);

// property_filename_ 的值为PROP_FILENAME #define PROP_FILENAME "/dev/__properties__"
  if (is_dir(property_filename_)) {
    if (access("/dev/__properties__/property_info", R_OK) == 0) {
      contexts_ = new (contexts_data_) ContextsSerialized();
      if (!contexts_->Initialize(false, property_filename_, nullptr)) {
        return false;
      }
    } else {
      contexts_ = new (contexts_data_) ContextsSplit();
      if (!contexts_->Initialize(false, property_filename_, nullptr)) {
        return false;
      }
    }
  } else {
    contexts_ = new (contexts_data_) ContextsPreSplit();
    if (!contexts_->Initialize(false, property_filename_, nullptr)) {
      return false;
    }
  }
  initialized_ = true;
  return true;
}

初始化的时候,首先会将构造方法中的参数filename拷贝到成员变量property_filename_中,可以从调用构造方法的地方追代码,可以得知property_filename_的值为"/dev/__properties__", 是一个目录 :

ls -l /dev | grep properties                                          
drwx--x--x  2 root      root             8920 2021-11-02 17:42 __properties__

尽然它是一个目录, 则会走到if (access("/dev/__properties__/property_info", R_OK) == 0)这个if语句。 这个if语句会判断/dev/__properties__/property_info这个文件是否可读。我们看一下这个文件的权限信息:

ls -l /dev/__properties__/property_info
-r--r--r-- 1 root root 98768 2021-11-02 17:42 /dev/__properties__/property_info

这个文件对于所有的用户都有读权限。 所以会继续执行以下逻辑 :

      contexts_ = new (contexts_data_) ContextsSerialized();
      if (!contexts_->Initialize(false, property_filename_, nullptr)) {
        return false;
      }

我们之所以分析这个逻辑, 就是为了要知道, contexts_对象的真实类型。 现在我们知道, 它的真实类型是ContextsSerialized。

所以结合上一节的分析, 拦截读取属性的逻辑, 是在ContextsSerialized类的GetPropAreaForName方法中实现的。下面我们去看这个方法的实现。 在文件bionic/libc/system_properties/contexts_serialized.cpp 中:

prop_area* ContextsSerialized::GetPropAreaForName(const char* name) {
  uint32_t index;
  property_info_area_file_->GetPropertyInfoIndexes(name, &index, nullptr);
  if (index == ~0u || index >= num_context_nodes_) {
    async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Could not find context for property \"%s\"",
                          name);
    return nullptr;
  }
  auto* context_node = &context_nodes_[index];
  if (!context_node->pa()) {
    // We explicitly do not check no_access_ in this case because unlike the
    // case of foreach(), we want to generate an selinux audit for each
    // non-permitted property access in this function.
    context_node->Open(false, nullptr);
  }
  return context_node->pa();
}

该方法的核心逻辑有三个。

  1. 通过property_info_area_file_->GetPropertyInfoIndexes根据属性名称(这里是ro.serialno)获取一个索引inidex;
  2. 通过&context_nodes_[index]获取一个context_node指针;
  3. 通过context_node->pa()获取prop_area指针, 并将该指针返回。

property_info_area_file_和context_nodes_都是在ContextsSerialized的Initialize方法中初始化的。我们看一下Initialize方法,以便可以得知property_info_area_file_和context_nodes_到底是做什么的。

bool ContextsSerialized::Initialize(bool writable, const char* filename, bool* fsetxattr_failed) {

// filename_ 的值为PROP_FILENAME #define PROP_FILENAME "/dev/__properties__"
  filename_ = filename;
  if (!InitializeProperties()) {
    return false;
  }

  if (writable) {
    mkdir(filename_, S_IRWXU | S_IXGRP | S_IXOTH);
    bool open_failed = false;
    if (fsetxattr_failed) {
      *fsetxattr_failed = false;
    }

    for (size_t i = 0; i < num_context_nodes_; ++i) {
      if (!context_nodes_[i].Open(true, fsetxattr_failed)) {
        open_failed = true;
      }
    }
    if (open_failed || !MapSerialPropertyArea(true, fsetxattr_failed)) {
      FreeAndUnmap();
      return false;
    }
  } else {
    if (!MapSerialPropertyArea(false, nullptr)) {
      FreeAndUnmap();
      return false;
    }
  }
  return true;
}

Initialize方法又调用了InitializeProperties方法。 InitializeProperties方法代码如下:

bool ContextsSerialized::InitializeProperties() {
  if (!property_info_area_file_.LoadDefaultPath()) {
    return false;
  }

  if (!InitializeContextNodes()) {
    FreeAndUnmap();
    return false;
  }

  return true;
}

从property_info_area_file_.LoadDefaultPath()来看, 是从一个文件中加载数据。InitializeContextNodes用来初始化ContextNodes。

我们先看一下LoadDefaultPath这个方法,实现在system/core/property_service/libpropertyinfoparser/property_info_parser.cpp中:

bool PropertyInfoAreaFile::LoadDefaultPath() {
  return LoadPath("/dev/__properties__/property_info");
}

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;
}

LoadDefaultPath又调用LoadPath, 将/dev/__properties__/property_info这个文件, 通过mmap映射到当前进程的内存空间中, 起始地址为map_result 。然后通过auto property_info_area = reinterpret_cast<PropertyInfoArea*>(map_result);这句代码来看,/dev/__properties__/property_info这个文件中,存放的是序列化的PropertyInfoArea对象。整个文件映射到内存中后,就是一个PropertyInfoArea数组。

所以到现在我们可以知道,为什么SystemProperties中的contexts_的类型为ContextsSerialized。

但是现在我们还不知道/dev/__properties__/property_info中到底是什么对象。

下面我们接着看InitializeContextNodes的实现:

bool ContextsSerialized::InitializeContextNodes() {
  auto num_context_nodes = property_info_area_file_->num_contexts();
  auto context_nodes_mmap_size = sizeof(ContextNode) * num_context_nodes;
  // We want to avoid malloc in system properties, so we take an anonymous map instead (b/31659220).
  void* const map_result = mmap(nullptr, context_nodes_mmap_size, PROT_READ | PROT_WRITE,
                                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (map_result == MAP_FAILED) {
    return false;
  }

  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, map_result, context_nodes_mmap_size,
        "System property context nodes");

  context_nodes_ = reinterpret_cast<ContextNode*>(map_result);
  num_context_nodes_ = num_context_nodes;
  context_nodes_mmap_size_ = context_nodes_mmap_size;

  for (size_t i = 0; i < num_context_nodes; ++i) {
    new (&context_nodes_[i]) ContextNode(property_info_area_file_->context(i), filename_);
  }

  return true;
}

首先从property_info_area_file_中获取contexts的个数。这个property_info_area_file_我们上面说过,他是读取的/dev/__properties__/property_info文件,文件中是序列化的对象,所以可以认为,对象的个数,就是num_context_nodes 。

然后根据num_context_nodes 计算创建ContextNode所需要的内存空间, 通过mmap匿名映射一块内存,映射后内存的起始地址为map_result, 通过context_nodes_ = reinterpret_cast<ContextNode*>(map_result)这行代码, 将这块内存当做context_nodes_ 数组。

最后通过for循环,创建各个ContextNode。

总结一下上面的逻辑:

从property_info_area_file_中读取序列化的对象PropertyInfoArea,通过这些序列化的对象,创建多个ContextNode,并将这些ContextNode对象,放到一个数组context_nodes_中。可以认为一个PropertyInfoArea对象,对应一个ContextNode对象。

但是分析到这里,我们还是不知道这些对象到底是什么。要搞明白这个问题, 我们就要知道/dev/__properties__/property_info文件中存放的到底是什么数据。 但是这个文件是序列化的,通过cat命令,打印出来的都是乱码:

device_type�Dwake_on_hotplug0�D�D�D�@������hw`���x�x�p���������hwui|���
Luse_vulkan��
             ���
                ���
��������imei_match����������recovery������status(`�D�`�8�	��������imei_rpmbH�X������status|���������
                        ��������incremental����a����enable������������������init�P�,�,����������userspace_reboot0�@�
                                  �is_supportedl�������|���������kernel����Lqemu���T������������������android���L	bootanim�T�4�4�,���������ebpf8�H�	Lsupportedp���������L����qemu����������	e����khungtask��$���$�����������lib_gui������frame_event_history_size@�T�T�T�P�e����llkp�����������������lmk������<`�����������(�H�p����D	critical��Dcritical_upgrade�Ddebug(�D	downgrade_pressureL�Dkill_heaviest_taskp�D	kill_timeout_ms��D	low��D	medium��D	psi_complete_stall_ms��D	psi_partial_stall_ms

所以我们还是只能通过读代码去分析。

下面我们看一下这个文件是如何初始化的。 属性的初始化是在init中, 下面我们继续分析init中属性初始化相关的代码。


Init初始化属性系统

init的代码所在文件为system/core/init/init.cpp,从这个文件中搜索property关键字,可以找到如下代码:

int SecondStageMain(int argc, char** argv) {
    //......
    property_init();
    //......
    
    // Propagate the kernel variables to internal variables
    // used by init as well as the current required properties.
    export_kernel_boot_props();
    
    //......
    property_load_boot_defaults(load_debug_prop);
    //......
}

property_init

首先看property_init方法的实现:

property_init这个函数实现在system/core/init/property_service.cpp中:

void property_init() {
    mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
    CreateSerializedPropertyInfo();
    if (__system_property_area_init()) {
        LOG(FATAL) << "Failed to initialize property area";
    }
    if (!property_info_area.LoadDefaultPath()) {
        LOG(FATAL) << "Failed to load serialized property info file";
    }
}

这个方法中的逻辑还算清晰。分四步

  1. 创建/dev/__properties__这个目录
  2. CreateSerializedPropertyInfo
  3. __system_property_area_init
  4. property_info_area.LoadDefaultPath()

为了得到更多细节, 我们分别看一下这几个方法。

CreateSerializedPropertyInfo

该方法代码如下:

void CreateSerializedPropertyInfo() {
    auto property_infos = std::vector<PropertyInfoEntry>();
    if (access("/system/etc/selinux/plat_property_contexts", R_OK) != -1) {
        if (!LoadPropertyInfoFromFile("/system/etc/selinux/plat_property_contexts",
                                      &property_infos)) {
            return;
        }
        // Don't check for failure here, so we always have a sane list of properties.
        // E.g. In case of recovery, the vendor partition will not have mounted and we
        // still need the system / platform properties to function.
        if (!LoadPropertyInfoFromFile("/vendor/etc/selinux/vendor_property_contexts",
                                      &property_infos)) {
            // Fallback to nonplat_* if vendor_* doesn't exist.
            LoadPropertyInfoFromFile("/vendor/etc/selinux/nonplat_property_contexts",
                                     &property_infos);
        }
        if (access("/product/etc/selinux/product_property_contexts", R_OK) != -1) {
            LoadPropertyInfoFromFile("/product/etc/selinux/product_property_contexts",
                                     &property_infos);
        }
        if (access("/odm/etc/selinux/odm_property_contexts", R_OK) != -1) {
            LoadPropertyInfoFromFile("/odm/etc/selinux/odm_property_contexts", &property_infos);
        }
    } else {
        if (!LoadPropertyInfoFromFile("/plat_property_contexts", &property_infos)) {
            return;
        }
        if (!LoadPropertyInfoFromFile("/vendor_property_contexts", &property_infos)) {
            // Fallback to nonplat_* if vendor_* doesn't exist.
            LoadPropertyInfoFromFile("/nonplat_property_contexts", &property_infos);
        }
        LoadPropertyInfoFromFile("/product_property_contexts", &property_infos);
        LoadPropertyInfoFromFile("/odm_property_contexts", &property_infos);
    }

    auto serialized_contexts = std::string();
    auto error = std::string();
    if (!BuildTrie(property_infos, "u:object_r:default_prop:s0", "string", &serialized_contexts,
                   &error)) {
        LOG(ERROR) << "Unable to serialize property contexts: " << error;
        return;
    }

    constexpr static const char kPropertyInfosPath[] = "/dev/__properties__/property_info";
    if (!WriteStringToFile(serialized_contexts, kPropertyInfosPath, 0444, 0, 0, false)) {
        PLOG(ERROR) << "Unable to write serialized property infos to file";
    }
    selinux_android_restorecon(kPropertyInfosPath, 0);
}

首先通过access来判断文件/system/etc/selinux/plat_property_contexts是否可读。这里的代码是运行在init进程中的, init进程是root用户的,肯定可以读。我们可以看一下这个文件的权限信息:

ls -lh /system/etc/selinux/plat_property_contexts
-rw-r--r-- 1 root root 46K 2009-01-01 08:00 /system/etc/selinux/plat_property_contexts

对所有用户都是可读的。

既然这个文件可读, 就会执行到if语句里面。 if语句里面,会将多个目录下的xxx_property_contexts文件,读取到property_infos中, property_infos是一个std::vector<PropertyInfoEntry>集合。

这些文件的是一致的。我们来看一下/system/etc/selinux/plat_property_contexts中的内容。这个文件很长,我们截取一部分来看:

persist.sys.hdmi.keep_awake u:object_r:exported2_system_prop:s0 exact bool
persist.sys.sf.color_mode u:object_r:exported2_system_prop:s0 exact int
persist.sys.sf.color_saturation u:object_r:exported2_system_prop:s0 exact string
persist.sys.sf.native_mode u:object_r:exported2_system_prop:s0 exact int
pm.dexopt.ab-ota u:object_r:exported_pm_prop:s0 exact string

可以看到,第一列是属性名称, 第二列是selinux的安全上下文信息。所以到这里,基本上可以确定,我们之所以读取不到ro.serialno属性,是被selinux限制了。

我用了一台vivo手机,进入adb shell, 读取这几个文件,并没有ro.serialno对象的selinux context信息。但是从Android源码的system/sepolicy/private/property_contexts文件中找到了相关信息:

ro.serialno             u:object_r:serialno_prop:s0
ro.boot.serialno        u:object_r:serialno_prop:s0

回到代码逻辑, 我们接着看CreateSerializedPropertyInfo方法中的逻辑。 从上面的部分可以了解到,会将各个xxx_property_contexts文件中的属性以及属性对应的selinux安全上下文读取到property_infos集合中,下面看一下集合中的数据会被存放到何处:

    auto serialized_contexts = std::string();
    auto error = std::string();
    if (!BuildTrie(property_infos, "u:object_r:default_prop:s0", "string", &serialized_contexts,
                   &error)) {
        LOG(ERROR) << "Unable to serialize property contexts: " << error;
        return;
    }

这段代码通过BuildTrie函数, 将property_infos集合中的数据,转变成字符串serialized_contexts 。我们暂且不看它是如何转换的,我们先去看最后被存到哪里:

    constexpr static const char kPropertyInfosPath[] = "/dev/__properties__/property_info";
    if (!WriteStringToFile(serialized_contexts, kPropertyInfosPath, 0444, 0, 0, false)) {
        PLOG(ERROR) << "Unable to write serialized property infos to file";
    }

这段代码是关键信息。说明这些数据正是被写到/dev/__properties__/property_info中了。所以我们可以反推上面的BuildTrie方法正是将对象进行序列化,然后转变成乱码字符串的。如果没有序列化, /dev/__properties__/property_info中的内容应该就是下面这样的格式:

ro.serialno             u:object_r:serialno_prop:s0
ro.boot.serialno        u:object_r:serialno_prop:s0

所以做一下总结:

/dev/__properties__/property_info文件中存储的不是属性的值, 而是属性的selinux上下文。也就是说,每个属性是被哪个selinux上下文来保护的。

__system_property_area_init

上面只是初始化的属性对应的selinux信息,我们还没有看到属性值是怎么存储的。我们接着看__system_property_area_init方法。该方法实现在bionic/libc/bionic/system_property_api.cpp文件中:

__BIONIC_WEAK_FOR_NATIVE_BRIDGE
int __system_property_area_init() {
  bool fsetxattr_failed = false;
  return system_properties.AreaInit(PROP_FILENAME, &fsetxattr_failed) && !fsetxattr_failed ? 0 : -1;
}

这里的PROP_FILENAME的值为/dev/__properties__

#define PROP_FILENAME "/dev/__properties__"

__system_property_area_init调用system_properties.AreaInit。这个system_properties我们上面说过,是一个SystemProperties对象,实现在bionic/libc/system_properties/system_properties.cpp中:


bool SystemProperties::AreaInit(const char* filename, bool* fsetxattr_failed) {
  if (strlen(filename) >= PROP_FILENAME_MAX) {
    return false;
  }
  strcpy(property_filename_, filename);

  contexts_ = new (contexts_data_) ContextsSerialized();
  if (!contexts_->Initialize(true, property_filename_, fsetxattr_failed)) {
    return false;
  }
  initialized_ = true;
  return true;
}

这个代码我们看上去有点熟悉。 上面我们分析属性读取流程的时候,分析过SystemProperties的Init方法。注意这里的AreaInit和Init是不一样的。 这里的AreaInit是运行在init进程中的,上面的Init是运行在普通应用进程中的。

这里也会创建ContextsSerialized对象,并且调用该对象的Initialize方法。该方法的前两个参数为传入参数:

  1. writable, 如果是在init中执行,则传入true, 如果实在App进程中执行,则传入false
  2. filename, 我们已经知道, 该参数的值为 /dev/__properties__

下面我们再次看一下ContextsSerialized的Initialize方法:

bool ContextsSerialized::Initialize(bool writable, const char* filename, bool* fsetxattr_failed) {

// filename_ 的值为PROP_FILENAME #define PROP_FILENAME "/dev/__properties__"
  filename_ = filename;
  if (!InitializeProperties()) {
    return false;
  }

  if (writable) {
    mkdir(filename_, S_IRWXU | S_IXGRP | S_IXOTH);
    bool open_failed = false;
    if (fsetxattr_failed) {
      *fsetxattr_failed = false;
    }

    for (size_t i = 0; i < num_context_nodes_; ++i) {
      if (!context_nodes_[i].Open(true, fsetxattr_failed)) {
        open_failed = true;
      }
    }
    if (open_failed || !MapSerialPropertyArea(true, fsetxattr_failed)) {
      FreeAndUnmap();
      return false;
    }
  } else {
    if (!MapSerialPropertyArea(false, nullptr)) {
      FreeAndUnmap();
      return false;
    }
  }
  return true;
}

InitializeProperties的执行,前面已经讲过了, 就是从/dev/__properties__/property_info文件中读取信息, 然后创建ContextNode数组。 现在我们知道了, 每个ContextNode对象, 就代表一个属性的selinux信息,而不是属性的值。属性的值是单独存到其他地方的,至于存到哪里, 正是我们现在正在分析的问题。

因为现在是在init进程中执行,writable传入的值为true, 所以会走到对应的分支。 该分支中的关键代码是调用每个ContextNode的Open函数,且第一个参数传入的是true。

接下来我们看一下ContextNode的Open函数, 实现在bionic/libc/system_properties/context_node.cpp中:

bool ContextNode::Open(bool access_rw, bool* fsetxattr_failed) {
  lock_.lock();
  if (pa_) {
    lock_.unlock();
    return true;
  }

// 格式为 /dev/__properties__/u:object_r:serialno_prop:s0
  char filename[PROP_FILENAME_MAX];
  int len = async_safe_format_buffer(filename, sizeof(filename), "%s/%s", filename_, context_);
  if (len < 0 || len >= PROP_FILENAME_MAX) {
    lock_.unlock();
    return false;
  }

  if (access_rw) {
    pa_ = prop_area::map_prop_area_rw(filename, context_, fsetxattr_failed);
  } else {
    pa_ = prop_area::map_prop_area(filename);
  }
  lock_.unlock();
  return pa_;
}

第一句关键代码是async_safe_format_buffer(filename, sizeof(filename), "%s/%s", filename_, context_);, 将filename_和context_这两个成员变量,拼接成filename 。 filename_我们已经知道了,值为/dev/__properties__, 那么这个context_到底是什么呢?

让我们回忆一下,ContextNode可以看做是和/dev/__properties__/property_info中的序列化对象是对应的。而/dev/__properties__/property_info中每个对象的关键信息如下:

persist.sys.hdmi.keep_awake u:object_r:exported2_system_prop:s0 exact bool
persist.sys.sf.color_mode u:object_r:exported2_system_prop:s0 exact int
persist.sys.sf.color_saturation u:object_r:exported2_system_prop:s0 exact string
persist.sys.sf.native_mode u:object_r:exported2_system_prop:s0 exact int
pm.dexopt.ab-ota u:object_r:exported_pm_prop:s0 exact string

其中第一列为name, 也就是属性名。 第二列为selinux上下文信息, 也就是这里的context_成员变量。

所以filename_和context_拼接成的filename的值的格式为 /dev/__properties__/u:object_r:serialno_prop:s0, 当然, 最后的部分context_是根据ContextNode而变化的。

我找了个root手机, 看了下/dev/__properties__/目录下, 有很多这种格式的文件:

 ls -l /dev/__properties__
total 796
-r--r--r-- 1 root root 131072 1970-05-01 06:58 properties_serial
-r--r--r-- 1 root root  26968 1970-05-01 06:58 property_info
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:adsprpc_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:audio_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:bg_boot_complete_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:bg_daemon_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:bluetooth_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:bootloader_boot_reason_prop:s0

//......

每个文件的权限为-r--r--r--,好像是这些文件对所有用户均可读。 但是经过测试发现, 根本就读不到。这是因为每个文件都具有selinux上下文, selinux会禁止普通进程读取这些文件。我们通过ls -lZ再次查看一下(-Z参数会打印出来每个文件的selinux上下文):

sagit:/ # ls -lZ /dev/__properties__
total 796
-r--r--r-- 1 root root u:object_r:properties_serial:s0                131072 1970-05-01 06:58 properties_serial
-r--r--r-- 1 root root u:object_r:property_info:s0                     26968 1970-05-01 06:58 property_info
-r--r--r-- 1 root root u:object_r:adsprpc_prop:s0                     131072 1970-05-01 06:58 u:object_r:adsprpc_prop:s0
-r--r--r-- 1 root root u:object_r:audio_prop:s0                       131072 1970-05-01 06:58 u:object_r:audio_prop:s0
-r--r--r-- 1 root root u:object_r:bg_boot_complete_prop:s0            131072 1970-05-01 06:58 u:object_r:bg_boot_complete_prop:s0
-r--r--r-- 1 root root u:object_r:bg_daemon_prop:s0                   131072 1970-05-01 06:58 u:object_r:bg_daemon_prop:s0
-r--r--r-- 1 root root u:object_r:bluetooth_prop:s0                   131072 1970-05-01 06:58 u:object_r:bluetooth_prop:s0
-r--r--r-- 1 root root u:object_r:bootloader_boot_reason_prop:s0      131072 1970-05-01 06:58 u:object_r:bootloader_boot_reason_prop:s0
-r--r--r-- 1 root root u:object_r:boottime_prop:s0                    131072 2021-12-16 13:26 u:object_r:boottime_prop:s0
-r--r--r-- 1 root root u:object_r:bservice_prop:s0                    131072 1970-05-01 06:58 u:object_r:bservice_prop:s0

可以看出,文件名和selinux的上下文是一致的。看到这里我们大概已经知道了,具体的属性值可能就是存放到这些文件中的。

我们回到ContextNode的Open函数,已经知道filename的值为/dev/__properties__/u:object_r:serialno_prop:s0这种格式。下面我们继续看代码。

因为access_rw值为true, 所以会执行pa_ = prop_area::map_prop_area_rw(filename, context_, fsetxattr_failed);这行代码。

传入的值filename 和 context_我们已经分析过。 接下来我们看map_prop_area_rw的实现,在bionic/libc/system_properties/prop_area.cpp中:

prop_area* prop_area::map_prop_area_rw(const char* filename, const char* context,
                                       bool* fsetxattr_failed) {
  /* dev is a tmpfs that we can use to carve a shared workspace
   * out of, so let's do that...
   */
  const int fd = open(filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL, 0444);

  if (fd < 0) {
    if (errno == EACCES) {
      /* for consistency with the case where the process has already
       * mapped the page in and segfaults when trying to write to it
       */
      abort();
    }
    return nullptr;
  }

  if (context) {
    if (fsetxattr(fd, XATTR_NAME_SELINUX, context, strlen(context) + 1, 0) != 0) {
      async_safe_format_log(ANDROID_LOG_ERROR, "libc",
                            "fsetxattr failed to set context (%s) for \"%s\"", context, filename);
      /*
       * fsetxattr() will fail during system properties tests due to selinux policy.
       * We do not want to create a custom policy for the tester, so we will continue in
       * this function but set a flag that an error has occurred.
       * Init, which is the only daemon that should ever call this function will abort
       * when this error occurs.
       * Otherwise, the tester will ignore it and continue, albeit without any selinux
       * property separation.
       */
      if (fsetxattr_failed) {
        *fsetxattr_failed = true;
      }
    }
  }

  if (ftruncate(fd, PA_SIZE) < 0) {
    close(fd);
    return nullptr;
  }

  pa_size_ = PA_SIZE;
  pa_data_size_ = pa_size_ - sizeof(prop_area);

  void* const memory_area = mmap(nullptr, pa_size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  if (memory_area == MAP_FAILED) {
    close(fd);
    return nullptr;
  }

  prop_area* pa = new (memory_area) prop_area(PROP_AREA_MAGIC, PROP_AREA_VERSION);

  close(fd);
  return pa;
}

首先调用open打开文件(文件格式为 /dev/__properties__/u:object_r:serialno_prop:s0), 这里有一个O_CREAT,意思是如果文件不存在的话, 就创建文件。因为这是init第一次初始化系统属性,可以得知,之前肯定是不存在的, 执行完open之后才创建出来的。

context就是调用该函数时传入的属性对应的selinux上下文,如u:object_r:serialno_prop:s0。然后调用fsetxattr为文件设置selinux上下文。

到此为止,/dev/__properties__/u:object_r:serialno_prop:s0文件也创建出来了,对应的selinux上下文也设置好了。

接下来调用mmap将文件映射成一个prop_area对象。

ContextsSerialized::Initialize中是循环调用ContextNode的,所以会将/dev/__properties__/下所有的文件均创建出来, 并且都会设置好selinux上下文,创建所有的prop_area对象。

所以可以得知, prop_area, ContextNode,/dev/__properties__/下的每个文件, 还有 /dev/__properties__/property_info中的每个对象, 是一一对应的。

到现在为止,属性对应的文件和数据结构都已经创建完了,但是我们发现, 这些文件和数据结构都是空的。这里并没有将真正的属性值存储进来。我们接着往下分析。

property_info_area.LoadDefaultPath()

property_init中调用完__system_property_area_init()之后, 接下来会调用property_info_area.LoadDefaultPath(), 这个逻辑也是将 /dev/__properties__/property_info中的内容读取出来。

bool PropertyInfoAreaFile::LoadDefaultPath() {
  return LoadPath("/dev/__properties__/property_info");
}

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;
}

感觉和上面__system_property_area_init的逻辑重复了。不知道是不是多余的逻辑。没有看到更多的有用信息,先暂且不管。我们继续往下分析,找到属性值是如何存储到文件中的。

export_kernel_boot_props

上面我们分析过,init.cpp中,调用完property_init之后,会调用export_kernel_boot_props。该函数也定义在system/core/init/init.cpp中:

static void export_kernel_boot_props() {
    constexpr const char* UNSET = "";
    struct {
        const char *src_prop;
        const char *dst_prop;
        const char *default_value;
    } prop_map[] = {
        { "ro.boot.serialno",   "ro.serialno",   UNSET, },
        { "ro.boot.mode",       "ro.bootmode",   "unknown", },
        { "ro.boot.baseband",   "ro.baseband",   "unknown", },
        { "ro.boot.bootloader", "ro.bootloader", "unknown", },
        { "ro.boot.hardware",   "ro.hardware",   "unknown", },
        { "ro.boot.revision",   "ro.revision",   "0", },
    };
    for (const auto& prop : prop_map) {
        std::string value = GetProperty(prop.src_prop, prop.default_value);
        if (value != UNSET)
            property_set(prop.dst_prop, value);
    }
}

这里看到了我们所关心的ro.serialno, 可见它是从内核启动参数ro.boot.serialno而来的。可见这个属性有特别之处。我们先不管这中特殊情况,继续分析下面的代码,看一下普通的属性是如何写入的。

property_load_boot_defaults

上面我们分析过,init.cpp中,调用完export_kernel_boot_props之后,会调用property_load_boot_defaults,下面我们继续分析这个函数。

和property_init一样,该函数也定义在system/core/init/property_service.cpp中:

void property_load_boot_defaults(bool load_debug_prop) {
    // TODO(b/117892318): merge prop.default and build.prop files into one
    // We read the properties and their values into a map, in order to always allow properties
    // loaded in the later property files to override the properties in loaded in the earlier
    // property files, regardless of if they are "ro." properties or not.
    std::map<std::string, std::string> properties;
    if (!load_properties_from_file("/system/etc/prop.default", nullptr, &properties)) {
        // Try recovery path
        if (!load_properties_from_file("/prop.default", nullptr, &properties)) {
            // Try legacy path
            load_properties_from_file("/default.prop", nullptr, &properties);
        }
    }
    load_properties_from_file("/system/build.prop", nullptr, &properties);
    load_properties_from_file("/vendor/default.prop", nullptr, &properties);
    load_properties_from_file("/vendor/build.prop", nullptr, &properties);
    if (SelinuxGetVendorAndroidVersion() >= __ANDROID_API_Q__) {
        load_properties_from_file("/odm/etc/build.prop", nullptr, &properties);
    } else {
        load_properties_from_file("/odm/default.prop", nullptr, &properties);
        load_properties_from_file("/odm/build.prop", nullptr, &properties);
    }
    load_properties_from_file("/product/build.prop", nullptr, &properties);
    load_properties_from_file("/product_services/build.prop", nullptr, &properties);
    load_properties_from_file("/factory/factory.prop", "ro.*", &properties);

    if (load_debug_prop) {
        LOG(INFO) << "Loading " << kDebugRamdiskProp;
        load_properties_from_file(kDebugRamdiskProp, nullptr, &properties);
    }

    for (const auto& [name, value] : properties) {
        std::string error;
        if (PropertySet(name, value, &error) != PROP_SUCCESS) {
            LOG(ERROR) << "Could not set '" << name << "' to '" << value
                       << "' while loading .prop files" << error;
        }
    }

    property_initialize_ro_product_props();
    property_derive_build_fingerprint();

    update_sys_usb_config();
}

这个函数的逻辑也很清晰。

首先将系统各个位置的prop文件读取到std::map<std::string, std::string> properties中。

非root手机是无法读取这些文件的:

ls -lh /system/etc/prop.default
-rw------- 1 root root 1.5K 2009-01-01 08:00 /system/etc/prop.default
ls -lh /system/build.prop
-rw------- 1 root root 7.7K 2009-01-01 08:00 /system/build.prop

这些文件只对root用户有读写权限。为了编译理解代码, 我找来一台root的手机, 来看下文件中的内容:

ro.mi.development=false
ro.miui.version.code_time=1567440000
ro.rom.zone=1
ro.miui.ui.version.code=8
ro.miui.ui.version.name=V10
ro.miui.has_security_keyboard=1
ro.fota.oem=Xiaomi

可见这些文件才是真正的属性。

将这些文件读取到std::map<std::string, std::string> properties中之后, 会通过for循环调用PropertySet将所有属性写入。我们继续分析PropertySet。该函数定义在system/core/init/property_service.cpp中:

static uint32_t PropertySet(const std::string& name, const std::string& value, std::string* error) {
    size_t valuelen = value.size();

    if (!IsLegalPropertyName(name)) {
        *error = "Illegal property name";
        return PROP_ERROR_INVALID_NAME;
    }

    if (valuelen >= PROP_VALUE_MAX && !StartsWith(name, "ro.")) {
        *error = "Property value too long";
        return PROP_ERROR_INVALID_VALUE;
    }

    if (mbstowcs(nullptr, value.data(), 0) == static_cast<std::size_t>(-1)) {
        *error = "Value is not a UTF8 encoded string";
        return PROP_ERROR_INVALID_VALUE;
    }

    prop_info* pi = (prop_info*) __system_property_find(name.c_str());
    if (pi != nullptr) {
        // ro.* properties are actually "write-once".
        if (StartsWith(name, "ro.")) {
            *error = "Read-only property was already set";
            return PROP_ERROR_READ_ONLY_PROPERTY;
        }

        __system_property_update(pi, value.c_str(), valuelen);
    } else {
        int rc = __system_property_add(name.c_str(), name.size(), value.c_str(), valuelen);
        if (rc < 0) {
            *error = "__system_property_add failed";
            return PROP_ERROR_SET_FAILED;
        }
    }

    // Don't write properties to disk until after we have read all default
    // properties to prevent them from being overwritten by default values.
    if (persistent_properties_loaded && StartsWith(name, "persist.")) {
        WritePersistentProperty(name, value);
    }
    property_changed(name, value);
    return PROP_SUCCESS;
}

这个函数中的核心代码其实很少。 首先通过__system_property_find来获取对应的prop_info, 但是因为是第一次存储属性, 肯定会返回nullptr, 所以会继续调用__system_property_add来增加属性。

我们继续分析__system_property_add函数,该函数定义在bionic/libc/bionic/system_property_api.cpp中:

__BIONIC_WEAK_FOR_NATIVE_BRIDGE
int __system_property_add(const char* name, unsigned int namelen, const char* value,
                          unsigned int valuelen) {
  return system_properties.Add(name, namelen, value, valuelen);
}

该函数又调用SystemProperties类的Add方法。该方法实现在bionic/libc/system_properties/system_properties.cpp中:

int SystemProperties::Add(const char* name, unsigned int namelen, const char* value,
                          unsigned int valuelen) {
  if (valuelen >= PROP_VALUE_MAX && !is_read_only(name)) {
    return -1;
  }

  if (namelen < 1) {
    return -1;
  }

  if (!initialized_) {
    return -1;
  }

  prop_area* serial_pa = contexts_->GetSerialPropArea();
  if (serial_pa == nullptr) {
    return -1;
  }

  prop_area* pa = contexts_->GetPropAreaForName(name);
  if (!pa) {
    async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Access denied adding property \"%s\"", name);
    return -1;
  }

  bool ret = pa->add(name, namelen, value, valuelen);
  if (!ret) {
    return -1;
  }

  // There is only a single mutator, but we want to make sure that
  // updates are visible to a reader waiting for the update.
  atomic_store_explicit(serial_pa->serial(),
                        atomic_load_explicit(serial_pa->serial(), memory_order_relaxed) + 1,
                        memory_order_release);
  __futex_wake(serial_pa->serial(), INT32_MAX);
  return 0;
}

首先调用contexts_->GetPropAreaForName来获取prop_area,因为当前代码是在init中执行,init进程是root用户,所以肯定有权限获取prop_area。

根据前面的分析,我们已经知道一个prop_area其实就对应/dev/__properties__/下面的一个文件。比如 ro.serialno属性对应的prop_area,就是/dev/__properties__/u:object_r:serialno_prop:s0文件的内容映射(mmap)。

然后调用pa->add, 写入属性。可以认为这里就是真正将属性写入到prop_area中, 因为prop_area是/dev/__properties__/下文件的映射, 所以会直接同步到文件中。

prop_area的实现比较复杂,使用了trie树,并不是普通的键值对, /dev/__properties__/下的属性文件也不是普通文本文件,而是序列化的对象。所以我们就不再继续跟进代码了。大家可以看下bionic/libc/system_properties/include/system_properties/prop_area.h中的介绍。

// Properties are stored in a hybrid trie/binary tree structure.
// Each property's name is delimited at '.' characters, and the tokens are put
// into a trie structure.  Siblings at each level of the trie are stored in a
// binary tree.  For instance, "ro.secure"="1" could be stored as follows:
//
// +-----+   children    +----+   children    +--------+
// |     |-------------->| ro |-------------->| secure |
// +-----+               +----+               +--------+
//                       /    \                /   |
//                 left /      \ right   left /    |  prop   +===========+
//                     v        v            v     +-------->| ro.secure |
//                  +-----+   +-----+     +-----+            +-----------+
//                  | net |   | sys |     | com |            |     1     |
//                  +-----+   +-----+     +-----+            +===========+

// Represents a node in the trie.

还可以参考一下维基百科上面对trie树的介绍:

https://zh.wikipedia.org/wiki/Trie

到这里我们就已经分析完了init中对属性的初始化过程。总结一下包括如下几个步骤:

  1. 创建/dev/__properties__目录。
  2. 读取系统中的各个xxx_property_contexts文件中的属性名和属性对应的selinux上下文信息,将这些信息序列化到
    /dev/__properties__/property_info文件中。
  3. 通过读取/dev/__properties__/property_info,创建各种数据结构。其中ContextsSerialized可以看做属性selinux上下文的管理者。它会创建一个ContextNode数组,每个ContextNode代表一个属性以及属性的selinux上下文信息。然后每个ContextNode在Open的时候,会创建对应的/dev/__properties__/${selinux context}文件,为这个文件设置selinux context,然后将这个文件通过mmap映射到内存中,形成一个prop_area对象。
  4. 通过读取各个位置的属性文件(如/system/etc/prop.default,这些文件里面存放的是真正的属性键值对),将这些属性值设置到对应的prop_area对象中,因为该对象是/dev/__properties__/${selinux context}文件的内存映射, 所以同时也会写到文件中。

App读取属性是如何被拦截的

从上面的 App读取属性流程分析 一节中我们已经得知了一些属性拦截相关流程,再加上上一节对init中属性系统初始化的分析,我们对整个属性系统的细节已经有了更清晰的了解。

现在我们对App读取属性流程分析这一节进行一些总结:

  1. App也要对属性系统进行初始化。包括创建ContextsSerialized,解析读取/dev/__properties__/property_info文件创建ContextNode数组。ContextNode中保存的是属性名和属性selinux context的对应关系。
  2. 调用ContextsSerialized的GetPropAreaForName方法时, 因为没有权限,获取的是一个prop_area的空指针。

下面我们接着下面的代码来分析:

const prop_info* SystemProperties::Find(const char* name) {
  if (!initialized_) {
    return nullptr;
  }

  prop_area* pa = contexts_->GetPropAreaForName(name);
  if (!pa) {
    async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Access denied finding property \"%s\"", name);
    return nullptr;
  }

  return pa->find(name);
}

首先进入ContextsSerialized的GetPropAreaForName方法,实现在bionic/libc/system_properties/contexts_serialized.cpp中:

prop_area* ContextsSerialized::GetPropAreaForName(const char* name) {
  uint32_t index;
  property_info_area_file_->GetPropertyInfoIndexes(name, &index, nullptr);
  if (index == ~0u || index >= num_context_nodes_) {
    async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Could not find context for property \"%s\"",
                          name);
    return nullptr;
  }
  auto* context_node = &context_nodes_[index];
  if (!context_node->pa()) {
    // We explicitly do not check no_access_ in this case because unlike the
    // case of foreach(), we want to generate an selinux audit for each
    // non-permitted property access in this function.
    context_node->Open(false, nullptr);
  }
  return context_node->pa();
}

该方法中, 会根据属性名name, 获取对应的ContextNode对象, 通过调用pa方法获取一个prop_area指针,如果为空指针,则会调用Open来初始化这个prop_area指针,然后再次调用pa方法获取prop_area指针,并返回这个指针。

我们已经知道,因为没有权限读取属性,返回的prop_area肯定是空指针。也就是说,在Open方法中,无法成功创建这个prop_area对象。

Open方法有两个参数:

  1. access_rw, 传入false(init初始化属性系统时传入的该参数是true, 因为需要将属性写入,而App执行这个代码时传入的是false, 说明App只能读取)
  2. fsetxattr_failed,传入nullptr, 这我们不用关心。

下面我们去看一下Open方法为什么无法创建prop_area对象,该方法实现在bionic/libc/system_properties/context_node.cpp中:

bool ContextNode::Open(bool access_rw, bool* fsetxattr_failed) {
  lock_.lock();
  if (pa_) {
    lock_.unlock();
    return true;
  }

// 格式为 /dev/__properties__/u:object_r:serialno_prop:s0
  char filename[PROP_FILENAME_MAX];
  int len = async_safe_format_buffer(filename, sizeof(filename), "%s/%s", filename_, context_);
  if (len < 0 || len >= PROP_FILENAME_MAX) {
    lock_.unlock();
    return false;
  }

  if (access_rw) {
    pa_ = prop_area::map_prop_area_rw(filename, context_, fsetxattr_failed);
  } else {
    pa_ = prop_area::map_prop_area(filename);
  }
  lock_.unlock();
  return pa_;
}

首先根据filename_, context_两个参数拼接出filename, 拼接出的filename的格式为/dev/__properties__/${selinux context}, 比如/dev/__properties__/u:object_r:serialno_prop:s0

因为access_rw为false, 会通过prop_area::map_prop_area来获取prop_area指针。下面我们分析一下这个方法。该方法实现在bionic/libc/system_properties/prop_area.cpp

prop_area* prop_area::map_prop_area(const char* filename) {
  int fd = open(filename, O_CLOEXEC | O_NOFOLLOW | O_RDONLY);
  if (fd == -1) return nullptr;

  prop_area* map_result = map_fd_ro(fd);
  close(fd);

  return map_result;
}

逻辑比较简单。 首先通过open打开/dev/__properties__/${selinux context}文件,然后将该文件映射成prop_area。

之所以会无法获取prop_area对象,是因为open打开文件失败了。因为没有权限访问这个文件。回顾一下init初始化属性系统的时候,会创建这个文件, 并设置selinux上下文。比如:

文件 /dev/__properties__/u:object_r:serialno_prop:s0
对应的selinux context为 u:object_r:serialno_prop:s0

我们看一下Android中定义的和 u:object_r:serialno_prop:s0相关的策略,定义在system/sepolicy/prebuilts/api/29.0/public/domain.te文件中:

# Do not allow reading device's serial number from system properties except form
# a few whitelisted domains.
neverallow {
  domain
  -adbd
  -dumpstate
  -fastbootd
  -hal_camera_server
  -hal_cas_server
  -hal_drm_server
  -init
  -mediadrmserver
  -recovery
  -shell
  -system_server
  -vendor_init
} serialno_prop:file r_file_perms;

这个策略的意义是:除了init,adbd等特殊的域(domain)之外,其他域无法访问selinux context为serialno_prop的文件。

这里的域可以认为是selinux给进程设置的一个标签,每个进程有不同的标签, 每个标签对应不同的策略, 所以就可以实现不同的进程访问不同的文件。

通过ps -Z可以显示进程的selinux域:

我们看一下init进程的域为 init:

1901:/ $ ps -eZ | grep init
u:r:init:s0                    root              1      0 2221092   3296 0                   0 S init

adbd进程的域为adbd:

1901:/ $ ps -eZ | grep adb                                                                                                                                            
u:r:adbd:s0                    shell         12956      1 2323304   4588 0                   0 S adbd

所以跟根据selinux策略, 他们可以访问/dev/__properties__/u:object_r:serialno_prop:s0读取序列号。

再看一下普通App的selinux域:

1901:/ $ ps -eZ | grep com.smile.gifmaker                                                                                                                             
u:r:untrusted_app_27:s0:c512,c768 u0_a299     6305    842 10498244 476308 0                  0 S com.smile.gifmaker
u:r:untrusted_app_27:s0:c512,c768 u0_a299     6557    842 6627940 193148 0                   0 S com.smile.gifmaker:messagesdk
u:r:untrusted_app_27:s0:c512,c768 u0_a299     6773    842 6705184 202628 0                   0 S com.smile.gifmaker:pushservice
1901:/ $ ps -eZ | grep com.tencent.mobileqq                                                                                                                           
u:r:untrusted_app_27:s0:c512,c768 u0_a296     6094    845 1639688 150228 0                   0 S com.tencent.mobileqq:MSF
u:r:untrusted_app_27:s0:c512,c768 u0_a296     6117    845 1668920 203432 0                   0 S com.tencent.mobileqq
1901:/ $ ps -eZ | grep com.ss.android.ugc.aweme                                                                                                                       
u:r:untrusted_app_29:s0:c41,c257,c512,c768 u0_a297 8504 842 11194652 473736 0                0 S com.ss.android.ugc.aweme
u:r:untrusted_app_29:s0:c41,c257,c512,c768 u0_a297 8888 842 6901604 213232 0                 0 S com.ss.android.ugc.aweme:push
u:r:untrusted_app_29:s0:c41,c257,c512,c768 u0_a297 8964 842 6903492 214656 0                 0 S com.ss.android.ugc.aweme:pushservice

普通App的selinux域都是untrusted_app,根据selinux策略, 普通App是无法访问/dev/__properties__/u:object_r:serialno_prop:s0文件的,也就无法读取文件中的ro.serialno属性。

到此为止, 我们就明白了Android是如何通过selinux来保护ro.serialno属性不被随意读取的。

不光是ro.serialno属性,其他属性也会有独立的selinux策略来保护。

可见,Android对用户隐私数据的保护力度越来越大了,也越来越细。


总结

本文我们主要通过跟踪init初始化属性系统App读取属性这两个关键流程,来对系统属性进行了较为全面的分析,厘清了为什么普通App无法读取关键属性。同时对属性系统中的关键细节进行了分析, 包括一些关键目录,关键文件,关键对象和关键数据结构。


相关源码

frameworks/base/core/java/android/os/Build.java
frameworks/base/core/res/AndroidManifest.xml
frameworks/base/services/core/java/com/android/server/os/DeviceIdentifiersPolicyService.java
frameworks/base/core/java/android/os/SystemProperties.java
frameworks/base/core/jni/android_os_SystemProperties.cpp
system/core/base/properties.cpp
system/core/base/include/android-base/properties.h
bionic/tools/versioner/current/sys/cdefs.h
bionic/libc/bionic/system_property_api.cpp
bionic/libc/system_properties/include/system_properties/system_properties.h
bionic/libc/system_properties/system_properties.cpp
bionic/libc/system_properties/contexts_serialized.cpp
system/core/property_service/libpropertyinfoparser/property_info_parser.cpp
system/core/init/init.cpp
system/core/init/property_service.cpp
system/sepolicy/private/property_contexts
bionic/libc/bionic/system_property_api.cpp
bionic/libc/system_properties/context_node.cpp
bionic/libc/system_properties/prop_area.cpp
bionic/libc/bionic/system_property_api.cpp
bionic/libc/system_properties/include/system_properties/prop_area.h
system/sepolicy/prebuilts/api/29.0/public/domain.te

该文中引用的源码是Android 10的,不同的Android版本可能会有差异

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值