• Aucun résultat trouvé

Multi-User External Storage

Dans le document Android Security Internals (Page 132-138)

In order to uphold the Android security model in a multi-user environ-ment, the Android Compatibility Definition Document (CDD) places numerous requirements on external storage. The most important of these is that “Each user instance on an Android device MUST have separate and isolated external storage directories.” 4

Unfortunately, implementing this requirement poses a problem because external storage has traditionally been world-readable and implemented using the FAT filesystem, which does not support permissions. Google’s implementation of multi-user external storage leverages three Linux kernel features in order to provide backward-compatible, per-user external storage:

mount namespaces, bind mounts, and shared subtrees.

Advanced Linux Mount Features

As in other Unix systems, Linux manages all files from all storage devices as part of a single directory tree. Each filesystem is linked to a specific subtree by mounting it at a specified directory, called the mount point. Traditionally, the directory tree has been shared by all processes, and each process sees the same directory hierarchy.

3. “Filesystem in Userspace,” http://fuse.sourceforge.net/

4. Google, Android 4.4 Compatibility Definition, “9.5. Multi-User Support,” http://static

Linux 2.4.19 and later versions added support for per-process mount namespaces, which allows each process to have its own set of mount points and thus use a directory hierarchy different from that of other processes.5 The current list of mounts for each process can be read from the /proc/PID/

mounts virtual file, where PID is the process ID. A forked Linux process can request a separate mount namespace by specifying the CLONE_NEWNS flag to the Linux-specific clone()6 and unshare()7 system calls. In this case, the namespace of the parent process is referred to as the parent namespace.

A bind mount allows a directory or file to be mounted at another path in the directory tree, making the same file or directory visible at multiple loca-tions. A bind mount is created by specifying the MS_BIND flag to the mount()

system call, or by passing the --bind parameter to the mount command.

Finally, shared subtrees,8 which were first introduced in Linux 2.6.15, pro-vide a way to control how filesystem mounts are propagated across mount namespaces. Shared subtrees make it possible for a process to have its own namespace but still access filesystems that are mounted after it starts.

Shared subtrees provide four different mount types, of which Android uses the shared and slave mount. A shared mount created in a parent namespace propagates to all child namespaces and is thus visible to all processes that have cloned off a namespace. A slave mount has a master mount that is a shared mount, and also propagates new mounts. However, the propagation is one-way only: mounts at the master propagate to the slave, but mounts at the slave do not propagate to the master. This scheme allows a process to keep its mounts invisible to any other process, while still being able to see shared system mounts. Shared mounts are created by passing the MS_SHARED

flag to the mount() system call, while creating slave mounts requires passing the MS_SLAVE flag.

Android Implementation

Since Android 4.4, mounting external storage directly is no longer sup-ported but is emulated using the FUSE sdcard daemon, even when the underlying device is a physical SD card. We’ll base our discussion on a con-figuration that is backed by a directory on internal storage, which is typical for devices without a physical SD card. (The official documentation9 con-tains more details on other possible configurations.)

On a device where primary external storage is backed by internal stor-age, the sdcard FUSE daemon uses the /data/media/ directory as a source and

5. Michael Kerrisk, The Linux Programming Interface: A Linux and UNIX System Programming Handbook, No Starch Press, 2010, pp. 261

6. Ibid., 598 7. Ibid., 603

8. Linux Kernel, Shared Subtrees, https://www.kernel.org/doc/Documentation/filesystems/

sharedsubtree.txt

9. Google, “External Storage: Typical Configuration Examples,” http://source.android.com/

devices/tech/storage/config-example.html

