java/com/facebook/soloader/UnpackingSoSource.java (440 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook.soloader; import android.content.Context; import android.os.Parcel; import android.os.StrictMode; import android.util.Log; import java.io.Closeable; import java.io.DataInput; import java.io.DataOutput; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.io.SyncFailedException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; /** {@link SoSource} that extracts libraries from an APK to the filesystem. */ public abstract class UnpackingSoSource extends DirectorySoSource { private static final String TAG = "fb-UnpackingSoSource"; private static final String STATE_FILE_NAME = "dso_state"; private static final String LOCK_FILE_NAME = "dso_lock"; private static final String INSTANCE_LOCK_FILE_NAME = "dso_instance_lock"; private static final String DEPS_FILE_NAME = "dso_deps"; private static final String MANIFEST_FILE_NAME = "dso_manifest"; protected static final byte STATE_DIRTY = 0; protected static final byte STATE_CLEAN = 1; private static final byte MANIFEST_VERSION = 1; protected final Context mContext; @Nullable protected String mCorruptedLib; protected @Nullable FileLocker mInstanceLock; @Nullable private String[] mAbis; private final Map<String, Object> mLibsBeingLoaded = new HashMap<>(); protected UnpackingSoSource(Context context, String name) { super(getSoStorePath(context, name), RESOLVE_DEPENDENCIES); mContext = context; } protected UnpackingSoSource(Context context, File storePath) { super(storePath, RESOLVE_DEPENDENCIES); mContext = context; } public static File getSoStorePath(Context context, String name) { return new File(context.getApplicationInfo().dataDir + "/" + name); } // The state can be either STATE_DIRTY or STATE_CLEAN. // If state is STATE_DIRTY, it means that either last unpacking did not finish // successfully, or dependencies changed. Either way, we have to wipe everything // and unpack again. // If state is STATE_CLEAN, last unpacking finished successfully, so we have // a valid dso store, but PREPARE_FLAG_FORCE_REFRESH flag was passed, so we // might want to regenerate the store for some other reason, such as a // corrupted lib or to change the compression of the libraries in the store. protected abstract Unpacker makeUnpacker(byte state) throws IOException; @Override public String[] getSoSourceAbis() { if (mAbis == null) { return super.getSoSourceAbis(); } return mAbis; } public void setSoSourceAbis(final String[] abis) { mAbis = abis; } public static class Dso { public final String name; public final String hash; public Dso(String name, String hash) { this.name = name; this.hash = hash; } } public static final class DsoManifest { public final Dso[] dsos; public DsoManifest(Dso[] dsos) { this.dsos = dsos; } /** @return Dso manifest, or {@code null} if manifest is corrupt or illegible. */ static final DsoManifest read(DataInput xdi) throws IOException { int version = xdi.readByte(); if (version != MANIFEST_VERSION) { throw new RuntimeException("wrong dso manifest version"); } int nrDso = xdi.readInt(); if (nrDso < 0) { throw new RuntimeException("illegal number of shared libraries"); } Dso[] dsos = new Dso[nrDso]; for (int i = 0; i < nrDso; ++i) { dsos[i] = new Dso(xdi.readUTF(), xdi.readUTF()); } return new DsoManifest(dsos); } public final void write(DataOutput xdo) throws IOException { xdo.writeByte(MANIFEST_VERSION); xdo.writeInt(dsos.length); for (int i = 0; i < dsos.length; ++i) { xdo.writeUTF(dsos[i].name); xdo.writeUTF(dsos[i].hash); } } } protected static interface InputDso extends Closeable { public void write(DataOutput out, byte[] ioBuffer) throws IOException; public Dso getDso(); public String getFileName(); public int available() throws IOException; public InputStream getStream(); }; public static class InputDsoStream implements InputDso { private final Dso dso; private final InputStream content; public InputDsoStream(Dso dso, InputStream content) { this.dso = dso; this.content = content; } @Override public void write(DataOutput out, byte[] ioBuffer) throws IOException { SysUtil.copyBytes(out, content, Integer.MAX_VALUE, ioBuffer); } @Override public Dso getDso() { return dso; } @Override public String getFileName() { return dso.name; } @Override public int available() throws IOException { return content.available(); } @Override public InputStream getStream() { return content; } @Override public void close() throws IOException { content.close(); } } protected abstract static class InputDsoIterator implements Closeable { public abstract boolean hasNext(); public abstract @Nullable InputDso next() throws IOException; @Override public void close() throws IOException { /* By default, do nothing */ } } protected abstract static class Unpacker implements Closeable { public abstract DsoManifest getDsoManifest() throws IOException; public abstract InputDsoIterator openDsoIterator() throws IOException; @Override public void close() throws IOException { /* By default, do nothing */ } } private static void writeState(File stateFileName, byte state) throws IOException { try (RandomAccessFile stateFile = new RandomAccessFile(stateFileName, "rw")) { stateFile.seek(0); stateFile.write(state); stateFile.setLength(stateFile.getFilePointer()); stateFile.getFD().sync(); } catch (SyncFailedException e) { Log.w(TAG, "state file sync failed", e); } } protected String getSoNameFromFileName(String fileName) { return fileName; } /** Delete files not mentioned in the given DSO list. */ private void deleteUnmentionedFiles(Dso[] dsos) throws IOException { String[] existingFiles = soDirectory.list(); if (existingFiles == null) { throw new IOException("unable to list directory " + soDirectory); } for (int i = 0; i < existingFiles.length; ++i) { String fileName = existingFiles[i]; if (fileName.equals(STATE_FILE_NAME) || fileName.equals(LOCK_FILE_NAME) || fileName.equals(INSTANCE_LOCK_FILE_NAME) || fileName.equals(DEPS_FILE_NAME) || fileName.equals(MANIFEST_FILE_NAME)) { continue; } boolean found = false; for (int j = 0; !found && j < dsos.length; ++j) { if ((dsos[j].name).equals(getSoNameFromFileName(fileName))) { found = true; } } if (!found) { File fileNameToDelete = new File(soDirectory, fileName); Log.v(TAG, "deleting unaccounted-for file " + fileNameToDelete); SysUtil.dumbDeleteRecursive(fileNameToDelete); } } } private void extractDso(InputDso iDso, byte[] ioBuffer) throws IOException { Log.i(TAG, "extracting DSO " + iDso.getDso().name); try { if (!soDirectory.setWritable(true)) { throw new IOException("cannot make directory writable for us: " + soDirectory); } extractDsoImpl(iDso, ioBuffer); } finally { if (!soDirectory.setWritable(false)) { Log.w(TAG, "error removing " + soDirectory.getCanonicalPath() + " write permission"); } } } private void extractDsoImpl(InputDso iDso, byte[] ioBuffer) throws IOException { File dsoFileName = new File(soDirectory, iDso.getFileName()); RandomAccessFile dsoFile = null; try { if (dsoFileName.exists() && !dsoFileName.setWritable(true)) { Log.w(TAG, "error adding write permission to: " + dsoFileName); } try { dsoFile = new RandomAccessFile(dsoFileName, "rw"); } catch (IOException ex) { Log.w(TAG, "error overwriting " + dsoFileName + " trying to delete and start over", ex); SysUtil.dumbDeleteRecursive(dsoFileName); // Throws on error; not existing is okay dsoFile = new RandomAccessFile(dsoFileName, "rw"); } int sizeHint = iDso.available(); if (sizeHint > 1) { SysUtil.fallocateIfSupported(dsoFile.getFD(), sizeHint); } iDso.write(dsoFile, ioBuffer); dsoFile.setLength(dsoFile.getFilePointer()); // In case we shortened file if (!dsoFileName.setExecutable(true /* allow exec... */, false /* ...for everyone */)) { throw new IOException("cannot make file executable: " + dsoFileName); } } catch (IOException e) { SysUtil.dumbDeleteRecursive(dsoFileName); throw e; } finally { if (!dsoFileName.setWritable(false)) { Log.w(TAG, "error removing " + dsoFileName + " write permission"); } if (dsoFile != null) { dsoFile.close(); } } } private void regenerate(byte state, DsoManifest desiredManifest, InputDsoIterator dsoIterator) throws IOException { Log.v(TAG, "regenerating DSO store " + getClass().getName()); File manifestFileName = new File(soDirectory, MANIFEST_FILE_NAME); try (RandomAccessFile manifestFile = new RandomAccessFile(manifestFileName, "rw")) { DsoManifest existingManifest = null; if (state == STATE_CLEAN) { try { existingManifest = DsoManifest.read(manifestFile); } catch (Exception ex) { Log.i(TAG, "error reading existing DSO manifest", ex); } } if (existingManifest == null) { existingManifest = new DsoManifest(new Dso[0]); } deleteUnmentionedFiles(desiredManifest.dsos); byte[] ioBuffer = new byte[32 * 1024]; while (dsoIterator.hasNext()) { try (InputDso iDso = dsoIterator.next()) { boolean obsolete = true; for (int i = 0; obsolete && i < existingManifest.dsos.length; ++i) { String iDsoName = iDso.getDso().name; if (existingManifest.dsos[i].name.equals(iDsoName) && existingManifest.dsos[i].hash.equals(iDso.getDso().hash)) { obsolete = false; } } File dsoFile = new File(soDirectory, iDso.getFileName()); if (!dsoFile.exists()) { // so file exists, but file name changed obsolete = true; } if (obsolete) { extractDso(iDso, ioBuffer); } } } } Log.v(TAG, "Finished regenerating DSO store " + getClass().getName()); } protected boolean depsChanged(final byte[] existingDeps, final byte[] deps) { return !Arrays.equals(existingDeps, deps); } protected boolean refreshLocked(final FileLocker lock, final int flags, final byte[] deps) throws IOException { final File stateFileName = new File(soDirectory, STATE_FILE_NAME); byte state; try (RandomAccessFile stateFile = new RandomAccessFile(stateFileName, "rw")) { try { state = stateFile.readByte(); if (state != STATE_CLEAN) { Log.v(TAG, "dso store " + soDirectory + " regeneration interrupted: wiping clean"); state = STATE_DIRTY; } } catch (EOFException ex) { state = STATE_DIRTY; } } final File depsFileName = new File(soDirectory, DEPS_FILE_NAME); DsoManifest desiredManifest = null; try (RandomAccessFile depsFile = new RandomAccessFile(depsFileName, "rw")) { byte[] existingDeps = new byte[(int) depsFile.length()]; if (depsFile.read(existingDeps) != existingDeps.length) { Log.v(TAG, "short read of so store deps file: marking unclean"); state = STATE_DIRTY; } if (depsChanged(existingDeps, deps)) { Log.v(TAG, "deps mismatch on deps store: regenerating"); state = STATE_DIRTY; } if (state == STATE_DIRTY || ((flags & SoSource.PREPARE_FLAG_FORCE_REFRESH) != 0)) { Log.v(TAG, "so store dirty: regenerating"); writeState(stateFileName, STATE_DIRTY); try (Unpacker u = makeUnpacker(state)) { desiredManifest = u.getDsoManifest(); try (InputDsoIterator idi = u.openDsoIterator()) { regenerate(state, desiredManifest, idi); } } } } if (desiredManifest == null) { return false; // No sync needed } final DsoManifest manifest = desiredManifest; Runnable syncer = createSyncer(lock, deps, stateFileName, depsFileName, manifest, false); if ((flags & PREPARE_FLAG_ALLOW_ASYNC_INIT) != 0) { new Thread(syncer, "SoSync:" + soDirectory.getName()).start(); } else { syncer.run(); } return true; } private Runnable createSyncer( final FileLocker lock, final byte[] deps, final File stateFileName, final File depsFileName, final DsoManifest manifest, final Boolean quietly) { return new Runnable() { @Override public void run() { try { try { Log.v(TAG, "starting syncer worker"); // N.B. We can afford to write the deps file and the manifest file without // synchronization or fsyncs because we've marked the DSO store STATE_DIRTY, which // will cause us to ignore all intermediate state when regenerating it. That is, // it's okay for the depsFile or manifestFile blocks to hit the disk before the // actual DSO data file blocks as long as both hit the disk before we reset // STATE_CLEAN. try (RandomAccessFile depsFile = new RandomAccessFile(depsFileName, "rw")) { depsFile.write(deps); depsFile.setLength(depsFile.getFilePointer()); } File manifestFileName = new File(soDirectory, MANIFEST_FILE_NAME); try (RandomAccessFile manifestFile = new RandomAccessFile(manifestFileName, "rw")) { manifest.write(manifestFile); } SysUtil.fsyncRecursive(soDirectory); writeState(stateFileName, STATE_CLEAN); } finally { Log.v(TAG, "releasing dso store lock for " + soDirectory + " (from syncer thread)"); lock.close(); } } catch (IOException ex) { if (!quietly) { throw new RuntimeException(ex); } } } }; } /** * Return an opaque blob of bytes that represents all the dependencies of this SoSource; if this * block differs from one we've previously saved, we go through the heavyweight refresh process * that involves calling {@link Unpacker#getDsoManifest} and {@link Unpacker#openDsoIterator}. * * <p>Subclasses should override this method if {@link Unpacker#getDsoManifest} is expensive. * * @return dependency block */ protected byte[] getDepsBlock() throws IOException { // Parcel is fine: we never parse the parceled bytes, so it's okay if the byte representation // changes beneath us. Parcel parcel = Parcel.obtain(); try (Unpacker u = makeUnpacker(STATE_CLEAN)) { Dso[] dsos = u.getDsoManifest().dsos; parcel.writeByte(MANIFEST_VERSION); parcel.writeInt(dsos.length); for (int i = 0; i < dsos.length; ++i) { parcel.writeString(dsos[i].name); parcel.writeString(dsos[i].hash); } } byte[] depsBlock = parcel.marshall(); parcel.recycle(); return depsBlock; } protected @Nullable FileLocker getOrCreateLock(File lockFileName, boolean blocking) throws IOException { return SysUtil.getOrCreateLockOnDir(soDirectory, lockFileName, blocking); } /** Verify or refresh the state of the shared library store. */ @Override protected void prepare(int flags) throws IOException { SysUtil.mkdirOrThrow(soDirectory); // LOCK_FILE_NAME is used to synchronize changes in the dso store. File lockFileName = new File(soDirectory, LOCK_FILE_NAME); FileLocker lock = getOrCreateLock(lockFileName, true); // INSTANCE_LOCK_FILE_NAME is used to signal to other processes/threads that // there is an initialized SoSource from which DSOs might be getting loaded. // This lock is held for the entire lifetime of the process. // This prevents from doing changes to DSOs which might prevent previously // initialized SoSources from loading libraries. if (mInstanceLock == null) { File instanceLockFileName = new File(soDirectory, INSTANCE_LOCK_FILE_NAME); mInstanceLock = getOrCreateLock(instanceLockFileName, false); } try { Log.v(TAG, "locked dso store " + soDirectory); if (refreshLocked(lock, flags, getDepsBlock())) { lock = null; // Lock transferred to syncer thread } else { Log.i(TAG, "dso store is up-to-date: " + soDirectory); } } finally { if (lock != null) { Log.v(TAG, "releasing dso store lock for " + soDirectory); lock.close(); } else { Log.v(TAG, "not releasing dso store lock for " + soDirectory + " (syncer thread started)"); } } } private Object getLibraryLock(String soName) { synchronized (mLibsBeingLoaded) { Object lock = mLibsBeingLoaded.get(soName); if (lock == null) { lock = new Object(); mLibsBeingLoaded.put(soName, lock); } return lock; } } @Override @Nullable public String getLibraryPath(String soName) throws IOException { File soFile = getSoFileByName(soName); if (soFile == null) { return null; } return soFile.getCanonicalPath(); } /** Prepare this SoSource extracting a corrupted library. */ protected synchronized void prepare(String soName) throws IOException { // Only one thread at a time can try to recover a corrupted lib from the same source Object lock = getLibraryLock(soName); synchronized (lock) { // While recovering, do not allow loading the same lib from another thread mCorruptedLib = soName; prepare(SoSource.PREPARE_FLAG_FORCE_REFRESH); } } @Override public int loadLibrary(String soName, int loadFlags, StrictMode.ThreadPolicy threadPolicy) throws IOException { Object lock = getLibraryLock(soName); synchronized (lock) { // Holds a lock on the specific library being loaded to avoid trying to recover it in another // thread while loading return loadLibraryFrom(soName, loadFlags, soDirectory, threadPolicy); } } }