From 9415ba0ae4401e94f5fb6760aa72b6515549da67 Mon Sep 17 00:00:00 2001 From: JW Wang Date: Fri, 6 Nov 2020 10:22:15 +0800 Subject: [PATCH] Populate APK files using hard links (2/n) This is an effort to improve the storage usage of RollbackManager. When committing a rollback, the apk/apex files will be deleted after the install session is committed successfully. We can use hard links to effectively transfer the files to PackageInstaller to avoid copy. Bug: 168562373 Test: atest RollbackTest StagedRollbackTest Change-Id: I749a17d769d1845bb0f9b34c8757b69b40b516f9 --- .../content/pm/IPackageInstallerSession.aidl | 1 + .../android/content/pm/PackageInstaller.java | 25 ++++++++++++++++++ .../server/pm/PackageInstallerSession.java | 26 +++++++++++++++++++ .../com/android/server/rollback/Rollback.java | 15 ++++++++--- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index 6ccbc36e26f6a..9a73be9d44a44 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -33,6 +33,7 @@ interface IPackageInstallerSession { ParcelFileDescriptor openRead(String name); void write(String name, long offsetBytes, long lengthBytes, in ParcelFileDescriptor fd); + void stageViaHardLink(String target); void addChecksums(String name, in Checksum[] checksums); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 0dcfd38294b93..d4a98f82a4f77 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -1048,6 +1048,31 @@ public class PackageInstaller { } } + /** + * Populate an APK file by creating a hard link to avoid the need to copy. + *

+ * Note this API is used by RollbackManager only and can only be called from system_server. + * {@code target} will be relabeled if link is created successfully. RollbackManager has + * to delete {@code target} when the session is committed successfully to avoid SELinux + * label conflicts. + *

+ * Note No more bytes should be written to the file once the link is created successfully. + * + * @param target the path of the link target + * + * @hide + */ + public void stageViaHardLink(String target) throws IOException { + try { + mSession.stageViaHardLink(target); + } catch (RuntimeException e) { + ExceptionUtils.maybeUnwrapIOException(e); + throw e; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Ensure that any outstanding data for given stream has been committed * to disk. This is only valid for streams returned from diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index c10d394d63442..f51d5146df87a 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -110,6 +110,7 @@ import android.os.ParcelableException; import android.os.Process; import android.os.RemoteException; import android.os.RevocableFileDescriptor; +import android.os.SELinux; import android.os.SystemProperties; import android.os.UserHandle; import android.os.incremental.IStorageHealthListener; @@ -1092,6 +1093,31 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + @Override + public void stageViaHardLink(String path) { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.SYSTEM_UID) { + throw new SecurityException("link() can only be run by the system"); + } + + try { + final File target = new File(path); + final File source = new File(stageDir, target.getName()); + try { + Os.link(path, source.getAbsolutePath()); + // Grant READ access for APK to be read successfully + Os.chmod(source.getAbsolutePath(), 0644); + } catch (ErrnoException e) { + e.rethrowAsIOException(); + } + if (!SELinux.restorecon(source)) { + throw new IOException("Can't relabel file: " + source); + } + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + private ParcelFileDescriptor openTargetInternal(String path, int flags, int mode) throws IOException, ErrnoException { // TODO: this should delegate to DCS so the system process avoids diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java index 63ed416f2859f..d9b67024018b2 100644 --- a/services/core/java/com/android/server/rollback/Rollback.java +++ b/services/core/java/com/android/server/rollback/Rollback.java @@ -581,9 +581,18 @@ class Rollback { ParcelFileDescriptor.MODE_READ_ONLY)) { final long token = Binder.clearCallingIdentity(); try { - session.write(packageCodePath.getName(), 0, - packageCodePath.length(), - fd); + boolean fallbackToCopy = false; + try { + // Populate apk/apex files using hard links to avoid copy + session.stageViaHardLink(packageCodePath.getAbsolutePath()); + } catch (Exception ignore) { + fallbackToCopy = true; + } + if (fallbackToCopy) { + session.write(packageCodePath.getName(), 0, + packageCodePath.length(), + fd); + } } finally { Binder.restoreCallingIdentity(token); }