Reconcile private volumes when mounted.

Many things can happen while a private volume is ejected, so we need
to reconcile newly mounted volumes against known state.

First, user IDs can be recycled, so we store the serial number in the
extended attributes of the /data/user/[id] directory inode.  Since a
serial number is always unique, we can quickly determine if a user
directory "10" really belongs to the current user "10".  When we
detect a mismatched serial number, we destroy all data belonging to
that user.  Gracefully handles upgrade case and assumes current serial
number is valid when none is defined.

Second, we destroy apps that we find no record of, either due to
uninstallation while the volume was unmounted, or reinstallation on
another volume.

When mounting a volume, ensure that data directories exist for all
current users.  Similarly, create data directories on all mounted
volumes when creating a user.  When forgetting a volume, gracefully
uninstall any apps that had been installed on that volume.

Bug: 20674082, 20275572
Change-Id: I4e3448837f7c03daf00d71681ebdc96e3d8b9cc9
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index c82ba24..dc0c471 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -2831,7 +2831,8 @@
                 Slog.i(TAG, "Trying to bind to DefaultContainerService");
 
             Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
-            if (mContext.bindService(service, mDefContainerConn, Context.BIND_AUTO_CREATE)) {
+            if (mContext.bindServiceAsUser(service, mDefContainerConn, Context.BIND_AUTO_CREATE,
+                    UserHandle.OWNER)) {
                 mBound = true;
                 return true;
             }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 477c26c..4217c59 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20342,8 +20342,9 @@
         if (info == null) return null;
         ApplicationInfo newInfo = new ApplicationInfo(info);
         newInfo.uid = applyUserId(info.uid, userId);
-        newInfo.dataDir = PackageManager.getDataDirForUser(info.volumeUuid, info.packageName,
-                userId).getAbsolutePath();
+        newInfo.dataDir = Environment
+                .getDataUserPackageDirectory(info.volumeUuid, userId, info.packageName)
+                .getAbsolutePath();
         return newInfo;
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index ca24e3a..2abd924 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -219,29 +219,17 @@
         synchronized (mSessions) {
             readSessionsLocked();
 
-            final File internalStagingDir = buildInternalStagingDir();
-            final ArraySet<File> unclaimedStages = Sets.newArraySet(
-                    internalStagingDir.listFiles(sStageFilter));
+            reconcileStagesLocked(StorageManager.UUID_PRIVATE_INTERNAL);
+
             final ArraySet<File> unclaimedIcons = Sets.newArraySet(
                     mSessionsDir.listFiles());
 
             // Ignore stages and icons claimed by active sessions
             for (int i = 0; i < mSessions.size(); i++) {
                 final PackageInstallerSession session = mSessions.valueAt(i);
-                unclaimedStages.remove(session.stageDir);
                 unclaimedIcons.remove(buildAppIconFile(session.sessionId));
             }
 
-            // Clean up orphaned staging directories
-            for (File stage : unclaimedStages) {
-                Slog.w(TAG, "Deleting orphan stage " + stage);
-                if (stage.isDirectory()) {
-                    mPm.mInstaller.rmPackageDir(stage.getAbsolutePath());
-                } else {
-                    stage.delete();
-                }
-            }
-
             // Clean up orphaned icons
             for (File icon : unclaimedIcons) {
                 Slog.w(TAG, "Deleting orphan icon " + icon);
@@ -255,6 +243,36 @@
         mStorage = mContext.getSystemService(StorageManager.class);
     }
 
+    private void reconcileStagesLocked(String volumeUuid) {
+        final File stagingDir = buildStagingDir(volumeUuid);
+        final ArraySet<File> unclaimedStages = Sets.newArraySet(
+                stagingDir.listFiles(sStageFilter));
+
+        // Ignore stages claimed by active sessions
+        for (int i = 0; i < mSessions.size(); i++) {
+            final PackageInstallerSession session = mSessions.valueAt(i);
+            unclaimedStages.remove(session.stageDir);
+        }
+
+        // Clean up orphaned staging directories
+        for (File stage : unclaimedStages) {
+            Slog.w(TAG, "Deleting orphan stage " + stage);
+            synchronized (mPm.mInstallLock) {
+                if (stage.isDirectory()) {
+                    mPm.mInstaller.rmPackageDir(stage.getAbsolutePath());
+                } else {
+                    stage.delete();
+                }
+            }
+        }
+    }
+
+    public void onPrivateVolumeMounted(String volumeUuid) {
+        synchronized (mSessions) {
+            reconcileStagesLocked(volumeUuid);
+        }
+    }
+
     public void onSecureContainersAvailable() {
         synchronized (mSessions) {
             final ArraySet<String> unclaimed = new ArraySet<>();
@@ -713,25 +731,11 @@
         throw new IllegalStateException("Failed to allocate session ID");
     }
 
-    private File buildInternalStagingDir() {
-        return new File(Environment.getDataDirectory(), "app");
+    private File buildStagingDir(String volumeUuid) {
+        return Environment.getDataAppDirectory(volumeUuid);
     }
 
-    private File buildStagingDir(String volumeUuid) throws FileNotFoundException {
-        if (volumeUuid == null) {
-            return buildInternalStagingDir();
-        } else {
-            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
-            if (vol != null && vol.type == VolumeInfo.TYPE_PRIVATE
-                    && vol.isMountedWritable()) {
-                return new File(vol.path, "app");
-            } else {
-                throw new FileNotFoundException("Failed to find volume for UUID " + volumeUuid);
-            }
-        }
-    }
-
-    private File buildStageDir(String volumeUuid, int sessionId) throws FileNotFoundException {
+    private File buildStageDir(String volumeUuid, int sessionId) {
         final File stagingDir = buildStagingDir(volumeUuid);
         return new File(stagingDir, "vmdl" + sessionId + ".tmp");
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 305eb8e..9a11397 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -191,6 +191,7 @@
 import libcore.util.EmptyArray;
 
 import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IMediaContainerService;
 import com.android.internal.app.ResolverActivity;
 import com.android.internal.content.NativeLibraryHelper;
@@ -430,6 +431,7 @@
 
     // Used for privilege escalation. MUST NOT BE CALLED WITH mPackages
     // LOCK HELD.  Can be called with mInstallLock held.
+    @GuardedBy("mInstallLock")
     final Installer mInstaller;
 
     /** Directory where installed third-party apps stored */
@@ -457,6 +459,7 @@
     // Keys are String (package name), values are Package.  This also serves
     // as the lock for the global state.  Methods that must be called with
     // this lock held have the prefix "LP".
+    @GuardedBy("mPackages")
     final ArrayMap<String, PackageParser.Package> mPackages =
             new ArrayMap<String, PackageParser.Package>();
 
@@ -1607,9 +1610,19 @@
         public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
             if (vol.type == VolumeInfo.TYPE_PRIVATE) {
                 if (vol.state == VolumeInfo.STATE_MOUNTED) {
-                    // TODO: ensure that private directories exist for all active users
-                    // TODO: remove user data whose serial number doesn't match
+                    final String volumeUuid = vol.getFsUuid();
+
+                    // Clean up any users or apps that were removed or recreated
+                    // while this volume was missing
+                    reconcileUsers(volumeUuid);
+                    reconcileApps(volumeUuid);
+
+                    // Clean up any install sessions that expired or were
+                    // cancelled while this volume was missing
+                    mInstallerService.onPrivateVolumeMounted(volumeUuid);
+
                     loadPrivatePackages(vol);
+
                 } else if (vol.state == VolumeInfo.STATE_EJECTING) {
                     unloadPrivatePackages(vol);
                 }
@@ -1626,7 +1639,17 @@
 
         @Override
         public void onVolumeForgotten(String fsUuid) {
-            // TODO: remove all packages hosted on this uuid
+            // Remove any apps installed on the forgotten volume
+            synchronized (mPackages) {
+                final List<PackageSetting> packages = mSettings.getVolumePackagesLPr(fsUuid);
+                for (PackageSetting ps : packages) {
+                    Slog.d(TAG, "Destroying " + ps.name + " because volume was forgotten");
+                    deletePackage(ps.name, new LegacyPackageDeleteObserver(null).getBinder(),
+                            UserHandle.USER_OWNER, PackageManager.DELETE_ALL_USERS);
+                }
+
+                mSettings.writeLPr();
+            }
         }
     };
 
@@ -2772,8 +2795,9 @@
                 pkg.applicationInfo.packageName = packageName;
                 pkg.applicationInfo.flags = ps.pkgFlags | ApplicationInfo.FLAG_IS_DATA_ONLY;
                 pkg.applicationInfo.privateFlags = ps.pkgPrivateFlags;
-                pkg.applicationInfo.dataDir = PackageManager.getDataDirForUser(ps.volumeUuid,
-                        packageName, userId).getAbsolutePath();
+                pkg.applicationInfo.dataDir = Environment
+                        .getDataUserPackageDirectory(ps.volumeUuid, userId, packageName)
+                        .getAbsolutePath();
                 pkg.applicationInfo.primaryCpuAbi = ps.primaryCpuAbiString;
                 pkg.applicationInfo.secondaryCpuAbi = ps.secondaryCpuAbiString;
             }
@@ -6617,8 +6641,8 @@
 
         } else {
             // This is a normal package, need to make its data directory.
-            dataPath = PackageManager.getDataDirForUser(pkg.volumeUuid, pkg.packageName,
-                    UserHandle.USER_OWNER);
+            dataPath = Environment.getDataUserPackageDirectory(pkg.volumeUuid,
+                    UserHandle.USER_OWNER, pkg.packageName);
 
             boolean uidError = false;
             if (dataPath.exists()) {
@@ -6772,6 +6796,18 @@
         if (DEBUG_INSTALL) Slog.i(TAG, "Linking native library dir for " + path);
         final int[] userIds = sUserManager.getUserIds();
         synchronized (mInstallLock) {
+            // Make sure all user data directories are ready to roll; we're okay
+            // if they already exist
+            if (!TextUtils.isEmpty(pkg.volumeUuid)) {
+                for (int userId : userIds) {
+                    if (userId != 0) {
+                        mInstaller.createUserData(pkg.volumeUuid, pkg.packageName,
+                                UserHandle.getUid(userId, pkg.applicationInfo.uid), userId,
+                                pkg.applicationInfo.seinfo);
+                    }
+                }
+            }
+
             // Create a native library symlink only if we have native libraries
             // and if the native libraries are 32 bit libraries. We do not provide
             // this symlink for 64 bit libraries.
@@ -11443,8 +11479,8 @@
         String pkgName = pkg.packageName;
 
         if (DEBUG_INSTALL) Slog.d(TAG, "installNewPackageLI: " + pkg);
-        final boolean dataDirExists = PackageManager.getDataDirForUser(volumeUuid, pkgName,
-                UserHandle.USER_OWNER).exists();
+        final boolean dataDirExists = Environment
+                .getDataUserPackageDirectory(volumeUuid, UserHandle.USER_OWNER, pkgName).exists();
         synchronized(mPackages) {
             if (mSettings.mRenamedPackages.containsKey(pkgName)) {
                 // A package with the same name is already installed, though
@@ -12301,6 +12337,8 @@
             final IPackageDeleteObserver2 observer, final int userId, final int flags) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.DELETE_PACKAGES, null);
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(observer);
         final int uid = Binder.getCallingUid();
         if (UserHandle.getUserId(uid) != userId) {
             mContext.enforceCallingPermission(
@@ -15239,6 +15277,127 @@
         sendResourcesChangedBroadcast(false, false, unloaded, null);
     }
 
+    /**
+     * Examine all users present on given mounted volume, and destroy data
+     * belonging to users that are no longer valid, or whose user ID has been
+     * recycled.
+     */
+    private void reconcileUsers(String volumeUuid) {
+        final File[] files = Environment.getDataUserDirectory(volumeUuid).listFiles();
+        if (ArrayUtils.isEmpty(files)) {
+            Slog.d(TAG, "No users found on " + volumeUuid);
+            return;
+        }
+
+        for (File file : files) {
+            if (!file.isDirectory()) continue;
+
+            final int userId;
+            final UserInfo info;
+            try {
+                userId = Integer.parseInt(file.getName());
+                info = sUserManager.getUserInfo(userId);
+            } catch (NumberFormatException e) {
+                Slog.w(TAG, "Invalid user directory " + file);
+                continue;
+            }
+
+            boolean destroyUser = false;
+            if (info == null) {
+                logCriticalInfo(Log.WARN, "Destroying user directory " + file
+                        + " because no matching user was found");
+                destroyUser = true;
+            } else {
+                try {
+                    UserManagerService.enforceSerialNumber(file, info.serialNumber);
+                } catch (IOException e) {
+                    logCriticalInfo(Log.WARN, "Destroying user directory " + file
+                            + " because we failed to enforce serial number: " + e);
+                    destroyUser = true;
+                }
+            }
+
+            if (destroyUser) {
+                synchronized (mInstallLock) {
+                    mInstaller.removeUserDataDirs(volumeUuid, userId);
+                }
+            }
+        }
+
+        final UserManager um = mContext.getSystemService(UserManager.class);
+        for (UserInfo user : um.getUsers()) {
+            final File userDir = Environment.getDataUserDirectory(volumeUuid, user.id);
+            if (userDir.exists()) continue;
+
+            try {
+                UserManagerService.prepareUserDirectory(userDir);
+                UserManagerService.enforceSerialNumber(userDir, user.serialNumber);
+            } catch (IOException e) {
+                Log.wtf(TAG, "Failed to create user directory on " + volumeUuid, e);
+            }
+        }
+    }
+
+    /**
+     * Examine all apps present on given mounted volume, and destroy apps that
+     * aren't expected, either due to uninstallation or reinstallation on
+     * another volume.
+     */
+    private void reconcileApps(String volumeUuid) {
+        final File[] files = Environment.getDataAppDirectory(volumeUuid).listFiles();
+        if (ArrayUtils.isEmpty(files)) {
+            Slog.d(TAG, "No apps found on " + volumeUuid);
+            return;
+        }
+
+        for (File file : files) {
+            final boolean isPackage = (isApkFile(file) || file.isDirectory())
+                    && !PackageInstallerService.isStageName(file.getName());
+            if (!isPackage) {
+                // Ignore entries which are not packages
+                continue;
+            }
+
+            boolean destroyApp = false;
+            String packageName = null;
+            try {
+                final PackageLite pkg = PackageParser.parsePackageLite(file,
+                        PackageParser.PARSE_MUST_BE_APK);
+                packageName = pkg.packageName;
+
+                synchronized (mPackages) {
+                    final PackageSetting ps = mSettings.mPackages.get(packageName);
+                    if (ps == null) {
+                        logCriticalInfo(Log.WARN, "Destroying " + packageName + " on + "
+                                + volumeUuid + " because we found no install record");
+                        destroyApp = true;
+                    } else if (!TextUtils.equals(volumeUuid, ps.volumeUuid)) {
+                        logCriticalInfo(Log.WARN, "Destroying " + packageName + " on "
+                                + volumeUuid + " because we expected it on " + ps.volumeUuid);
+                        destroyApp = true;
+                    }
+                }
+
+            } catch (PackageParserException e) {
+                logCriticalInfo(Log.WARN, "Destroying " + file + " due to parse failure: " + e);
+                destroyApp = true;
+            }
+
+            if (destroyApp) {
+                synchronized (mInstallLock) {
+                    if (packageName != null) {
+                        removeDataDirsLI(volumeUuid, packageName);
+                    }
+                    if (file.isDirectory()) {
+                        mInstaller.rmPackageDir(file.getAbsolutePath());
+                    } else {
+                        file.delete();
+                    }
+                }
+            }
+        }
+    }
+
     private void unfreezePackage(String packageName) {
         synchronized (mPackages) {
             final PackageSetting ps = mSettings.mPackages.get(packageName);
@@ -15538,14 +15697,11 @@
             // Technically, we shouldn't be doing this with the package lock
             // held.  However, this is very rare, and there is already so much
             // other disk I/O going on, that we'll let it slide for now.
-            final StorageManager storage = StorageManager.from(mContext);
-            final List<VolumeInfo> vols = storage.getVolumes();
-            for (VolumeInfo vol : vols) {
-                if (vol.getType() == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
-                    final String volumeUuid = vol.getFsUuid();
-                    if (DEBUG_INSTALL) Slog.d(TAG, "Removing user data on volume " + volumeUuid);
-                    mInstaller.removeUserDataDirs(volumeUuid, userHandle);
-                }
+            final StorageManager storage = mContext.getSystemService(StorageManager.class);
+            for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
+                final String volumeUuid = vol.getFsUuid();
+                if (DEBUG_INSTALL) Slog.d(TAG, "Removing user data on volume " + volumeUuid);
+                mInstaller.removeUserDataDirs(volumeUuid, userHandle);
             }
         }
         mUserNeedsBadging.delete(userHandle);
@@ -15599,10 +15755,10 @@
     }
 
     /** Called by UserManagerService */
-    void createNewUserLILPw(int userHandle, File path) {
+    void createNewUserLILPw(int userHandle) {
         if (mInstaller != null) {
             mInstaller.createUserConfig(userHandle);
-            mSettings.createNewUserLILPw(this, mInstaller, userHandle, path);
+            mSettings.createNewUserLILPw(this, mInstaller, userHandle);
             applyFactoryDefaultBrowserLPw(userHandle);
         }
     }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 0ad2b4a..9c23af3 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -3613,11 +3613,7 @@
         }
     }
 
-    void createNewUserLILPw(PackageManagerService service, Installer installer,
-            int userHandle, File path) {
-        path.mkdir();
-        FileUtils.setPermissions(path.toString(), FileUtils.S_IRWXU | FileUtils.S_IRWXG
-                | FileUtils.S_IXOTH, -1, -1);
+    void createNewUserLILPw(PackageManagerService service, Installer installer, int userHandle) {
         for (PackageSetting ps : mPackages.values()) {
             if (ps.pkg == null || ps.pkg.applicationInfo == null) {
                 continue;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 4300df6..1a79b4e 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -42,6 +42,11 @@
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
 import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
@@ -137,6 +142,8 @@
     static final int WRITE_USER_MSG = 1;
     static final int WRITE_USER_DELAY = 2*1000;  // 2 seconds
 
+    private static final String XATTR_SERIAL = "user.serial";
+
     private final Context mContext;
     private final PackageManagerService mPm;
     private final Object mInstallLock;
@@ -146,7 +153,6 @@
 
     private final File mUsersDir;
     private final File mUserListFile;
-    private final File mBaseUserPath;
 
     private final SparseArray<UserInfo> mUsers = new SparseArray<UserInfo>();
     private final SparseArray<Bundle> mUserRestrictions = new SparseArray<Bundle>();
@@ -210,7 +216,6 @@
                 // Make zeroth user directory, for services to migrate their files to that location
                 File userZeroDir = new File(mUsersDir, "0");
                 userZeroDir.mkdirs();
-                mBaseUserPath = baseUserPath;
                 FileUtils.setPermissions(mUsersDir.toString(),
                         FileUtils.S_IRWXU|FileUtils.S_IRWXG
                         |FileUtils.S_IROTH|FileUtils.S_IXOTH,
@@ -1237,7 +1242,6 @@
                     }
                     int userId = getNextAvailableIdLocked();
                     userInfo = new UserInfo(userId, name, null, flags);
-                    File userPath = new File(mBaseUserPath, Integer.toString(userId));
                     userInfo.serialNumber = mNextSerialNumber++;
                     long now = System.currentTimeMillis();
                     userInfo.creationTime = (now > EPOCH_PLUS_30_YEARS) ? now : 0;
@@ -1252,7 +1256,19 @@
                         }
                         userInfo.profileGroupId = parent.profileGroupId;
                     }
-                    mPm.createNewUserLILPw(userId, userPath);
+                    final StorageManager storage = mContext.getSystemService(StorageManager.class);
+                    for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
+                        final String volumeUuid = vol.getFsUuid();
+                        try {
+                            final File userDir = Environment.getDataUserDirectory(volumeUuid,
+                                    userId);
+                            prepareUserDirectory(userDir);
+                            enforceSerialNumber(userDir, userInfo.serialNumber);
+                        } catch (IOException e) {
+                            Log.wtf(LOG_TAG, "Failed to create user directory on " + volumeUuid, e);
+                        }
+                    }
+                    mPm.createNewUserLILPw(userId);
                     userInfo.partial = false;
                     scheduleWriteUserLocked(userInfo);
                     updateUserIdsLocked();
@@ -1856,6 +1872,87 @@
         return RESTRICTIONS_FILE_PREFIX + packageName + XML_SUFFIX;
     }
 
+    /**
+     * Create new {@code /data/user/[id]} directory and sets default
+     * permissions.
+     */
+    public static void prepareUserDirectory(File file) throws IOException {
+        if (!file.exists()) {
+            if (!file.mkdir()) {
+                throw new IOException("Failed to create " + file);
+            }
+        }
+        if (FileUtils.setPermissions(file.getAbsolutePath(), 0771, Process.SYSTEM_UID,
+                Process.SYSTEM_UID) != 0) {
+            throw new IOException("Failed to prepare " + file);
+        }
+    }
+
+    /**
+     * Enforce that serial number stored in user directory inode matches the
+     * given expected value. Gracefully sets the serial number if currently
+     * undefined.
+     *
+     * @throws IOException when problem extracting serial number, or serial
+     *             number is mismatched.
+     */
+    public static void enforceSerialNumber(File file, int serialNumber) throws IOException {
+        final int foundSerial = getSerialNumber(file);
+        Slog.v(LOG_TAG, "Found " + file + " with serial number " + foundSerial);
+
+        if (foundSerial == -1) {
+            Slog.d(LOG_TAG, "Serial number missing on " + file + "; assuming current is valid");
+            try {
+                setSerialNumber(file, serialNumber);
+            } catch (IOException e) {
+                Slog.w(LOG_TAG, "Failed to set serial number on " + file, e);
+            }
+
+        } else if (foundSerial != serialNumber) {
+            throw new IOException("Found serial number " + foundSerial
+                    + " doesn't match expected " + serialNumber);
+        }
+    }
+
+    /**
+     * Set serial number stored in user directory inode.
+     *
+     * @throws IOException if serial number was already set
+     */
+    private static void setSerialNumber(File file, int serialNumber)
+            throws IOException {
+        try {
+            final byte[] buf = Integer.toString(serialNumber).getBytes(StandardCharsets.UTF_8);
+            Os.setxattr(file.getAbsolutePath(), XATTR_SERIAL, buf, OsConstants.XATTR_CREATE);
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+    }
+
+    /**
+     * Return serial number stored in user directory inode.
+     *
+     * @return parsed serial number, or -1 if not set
+     */
+    private static int getSerialNumber(File file) throws IOException {
+        try {
+            final byte[] buf = new byte[256];
+            final int len = Os.getxattr(file.getAbsolutePath(), XATTR_SERIAL, buf);
+            final String serial = new String(buf, 0, len);
+            try {
+                return Integer.parseInt(serial);
+            } catch (NumberFormatException e) {
+                throw new IOException("Bad serial number: " + serial);
+            }
+        } catch (ErrnoException e) {
+            if (e.errno == OsConstants.ENODATA) {
+                return -1;
+            } else {
+                throw e.rethrowAsIOException();
+            }
+        }
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)