creates an emulated filesystem at /mnt/shell/emulated. Listing 4-12 shows how the sdcard service is declared in the device-specific init.rc file in this case {.

--snip--on init

mkdir /mnt/shell/emulated 0700 shell shellu mkdir /storage/emulated 0555 root rootv

export EXTERNAL_STORAGE /storage/emulated/legacyw export EMULATED_STORAGE_SOURCE /mnt/shell/emulatedx export EMULATED_STORAGE_TARGET /storage/emulatedy # Support legacy paths

symlink /storage/emulated/legacy /sdcardz symlink /storage/emulated/legacy /mnt/sdcard symlink /storage/emulated/legacy /storage/sdcard0 symlink /mnt/shell/emulated/0 /storage/emulated/legacy

# virtual sdcard daemon running as media_rw (1023)

service sdcard /system/bin/sdcard -u 1023 -g 1023 -l /data/media /mnt/shell/emulated{

class late_start

--snip--Listing 4-12: sdcard service declaration for emulated external storage

Here, the -u and -g options specify the user and group the daemon should run as, and -l specifies the layout used for emulated storage (dis-cussed later in this section). As you can see at u, the /mnt/shell/emulated/

directory (available via the EMULATED_STORAGE_SOURCE environment variable x) is owned and only accessible by the shell user. Its contents might look like Listing 4-13 on a device with five users.

# ls -l /mnt/shell/emulated/

drwxrwx--x root sdcard_r 0 drwxrwx--x root sdcard_r 10 drwxrwx--x root sdcard_r 11 drwxrwx--x root sdcard_r 12 drwxrwx--x root sdcard_r 13 drwxrwx--x root sdcard_r legacy drwxrwx--x root sdcard_r obb Listing 4-13: Contents of /mnt/shell/emulated/

As with app data directories, each user gets a dedicated external stor-age data directory named after their user ID. Android uses a combina-tion of mount namespaces and bind mounts in order to make each user’s external storage data directory available only to the applications that the user starts, without showing them other users’ data directories. Because all applications are forked off the zygote process (discussed in Chapter 2), exter-nal storage setup is implemented in two steps: the first one is common to all processes, and the second is specific to each process. First, mount points

that are shared by all forked app processes are set up in the unique zygote process. Then dedicated mount points, which are visible only to that pro-cess, are set up as part of each app’s process specialization.

Let’s first look at the shared part in the zygote process. Listing 4-14 shows an excerpt of the initZygote() function (found in dalvik/vm/Init.cpp) that highlights mount point setup.

static bool initZygote() {

setpgid(0,0);

if (unshare(CLONE_NEWNS) == -1) {u return -1;

}

// Mark rootfs as being a slave so that changes from default // namespace only flow into our children.

if (mount("rootfs", "/", NULL, (MS_SLAVE | MS_REC), NULL) == -1) {v return -1;

}

const char* target_base = getenv("EMULATED_STORAGE_TARGET");

if (target_base != NULL) {

if (mount("tmpfs", target_base, "tmpfs", MS_NOSUID | MS_NODEV,w "uid=0,gid=1028,mode=0751") == -1) {

return -1;

} } return true;

}

Listing 4-14: Mount point setup in zygote

Here, zygote passes the CLONE_NEWNS flag to the unshare() system call u in order to create a new, private mount namespace that will be shared by all its children (app processes). It then marks the root filesystem (mounted at /) as a slave by passing the MS_SLAVE flag to the mount() system call v. This ensures that changes from the default mount namespace, such as mounting encrypted containers or removable storage, only propagate to its children, while at the same time making sure that any mounts created by children do not propagate into the default namespace. Finally, zygote creates the memory-backed EMULATED_STORAGE_TARGET (usually /storage/emulated/) mount point by creating a tmpfs filesystem w, which children use to bind mount external storage into their private namespaces.

Listing 4-15 shows the process-specific mount point setup found in dalvik/vm/native/dalvik_system_Zygote.cpp that is executed when forking each app process off zygote. (Error handling, logging, and some variable declara-tions have been omitted.)

static int mountEmulatedStorage(uid_t uid, u4 mountMode) { userid_t userid = multiuser_get_user_id(uid);u

// Create a second private mount namespace for our process if (unshare(CLONE_NEWNS) == -1) {v

return -1;

}

// Create bind mounts to expose external storage if (mountMode == MOUNT_EXTERNAL_MULTIUSER

|| mountMode == MOUNT_EXTERNAL_MULTIUSER_ALL) { // These paths must already be created by init.rc const char* source = getenv("EMULATED_STORAGE_SOURCE");w const char* target = getenv("EMULATED_STORAGE_TARGET");x const char* legacy = getenv("EXTERNAL_STORAGE");y if (source == NULL || target == NULL || legacy == NULL) { return -1;

}

// /mnt/shell/emulated/0

snprintf(source_user, PATH_MAX, "%s/%d", source, userid);z // /storage/emulated/0

snprintf(target_user, PATH_MAX, "%s/%d", target, userid);{

if (mountMode == MOUNT_EXTERNAL_MULTIUSER_ALL) { // Mount entire external storage tree for all users if (mount(source, target, NULL, MS_BIND, NULL) == -1) { return -1;

} } else {

// Only mount user-specific external storage

if (mount(source_user, target_user, NULL, MS_BIND, NULL) == -1) {|

return -1;

} }

// Finally, mount user-specific path into place for legacy users if (mount(target_user, legacy, NULL, MS_BIND | MS_REC, NULL) == -1) {}

return -1;

} } else { return -1;

} return 0;

}

Listing 4-15: External storage setup for app processes

Here, the mountEmulatedStorage() function first obtains the current user ID from the process UID u, then uses the unshare() system call to create a new mount namespace for the process by passing the CLONE_NEWNS flag v.

The function then obtains the values of the EMULATED_STORAGE_SOURCE w,

EMULATED_STORAGE_TARGET x, and EXTERNAL_STORAGE y environment variables, which are all initialized in the device-specific init.rc file (see w, x, and y in Listing 4-12). It then prepares the mount source z and target { directory paths based on the values of EMULATED_STORAGE_SOURCE, EMULATED_STORAGE_TARGET, and the current user ID.

The directories are created if they don’t exist, and then the method bind mounts the source directory (such as /mnt/shell/emulated/0 for the owner user) at the target path (for example, /storage/emulated/0 for the owner user) |. This ensures that external storage is accessible from the Android shell (started with the adb shell command), which is used exten-sively for application development and debugging.

The final step is to recursively bind mount the target directory at the fixed legacy directory (/storage/emulated/legacy/) }. The legacy directory is symlinked to /sdcard/ in the device-specific init.rc file (z in Listing 4-12) for backward compatibility with apps that hardcode this path (normally obtained using the android.os.Environment.getExternalStorageDirectory() API).

After all steps have been executed, the newly created app process is guaranteed to see only the external storage allotted to the user that started it. We can verify this by looking at the list of mounts for two app process executed by different users as shown in Listing 4-16.

# cat /proc/7382/mounts

Listing 4-16: List of mount points for process started by different users

Here, the process started by the owner user with PID 7382 has a /storage/

emulated/0 mount point v, which is a bind mount of /mnt/shell/emulated/0/, and process 7538 (started by a secondary user) has a /storage/emulated/10 mount point y, which is a bind mount of /mnt/shell/emulated/10/.

Because neither process has a mount point for the other process’s exter-nal storage directory, each process can only see and modify its own files.

Both processes have a /storage/emulated/legacy mount point (w and z), but because it is bound to different directories (/storage/emulated/0/ and /mnt/

shell/emulated/10/, respectively), each process sees different contents. Both process can see /mnt/shell/emulated/ (u and x), but because this directory is only accessible to the shell user (permissions 0700), app processes cannot see its contents.

Dans le document Android Security Internals (Page 132-138)