From 9fb00183a04036a58ee208f5bfd6c9768982f0aa Mon Sep 17 00:00:00 2001 From: Daichi Hirono Date: Tue, 8 Nov 2016 14:12:17 +0900 Subject: [PATCH 1/2] Add StorageManager#openProxyFileDescriptor. The CL: * Adds StorageManager#openProxyFileDescriptor API. * Turns IProxyFileDescriptorCallback into a class so that it can provides default implementations. * Removes mActive state from FuseAppLoop, because the state will be managed at the MountService side. * Adds StorageManagerIntegrationTest to check if FUSE is correctly mounted or not. Since it's implementation details, CTS does not help to test the behavior. Test: StorageManagerIntegrationTest Bug: 29970149 Change-Id: Id78dd4abcf9325820e9c77c264f54bfa77b85a92 --- ....java => ProxyFileDescriptorCallback.java} | 24 +++- .../android/os/storage/IStorageManager.aidl | 3 + .../android/os/storage/StorageManager.java | 78 +++++++++- .../com/android/internal/os/AppFuseMount.aidl | 19 +++ .../com/android/internal/os/AppFuseMount.java | 24 +++- .../com/android/internal/os/FuseAppLoop.java | 112 ++++++++------- .../com_android_internal_os_FuseAppLoop.cpp | 11 +- .../StorageManagerIntegrationTest.java | 50 ++++++- .../android/server/StorageManagerService.java | 136 +++++++++++++++++- .../android/server/storage/AppFuseBridge.java | 124 +++++++++------- 10 files changed, 454 insertions(+), 127 deletions(-) rename core/java/android/os/{IProxyFileDescriptorCallback.java => ProxyFileDescriptorCallback.java} (69%) create mode 100644 core/java/com/android/internal/os/AppFuseMount.aidl diff --git a/core/java/android/os/IProxyFileDescriptorCallback.java b/core/java/android/os/ProxyFileDescriptorCallback.java similarity index 69% rename from core/java/android/os/IProxyFileDescriptorCallback.java rename to core/java/android/os/ProxyFileDescriptorCallback.java index e41e19422a27b..23848b9ba27d8 100644 --- a/core/java/android/os/IProxyFileDescriptorCallback.java +++ b/core/java/android/os/ProxyFileDescriptorCallback.java @@ -17,18 +17,21 @@ package android.os; import android.system.ErrnoException; +import android.system.OsConstants; /** * Callback that handles file system requests from ProxyFileDescriptor. * @hide */ -public interface IProxyFileDescriptorCallback { +public abstract class ProxyFileDescriptorCallback { /** * Returns size of bytes provided by the file descriptor. * @return Size of bytes * @throws ErrnoException */ - long onGetSize() throws ErrnoException; + public long onGetSize() throws ErrnoException { + throw new ErrnoException("onGetSize", OsConstants.EBADF); + } /** * Provides bytes read from file descriptor. @@ -39,7 +42,9 @@ public interface IProxyFileDescriptorCallback { * @return Size of bytes returned by the function. * @throws ErrnoException */ - int onRead(long offset, int size, byte[] data) throws ErrnoException; + public int onRead(long offset, int size, byte[] data) throws ErrnoException { + throw new ErrnoException("onRead", OsConstants.EBADF); + } /** * Handles bytes written to file descriptor. @@ -49,11 +54,20 @@ public interface IProxyFileDescriptorCallback { * @return Size of bytes processed by the function. * @throws ErrnoException */ - int onWrite(long offset, int size, byte[] data) throws ErrnoException; + public int onWrite(long offset, int size, byte[] data) throws ErrnoException { + throw new ErrnoException("onWrite", OsConstants.EBADF); + } /** * Processes fsync request. * @throws ErrnoException */ - void onFsync() throws ErrnoException; + public void onFsync() throws ErrnoException { + throw new ErrnoException("onFsync", OsConstants.EINVAL); + } + + /** + * Invoked after the file is closed. + */ + abstract public void onRelease(); } diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl index 27c0526d3e07b..59394b2c58d77 100644 --- a/core/java/android/os/storage/IStorageManager.aidl +++ b/core/java/android/os/storage/IStorageManager.aidl @@ -25,6 +25,7 @@ import android.os.storage.IObbActionListener; import android.os.storage.StorageVolume; import android.os.storage.VolumeInfo; import android.os.storage.VolumeRecord; +import com.android.internal.os.AppFuseMount; /** * WARNING! Update IMountService.h and IMountService.cpp if you change this @@ -289,4 +290,6 @@ interface IStorageManager { void addUserKeyAuth(int userId, int serialNumber, in byte[] token, in byte[] secret) = 70; void fixateNewestUserKeyAuth(int userId) = 71; void fstrim(int flags) = 72; + AppFuseMount mountProxyFileDescriptorBridge() = 73; + ParcelFileDescriptor openProxyFileDescriptor(int mountPointId, int fileId, int mode) = 74; } diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 03fd8d3af0612..d5acbeb61b9d7 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -30,6 +30,7 @@ import android.os.Binder; import android.os.Environment; import android.os.FileUtils; import android.os.Handler; +import android.os.ProxyFileDescriptorCallback; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; @@ -44,7 +45,10 @@ import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; - +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.AppFuseMount; +import com.android.internal.os.FuseAppLoop; import com.android.internal.os.RoSystemProperties; import com.android.internal.os.SomeArgs; import com.android.internal.util.Preconditions; @@ -62,6 +66,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /** @@ -1322,6 +1327,77 @@ public class StorageManager { } } + /** {@hide} */ + @VisibleForTesting + public @NonNull ParcelFileDescriptor openProxyFileDescriptor( + int mode, ProxyFileDescriptorCallback callback, ThreadFactory factory) + throws IOException { + // Retry is needed because the mount point mFuseAppLoop is using may be unmounted before + // invoking StorageManagerService#openProxyFileDescriptor. In this case, we need to re-mount + // the bridge by calling mountProxyFileDescriptorBridge. + int retry = 3; + while (retry-- > 0) { + try { + synchronized (mFuseAppLoopLock) { + if (mFuseAppLoop == null) { + final AppFuseMount mount = mStorageManager.mountProxyFileDescriptorBridge(); + if (mount == null) { + Log.e(TAG, "Failed to open proxy file bridge."); + throw new IOException("Failed to open proxy file bridge."); + } + mFuseAppLoop = FuseAppLoop.open(mount.mountPointId, mount.fd, factory); + } + + try { + final int fileId = mFuseAppLoop.registerCallback(callback); + final ParcelFileDescriptor pfd = + mStorageManager.openProxyFileDescriptor( + mFuseAppLoop.getMountPointId(), fileId, mode); + if (pfd != null) { + return pfd; + } + // Probably the bridge is being unmounted but mFuseAppLoop has not been + // noticed it yet. + mFuseAppLoop.unregisterCallback(fileId); + } catch (FuseAppLoop.UnmountedException error) { + Log.d(TAG, "mFuseAppLoop has been already unmounted."); + mFuseAppLoop = null; + continue; + } + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + break; + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + throw new IOException("Failed to mount bridge."); + } + + /** {@hide} */ + public @NonNull ParcelFileDescriptor openProxyFileDescriptor( + int mode, ProxyFileDescriptorCallback callback) + throws IOException { + return openProxyFileDescriptor(mode, callback, null); + } + + /** {@hide} */ + @VisibleForTesting + public int getProxyFileDescriptorMountPointId() { + synchronized (mFuseAppLoopLock) { + return mFuseAppLoop != null ? mFuseAppLoop.getMountPointId() : -1; + } + } + + private final Object mFuseAppLoopLock = new Object(); + + @GuardedBy("mFuseAppLoopLock") + private @Nullable FuseAppLoop mFuseAppLoop = null; + /// Consts to match the password types in cryptfs.h /** @hide */ public static final int CRYPT_TYPE_PASSWORD = 0; diff --git a/core/java/com/android/internal/os/AppFuseMount.aidl b/core/java/com/android/internal/os/AppFuseMount.aidl new file mode 100644 index 0000000000000..66cf95b753576 --- /dev/null +++ b/core/java/com/android/internal/os/AppFuseMount.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.internal.os; + +parcelable AppFuseMount; diff --git a/core/java/com/android/internal/os/AppFuseMount.java b/core/java/com/android/internal/os/AppFuseMount.java index b392186ccc771..04d72117d28ad 100644 --- a/core/java/com/android/internal/os/AppFuseMount.java +++ b/core/java/com/android/internal/os/AppFuseMount.java @@ -19,14 +19,26 @@ package com.android.internal.os; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; -import java.io.File; +import android.os.storage.IStorageManager; +import com.android.internal.util.Preconditions; +/** + * Parcelable class representing AppFuse mount. + * This conveys the result for IStorageManager#openProxyFileDescriptor. + * @see IStorageManager#openProxyFileDescriptor + */ public class AppFuseMount implements Parcelable { - final public File mountPoint; + final public int mountPointId; final public ParcelFileDescriptor fd; - public AppFuseMount(File mountPoint, ParcelFileDescriptor fd) { - this.mountPoint = mountPoint; + /** + * @param mountPointId Integer number for mount point that is unique in the lifetime of + * StorageManagerService. + * @param fd File descriptor pointing /dev/fuse and tagged with the mount point. + */ + public AppFuseMount(int mountPointId, ParcelFileDescriptor fd) { + Preconditions.checkNotNull(fd); + this.mountPointId = mountPointId; this.fd = fd; } @@ -37,7 +49,7 @@ public class AppFuseMount implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(this.mountPoint.getPath()); + dest.writeInt(this.mountPointId); dest.writeParcelable(fd, flags); } @@ -45,7 +57,7 @@ public class AppFuseMount implements Parcelable { new Parcelable.Creator() { @Override public AppFuseMount createFromParcel(Parcel in) { - return new AppFuseMount(new File(in.readString()), in.readParcelable(null)); + return new AppFuseMount(in.readInt(), in.readParcelable(null)); } @Override diff --git a/core/java/com/android/internal/os/FuseAppLoop.java b/core/java/com/android/internal/os/FuseAppLoop.java index 34253ce96a06f..3603b6df11dad 100644 --- a/core/java/com/android/internal/os/FuseAppLoop.java +++ b/core/java/com/android/internal/os/FuseAppLoop.java @@ -18,34 +18,38 @@ package com.android.internal.os; import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.IProxyFileDescriptorCallback; +import android.os.ProxyFileDescriptorCallback; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import android.system.OsConstants; import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; +import java.util.concurrent.ThreadFactory; public class FuseAppLoop { private static final String TAG = "FuseAppLoop"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); public static final int ROOT_INODE = 1; private static final int MIN_INODE = 2; + private static final ThreadFactory sDefaultThreadFactory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, TAG); + } + }; private final Object mLock = new Object(); - private final File mParent; + private final int mMountPointId; + private final Thread mThread; @GuardedBy("mLock") private final SparseArray mCallbackMap = new SparseArray<>(); - @GuardedBy("mLock") - private boolean mActive = true; - /** * Sequential number can be used as file name and inode in AppFuse. * 0 is regarded as an error, 1 is mount point. So we start the number from 2. @@ -53,35 +57,40 @@ public class FuseAppLoop { @GuardedBy("mLock") private int mNextInode = MIN_INODE; - private FuseAppLoop(@NonNull File parent) { - mParent = parent; - } - - public static @NonNull FuseAppLoop open( - @NonNull File parent, @NonNull ParcelFileDescriptor fd) { - Preconditions.checkNotNull(parent); - Preconditions.checkNotNull(fd); - final FuseAppLoop bridge = new FuseAppLoop(parent); + private FuseAppLoop( + int mountPointId, @NonNull ParcelFileDescriptor fd, @Nullable ThreadFactory factory) { + mMountPointId = mountPointId; final int rawFd = fd.detachFd(); - new Thread(new Runnable() { + if (factory == null) { + factory = sDefaultThreadFactory; + } + mThread = factory.newThread(new Runnable() { @Override public void run() { - bridge.native_start_loop(rawFd); + // rawFd is closed by native_start_loop. Java code does not need to close it. + native_start_loop(rawFd); } - }, TAG).start(); - return bridge; + }); } - public @NonNull ParcelFileDescriptor openFile(int mode, IProxyFileDescriptorCallback callback) + public static @NonNull FuseAppLoop open(int mountPointId, @NonNull ParcelFileDescriptor fd, + @Nullable ThreadFactory factory) { + Preconditions.checkNotNull(fd); + final FuseAppLoop loop = new FuseAppLoop(mountPointId, fd, factory); + loop.mThread.start(); + return loop; + } + + public int registerCallback(@NonNull ProxyFileDescriptorCallback callback) throws UnmountedException, IOException { - int id; + if (mThread.getState() == Thread.State.TERMINATED) { + throw new UnmountedException(); + } synchronized (mLock) { - if (!mActive) { - throw new UnmountedException(); - } if (mCallbackMap.size() >= Integer.MAX_VALUE - MIN_INODE) { throw new IOException("Too many opened files."); } + int id; while (true) { id = mNextInode; mNextInode++; @@ -92,24 +101,17 @@ public class FuseAppLoop { break; } } - - // Register callback after we succeed to create pfd. mCallbackMap.put(id, new CallbackEntry(callback)); - } - try { - return ParcelFileDescriptor.open(new File(mParent, String.valueOf(id)), mode); - } catch (FileNotFoundException error) { - synchronized (mLock) { - mCallbackMap.remove(id); - } - throw error; + return id; } } - public @Nullable File getMountPoint() { - synchronized (mLock) { - return mActive ? mParent : null; - } + public void unregisterCallback(int id) { + mCallbackMap.remove(id); + } + + public int getMountPointId() { + return mMountPointId; } private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException { @@ -128,7 +130,7 @@ public class FuseAppLoop { try { return getCallbackEntryOrThrowLocked(inode).callback.onGetSize(); } catch (ErrnoException exp) { - return -exp.errno; + return getError(exp); } } } @@ -147,7 +149,7 @@ public class FuseAppLoop { // file twice. return (int) inode; } catch (ErrnoException exp) { - return -exp.errno; + return getError(exp); } } } @@ -160,7 +162,7 @@ public class FuseAppLoop { getCallbackEntryOrThrowLocked(inode).callback.onFsync(); return 0; } catch (ErrnoException exp) { - return -exp.errno; + return getError(exp); } } } @@ -169,12 +171,14 @@ public class FuseAppLoop { @SuppressWarnings("unused") private int onRelease(long inode) { synchronized(mLock) { - mCallbackMap.remove(checkInode(inode)); - if (mCallbackMap.size() == 0) { - mActive = false; - return -1; + try { + getCallbackEntryOrThrowLocked(inode).callback.onRelease(); + return 0; + } catch (ErrnoException exp) { + return getError(exp); + } finally { + mCallbackMap.remove(checkInode(inode)); } - return 0; } } @@ -185,7 +189,7 @@ public class FuseAppLoop { try { return getCallbackEntryOrThrowLocked(inode).callback.onRead(offset, size, bytes); } catch (ErrnoException exp) { - return -exp.errno; + return getError(exp); } } } @@ -197,11 +201,17 @@ public class FuseAppLoop { try { return getCallbackEntryOrThrowLocked(inode).callback.onWrite(offset, size, bytes); } catch (ErrnoException exp) { - return -exp.errno; + return getError(exp); } } } + private static int getError(@NonNull ErrnoException exp) { + // Should not return ENOSYS because the kernel stops + // dispatching the FUSE action once FUSE implementation returns ENOSYS for the action. + return exp.errno != OsConstants.ENOSYS ? -exp.errno : -OsConstants.EIO; + } + native boolean native_start_loop(int fd); private static int checkInode(long inode) { @@ -212,9 +222,9 @@ public class FuseAppLoop { public static class UnmountedException extends Exception {} private static class CallbackEntry { - final IProxyFileDescriptorCallback callback; + final ProxyFileDescriptorCallback callback; boolean opened; - CallbackEntry(IProxyFileDescriptorCallback callback) { + CallbackEntry(ProxyFileDescriptorCallback callback) { Preconditions.checkNotNull(callback); this.callback = callback; } diff --git a/core/jni/com_android_internal_os_FuseAppLoop.cpp b/core/jni/com_android_internal_os_FuseAppLoop.cpp index 92a6934b09a47..dd003eb364c5e 100644 --- a/core/jni/com_android_internal_os_FuseAppLoop.cpp +++ b/core/jni/com_android_internal_os_FuseAppLoop.cpp @@ -51,7 +51,6 @@ private: JNIEnv* const mEnv; jobject const mSelf; ScopedLocalRef mJniBuffer; - bool mActive; template T checkException(T result) const { @@ -67,8 +66,7 @@ public: Callback(JNIEnv* env, jobject self) : mEnv(env), mSelf(self), - mJniBuffer(env, nullptr), - mActive(true) {} + mJniBuffer(env, nullptr) {} bool Init() { mJniBuffer.reset(mEnv->NewByteArray(kBufferSize)); @@ -76,7 +74,7 @@ public: } bool IsActive() override { - return mActive; + return true; } int64_t OnGetSize(uint64_t inode) override { @@ -92,10 +90,7 @@ public: } int32_t OnRelease(uint64_t inode) override { - if (checkException(mEnv->CallIntMethod(mSelf, gOnReleaseMethod, inode)) == -1) { - mActive = false; - } - return fuse::kFuseSuccess; + return checkException(mEnv->CallIntMethod(mSelf, gOnReleaseMethod, inode)); } int32_t OnRead(uint64_t inode, uint64_t offset, uint32_t size, void* buffer) override { diff --git a/core/tests/coretests/src/android/os/storage/StorageManagerIntegrationTest.java b/core/tests/coretests/src/android/os/storage/StorageManagerIntegrationTest.java index 37f00075e183e..ff98eb763f76d 100644 --- a/core/tests/coretests/src/android/os/storage/StorageManagerIntegrationTest.java +++ b/core/tests/coretests/src/android/os/storage/StorageManagerIntegrationTest.java @@ -18,15 +18,20 @@ package android.os.storage; import android.content.Context; import android.os.Environment; +import android.os.ProxyFileDescriptorCallback; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; import android.test.InstrumentationTestCase; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.Suppress; import android.util.Log; import com.android.frameworks.coretests.R; - +import com.android.internal.os.FuseAppLoop; import java.io.DataInputStream; import java.io.IOException; +import java.util.concurrent.ThreadFactory; import java.io.File; import java.io.FileInputStream; @@ -243,4 +248,47 @@ public class StorageManagerIntegrationTest extends StorageManagerBaseTest { verifyObb1Contents(filePath); unmountObb(filePath, DONT_FORCE); } + + @LargeTest + public void testOpenProxyFileDescriptor() throws Exception { + final ProxyFileDescriptorCallback callback = new ProxyFileDescriptorCallback() { + @Override + public long onGetSize() throws ErrnoException { + return 0; + } + + @Override + public void onRelease() {} + }; + + final MyThreadFactory factory = new MyThreadFactory(); + int firstMountId; + try (final ParcelFileDescriptor fd = mSm.openProxyFileDescriptor( + ParcelFileDescriptor.MODE_READ_ONLY, callback, factory)) { + assertNotSame(Thread.State.TERMINATED, factory.thread.getState()); + firstMountId = mSm.getProxyFileDescriptorMountPointId(); + assertNotSame(-1, firstMountId); + } + + // After closing descriptor, the loop should terminate. + factory.thread.join(3000); + assertEquals(Thread.State.TERMINATED, factory.thread.getState()); + + // StorageManager should mount another bridge on the next open request. + try (final ParcelFileDescriptor fd = mSm.openProxyFileDescriptor( + ParcelFileDescriptor.MODE_WRITE_ONLY, callback, factory)) { + assertNotSame(Thread.State.TERMINATED, factory.thread.getState()); + assertNotSame(firstMountId, mSm.getProxyFileDescriptorMountPointId()); + } + } + + private static class MyThreadFactory implements ThreadFactory { + Thread thread = null; + + @Override + public Thread newThread(Runnable r) { + thread = new Thread(r); + return thread; + } + } } \ No newline at end of file diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 55d31c39fd736..f9b9d6f546693 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -88,11 +88,13 @@ import android.util.AtomicFile; import android.util.Log; import android.util.Pair; import android.util.Slog; +import android.util.SparseArray; import android.util.TimeUtils; import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IMediaContainerService; +import com.android.internal.os.AppFuseMount; import com.android.internal.os.SomeArgs; import com.android.internal.os.Zygote; import com.android.internal.util.ArrayUtils; @@ -104,7 +106,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.server.NativeDaemonConnector.Command; import com.android.server.NativeDaemonConnector.SensitiveArg; import com.android.server.pm.PackageManagerService; - +import com.android.server.storage.AppFuseBridge; import libcore.io.IoUtils; import libcore.util.EmptyArray; @@ -135,6 +137,7 @@ import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -337,6 +340,15 @@ class StorageManagerService extends IStorageManager.Stub private volatile int mCurrentUserId = UserHandle.USER_SYSTEM; + /** Holding lock for AppFuse business */ + private final Object mAppFuseLock = new Object(); + + @GuardedBy("mAppFuseLock") + private int mNextAppFuseName = 0; + + @GuardedBy("mAppFuseLock") + private final SparseArray mAppFusePids = new SparseArray<>(); + private VolumeInfo findVolumeByIdOrThrow(String id) { synchronized (mLock) { final VolumeInfo vol = mVolumes.get(id); @@ -3010,6 +3022,128 @@ class StorageManagerService extends IStorageManager.Stub } } + + class CloseableHolder implements AutoCloseable { + @Nullable T mCloseable; + + CloseableHolder(T closeable) { + mCloseable = closeable; + } + + @Nullable T get() { + return mCloseable; + } + + @Nullable T release() { + final T result = mCloseable; + mCloseable = null; + return result; + } + + @Override + public void close() { + if (mCloseable != null) { + IoUtils.closeQuietly(mCloseable); + } + } + } + + class AppFuseMountScope implements AppFuseBridge.IMountScope { + final int mUid; + final int mName; + final ParcelFileDescriptor mDeviceFd; + + AppFuseMountScope(int uid, int pid, int name) throws NativeDaemonConnectorException { + final NativeDaemonEvent event = mConnector.execute( + "appfuse", "mount", uid, Process.myPid(), name); + mUid = uid; + mName = name; + synchronized (mLock) { + mAppFusePids.put(name, pid); + } + if (event.getFileDescriptors() != null && + event.getFileDescriptors().length > 0) { + mDeviceFd = new ParcelFileDescriptor(event.getFileDescriptors()[0]); + } else { + mDeviceFd = null; + } + } + + @Override + public void close() throws NativeDaemonConnectorException { + try { + IoUtils.closeQuietly(mDeviceFd); + mConnector.execute( + "appfuse", "unmount", mUid, Process.myPid(), mName); + } finally { + synchronized (mLock) { + mAppFusePids.delete(mName); + } + } + } + + @Override + public ParcelFileDescriptor getDeviceFileDescriptor() { + return mDeviceFd; + } + } + + @Override + public AppFuseMount mountProxyFileDescriptorBridge() throws RemoteException { + final int uid = Binder.getCallingUid(); + final int pid = Binder.getCallingPid(); + final int name; + synchronized (mAppFuseLock) { + name = mNextAppFuseName++; + } + try (CloseableHolder mountScope = + new CloseableHolder<>(new AppFuseMountScope(uid, pid, name))) { + if (mountScope.get().getDeviceFileDescriptor() == null) { + throw new RemoteException("Failed to obtain device FD"); + } + + // Create communication channel. + final ArrayBlockingQueue channel = new ArrayBlockingQueue<>(1); + final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createSocketPair(); + try (CloseableHolder remote = new CloseableHolder<>(fds[0])) { + new Thread( + new AppFuseBridge(mountScope.release(), fds[1], channel), + AppFuseBridge.TAG).start(); + if (!channel.take()) { + throw new RemoteException("Failed to init AppFuse mount point"); + } + + return new AppFuseMount(name, remote.release()); + } + } catch (NativeDaemonConnectorException e){ + throw e.rethrowAsParcelableException(); + } catch (IOException | InterruptedException error) { + throw new RemoteException(error.getMessage()); + } + } + + @Override + public ParcelFileDescriptor openProxyFileDescriptor(int mountId, int fileId, int mode) { + final int uid = Binder.getCallingUid(); + final int pid = Binder.getCallingPid(); + try { + synchronized (mAppFuseLock) { + final int expectedPid = mAppFusePids.get(mountId, -1); + if (expectedPid == -1) { + Slog.i(TAG, "The mount point has already been unmounted"); + return null; + } + if (expectedPid != pid) { + throw new SecurityException("Mount point was not created by this process."); + } + } + return AppFuseBridge.openFile(uid, mountId, fileId, mode); + } catch (FileNotFoundException error) { + Slog.e(TAG, "Failed to openProxyFileDescriptor", error); + return null; + } + } + @Override public int mkdirs(String callingPkg, String appPath) { final int userId = UserHandle.getUserId(Binder.getCallingUid()); diff --git a/services/core/java/com/android/server/storage/AppFuseBridge.java b/services/core/java/com/android/server/storage/AppFuseBridge.java index 23be9a3a27f7d..5a1f473b5ea28 100644 --- a/services/core/java/com/android/server/storage/AppFuseBridge.java +++ b/services/core/java/com/android/server/storage/AppFuseBridge.java @@ -16,79 +16,95 @@ package com.android.server.storage; -import android.annotation.CallSuper; -import android.annotation.WorkerThread; -import android.os.Handler; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import android.system.Os; -import android.system.OsConstants; -import android.util.Log; -import com.android.internal.os.AppFuseMount; +import com.android.internal.util.Preconditions; import libcore.io.IoUtils; - import java.io.File; -import java.io.FileDescriptor; import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.BlockingQueue; -public class AppFuseBridge implements Runnable { - private static final String TAG = AppFuseBridge.class.getSimpleName(); - - private final FileDescriptor mDeviceFd; - private final FileDescriptor mProxyFd; - private final CountDownLatch mMountLatch = new CountDownLatch(1); +/** + * Runnable that delegates FUSE command from the kernel to application. + * run() blocks until all opened files on the FUSE mount point are closed. So this should be run in + * a separated thread. + */ +public class AppFuseBridge implements Runnable, AutoCloseable { + public static final String TAG = "AppFuseBridge"; /** - * @param deviceFd FD of /dev/fuse. Ownership of fd is taken by AppFuseBridge. - * @param proxyFd FD of socket pair. Ownership of fd is taken by AppFuseBridge. + * The path AppFuse is mounted to. + * The first number is UID who is mounting the FUSE. + * THe second number is mount ID. + * The path must be sync with vold. */ - private AppFuseBridge(FileDescriptor deviceFd, FileDescriptor proxyFd) { - mDeviceFd = deviceFd; + private static final String APPFUSE_MOUNT_NAME_TEMPLATE = "/mnt/appfuse/%d_%d"; + + private final IMountScope mMountScope; + private final ParcelFileDescriptor mProxyFd; + private final BlockingQueue mChannel; + + /** + * @param mountScope Listener to unmount mount point. + * @param proxyFd FD of socket pair. Ownership of FD is taken by AppFuseBridge. + * @param channel Channel that the runnable send mount result to. + */ + public AppFuseBridge( + IMountScope mountScope, ParcelFileDescriptor proxyFd, BlockingQueue channel) { + Preconditions.checkNotNull(mountScope); + Preconditions.checkNotNull(proxyFd); + Preconditions.checkNotNull(channel); + mMountScope = mountScope; mProxyFd = proxyFd; - } - - public static AppFuseMount startMessageLoop( - int uid, - String name, - FileDescriptor deviceFd, - Handler handler, - ParcelFileDescriptor.OnCloseListener listener) - throws IOException, ErrnoException, InterruptedException { - final FileDescriptor localFd = new FileDescriptor(); - final FileDescriptor remoteFd = new FileDescriptor(); - // Needs to specify OsConstants.SOCK_SEQPACKET to keep message boundaries. - Os.socketpair(OsConstants.AF_UNIX, OsConstants.SOCK_SEQPACKET, 0, remoteFd, localFd); - - // Caller must invoke #start() after instantiate AppFuseBridge. - // Otherwise FDs will be leaked. - final AppFuseBridge bridge = new AppFuseBridge(deviceFd, localFd); - final Thread thread = new Thread(bridge, TAG); - thread.start(); - try { - bridge.mMountLatch.await(); - } catch (InterruptedException error) { - throw error; - } - return new AppFuseMount( - new File("/mnt/appfuse/" + uid + "_" + name), - ParcelFileDescriptor.fromFd(remoteFd, handler, listener)); + mChannel = channel; } @Override public void run() { - // deviceFd and proxyFd must be closed in native_start_loop. - final int deviceFd = mDeviceFd.getInt$(); - final int proxyFd = mProxyFd.getInt$(); - mDeviceFd.setInt$(-1); - mProxyFd.setInt$(-1); - native_start_loop(deviceFd, proxyFd); + try { + // deviceFd and proxyFd must be closed in native_start_loop. + native_start_loop( + mMountScope.getDeviceFileDescriptor().detachFd(), + mProxyFd.detachFd()); + } finally { + close(); + } + } + + public static ParcelFileDescriptor openFile(int uid, int mountId, int fileId, int mode) + throws FileNotFoundException { + final File mountPoint = getMountPoint(uid, mountId); + try { + if (Os.stat(mountPoint.getPath()).st_ino != 1) { + throw new FileNotFoundException("Could not find bridge mount point."); + } + } catch (ErrnoException e) { + throw new FileNotFoundException( + "Failed to stat mount point: " + mountPoint.getParent()); + } + return ParcelFileDescriptor.open(new File(mountPoint, String.valueOf(fileId)), mode); + } + + private static File getMountPoint(int uid, int mountId) { + return new File(String.format(APPFUSE_MOUNT_NAME_TEMPLATE, uid, mountId)); + } + + @Override + public void close() { + IoUtils.closeQuietly(mMountScope); + IoUtils.closeQuietly(mProxyFd); + // Invoke countDown here in case where close is invoked before mount. + mChannel.offer(false); } // Used by com_android_server_storage_AppFuse.cpp. private void onMount() { - mMountLatch.countDown(); + mChannel.offer(true); + } + + public static interface IMountScope extends AutoCloseable { + ParcelFileDescriptor getDeviceFileDescriptor(); } private native boolean native_start_loop(int deviceFd, int proxyFd); From 500bffdba9e3b08abd26627af49ecc89a58c07ff Mon Sep 17 00:00:00 2001 From: Daichi Hirono Date: Tue, 17 Jan 2017 16:32:22 +0900 Subject: [PATCH 2/2] Publish StorageManager#openProxyFileDescriptor API. Bug: 29970149 Test: Build succeeded Change-Id: I1606b184968839fb99744387dcc058f15f6521c8 --- api/current.txt | 10 ++++++++++ api/system-current.txt | 10 ++++++++++ api/test-current.txt | 10 ++++++++++ .../android/os/ProxyFileDescriptorCallback.java | 1 - core/java/android/os/storage/StorageManager.java | 15 ++++++++++++++- 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/api/current.txt b/api/current.txt index b9cd7d0d35032..96058f1409872 100644 --- a/api/current.txt +++ b/api/current.txt @@ -29846,6 +29846,15 @@ package android.os { field public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8; // 0xfffffff8 } + public abstract class ProxyFileDescriptorCallback { + ctor public ProxyFileDescriptorCallback(); + method public void onFsync() throws android.system.ErrnoException; + method public long onGetSize() throws android.system.ErrnoException; + method public int onRead(long, int, byte[]) throws android.system.ErrnoException; + method public abstract void onRelease(); + method public int onWrite(long, int, byte[]) throws android.system.ErrnoException; + } + public class RecoverySystem { method public static void installPackage(android.content.Context, java.io.File) throws java.io.IOException; method public static void rebootWipeCache(android.content.Context) throws java.io.IOException; @@ -30263,6 +30272,7 @@ package android.os.storage { method public boolean isEncrypted(java.io.File); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); + method public android.os.ParcelFileDescriptor openProxyFileDescriptor(int, android.os.ProxyFileDescriptorCallback) throws java.io.IOException; method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); field public static final java.lang.String ACTION_MANAGE_STORAGE = "android.os.storage.action.MANAGE_STORAGE"; } diff --git a/api/system-current.txt b/api/system-current.txt index 75b0a34a101c2..20168b6f5f1de 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -32448,6 +32448,15 @@ package android.os { field public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8; // 0xfffffff8 } + public abstract class ProxyFileDescriptorCallback { + ctor public ProxyFileDescriptorCallback(); + method public void onFsync() throws android.system.ErrnoException; + method public long onGetSize() throws android.system.ErrnoException; + method public int onRead(long, int, byte[]) throws android.system.ErrnoException; + method public abstract void onRelease(); + method public int onWrite(long, int, byte[]) throws android.system.ErrnoException; + } + public class RecoverySystem { method public static void cancelScheduledUpdate(android.content.Context) throws java.io.IOException; method public static void installPackage(android.content.Context, java.io.File) throws java.io.IOException; @@ -32951,6 +32960,7 @@ package android.os.storage { method public boolean isEncrypted(java.io.File); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); + method public android.os.ParcelFileDescriptor openProxyFileDescriptor(int, android.os.ProxyFileDescriptorCallback) throws java.io.IOException; method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); field public static final java.lang.String ACTION_MANAGE_STORAGE = "android.os.storage.action.MANAGE_STORAGE"; } diff --git a/api/test-current.txt b/api/test-current.txt index de8602216f0ea..b361244ec7642 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -29956,6 +29956,15 @@ package android.os { field public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8; // 0xfffffff8 } + public abstract class ProxyFileDescriptorCallback { + ctor public ProxyFileDescriptorCallback(); + method public void onFsync() throws android.system.ErrnoException; + method public long onGetSize() throws android.system.ErrnoException; + method public int onRead(long, int, byte[]) throws android.system.ErrnoException; + method public abstract void onRelease(); + method public int onWrite(long, int, byte[]) throws android.system.ErrnoException; + } + public class RecoverySystem { method public static void installPackage(android.content.Context, java.io.File) throws java.io.IOException; method public static void rebootWipeCache(android.content.Context) throws java.io.IOException; @@ -30374,6 +30383,7 @@ package android.os.storage { method public boolean isEncrypted(java.io.File); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); + method public android.os.ParcelFileDescriptor openProxyFileDescriptor(int, android.os.ProxyFileDescriptorCallback) throws java.io.IOException; method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); field public static final java.lang.String ACTION_MANAGE_STORAGE = "android.os.storage.action.MANAGE_STORAGE"; } diff --git a/core/java/android/os/ProxyFileDescriptorCallback.java b/core/java/android/os/ProxyFileDescriptorCallback.java index 23848b9ba27d8..2e9f8d9a64397 100644 --- a/core/java/android/os/ProxyFileDescriptorCallback.java +++ b/core/java/android/os/ProxyFileDescriptorCallback.java @@ -21,7 +21,6 @@ import android.system.OsConstants; /** * Callback that handles file system requests from ProxyFileDescriptor. - * @hide */ public abstract class ProxyFileDescriptorCallback { /** diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index d5acbeb61b9d7..85df48f91a5a8 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -1327,6 +1327,7 @@ public class StorageManager { } } + /** {@hide} */ @VisibleForTesting public @NonNull ParcelFileDescriptor openProxyFileDescriptor( @@ -1378,7 +1379,19 @@ public class StorageManager { throw new IOException("Failed to mount bridge."); } - /** {@hide} */ + /** + * Opens seekable ParcelFileDescriptor that routes file operation requests to + * ProxyFileDescriptorCallback. + * + * @param mode The desired access mode, must be one of + * {@link ParcelFileDescriptor#MODE_READ_ONLY}, + * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or + * {@link ParcelFileDescriptor#MODE_READ_WRITE} + * @param callback Callback to process file operation requests issued on returned file + * descriptor. The callback is invoked on a thread managed by the framework. + * @return Seekable ParcelFileDescriptor. + * @throws IOException + */ public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback) throws IOException {