diff --git a/core/java/android/os/IProxyFileDescriptorCallback.java b/core/java/android/os/IProxyFileDescriptorCallback.java new file mode 100644 index 0000000000000..e41e19422a27b --- /dev/null +++ b/core/java/android/os/IProxyFileDescriptorCallback.java @@ -0,0 +1,59 @@ +/* + * 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 android.os; + +import android.system.ErrnoException; + +/** + * Callback that handles file system requests from ProxyFileDescriptor. + * @hide + */ +public interface IProxyFileDescriptorCallback { + /** + * Returns size of bytes provided by the file descriptor. + * @return Size of bytes + * @throws ErrnoException + */ + long onGetSize() throws ErrnoException; + + /** + * Provides bytes read from file descriptor. + * It needs to return exact requested size of bytes unless it reaches file end. + * @param offset Where to read bytes from. + * @param size Size for read bytes. + * @param data Byte array to store read bytes. + * @return Size of bytes returned by the function. + * @throws ErrnoException + */ + int onRead(long offset, int size, byte[] data) throws ErrnoException; + + /** + * Handles bytes written to file descriptor. + * @param offset Where to write bytes to. + * @param size Size for write bytes. + * @param data Byte array to be written to somewhere. + * @return Size of bytes processed by the function. + * @throws ErrnoException + */ + int onWrite(long offset, int size, byte[] data) throws ErrnoException; + + /** + * Processes fsync request. + * @throws ErrnoException + */ + void onFsync() throws ErrnoException; +} diff --git a/core/java/com/android/internal/os/FuseAppLoop.java b/core/java/com/android/internal/os/FuseAppLoop.java new file mode 100644 index 0000000000000..34253ce96a06f --- /dev/null +++ b/core/java/com/android/internal/os/FuseAppLoop.java @@ -0,0 +1,222 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IProxyFileDescriptorCallback; +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.util.Preconditions; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +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 final Object mLock = new Object(); + private final File mParent; + + @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. + */ + @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); + final int rawFd = fd.detachFd(); + new Thread(new Runnable() { + @Override + public void run() { + bridge.native_start_loop(rawFd); + } + }, TAG).start(); + return bridge; + } + + public @NonNull ParcelFileDescriptor openFile(int mode, IProxyFileDescriptorCallback callback) + throws UnmountedException, IOException { + int id; + synchronized (mLock) { + if (!mActive) { + throw new UnmountedException(); + } + if (mCallbackMap.size() >= Integer.MAX_VALUE - MIN_INODE) { + throw new IOException("Too many opened files."); + } + while (true) { + id = mNextInode; + mNextInode++; + if (mNextInode < 0) { + mNextInode = MIN_INODE; + } + if (mCallbackMap.get(id) == null) { + 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; + } + } + + public @Nullable File getMountPoint() { + synchronized (mLock) { + return mActive ? mParent : null; + } + } + + private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException { + final CallbackEntry entry = mCallbackMap.get(checkInode(inode)); + if (entry != null) { + return entry; + } else { + throw new ErrnoException("getCallbackEntry", OsConstants.ENOENT); + } + } + + // Called by JNI. + @SuppressWarnings("unused") + private long onGetSize(long inode) { + synchronized(mLock) { + try { + return getCallbackEntryOrThrowLocked(inode).callback.onGetSize(); + } catch (ErrnoException exp) { + return -exp.errno; + } + } + } + + // Called by JNI. + @SuppressWarnings("unused") + private int onOpen(long inode) { + synchronized(mLock) { + try { + final CallbackEntry entry = getCallbackEntryOrThrowLocked(inode); + if (entry.opened) { + throw new ErrnoException("onOpen", OsConstants.EMFILE); + } + entry.opened = true; + // Use inode as file handle. It's OK because AppFuse does not allow to open the same + // file twice. + return (int) inode; + } catch (ErrnoException exp) { + return -exp.errno; + } + } + } + + // Called by JNI. + @SuppressWarnings("unused") + private int onFsync(long inode) { + synchronized(mLock) { + try { + getCallbackEntryOrThrowLocked(inode).callback.onFsync(); + return 0; + } catch (ErrnoException exp) { + return -exp.errno; + } + } + } + + // Called by JNI. + @SuppressWarnings("unused") + private int onRelease(long inode) { + synchronized(mLock) { + mCallbackMap.remove(checkInode(inode)); + if (mCallbackMap.size() == 0) { + mActive = false; + return -1; + } + return 0; + } + } + + // Called by JNI. + @SuppressWarnings("unused") + private int onRead(long inode, long offset, int size, byte[] bytes) { + synchronized(mLock) { + try { + return getCallbackEntryOrThrowLocked(inode).callback.onRead(offset, size, bytes); + } catch (ErrnoException exp) { + return -exp.errno; + } + } + } + + // Called by JNI. + @SuppressWarnings("unused") + private int onWrite(long inode, long offset, int size, byte[] bytes) { + synchronized(mLock) { + try { + return getCallbackEntryOrThrowLocked(inode).callback.onWrite(offset, size, bytes); + } catch (ErrnoException exp) { + return -exp.errno; + } + } + } + + native boolean native_start_loop(int fd); + + private static int checkInode(long inode) { + Preconditions.checkArgumentInRange(inode, MIN_INODE, Integer.MAX_VALUE, "checkInode"); + return (int) inode; + } + + public static class UnmountedException extends Exception {} + + private static class CallbackEntry { + final IProxyFileDescriptorCallback callback; + boolean opened; + CallbackEntry(IProxyFileDescriptorCallback callback) { + Preconditions.checkNotNull(callback); + this.callback = callback; + } + } +} diff --git a/core/jni/Android.mk b/core/jni/Android.mk index a4e9576384064..be2e404820bc8 100644 --- a/core/jni/Android.mk +++ b/core/jni/Android.mk @@ -183,6 +183,7 @@ LOCAL_SRC_FILES:= \ android_content_res_Configuration.cpp \ android_animation_PropertyValuesHolder.cpp \ com_android_internal_net_NetworkStatsFactory.cpp \ + com_android_internal_os_FuseAppLoop.cpp \ com_android_internal_os_PathClassLoaderFactory.cpp \ com_android_internal_os_Zygote.cpp \ com_android_internal_util_VirtualRefBasePtr.cpp \ @@ -201,6 +202,7 @@ LOCAL_C_INCLUDES += \ $(TOP)/frameworks/base/media/jni \ $(TOP)/system/core/base/include \ $(TOP)/system/core/include \ + $(TOP)/system/core/libappfuse/include \ $(TOP)/system/media/camera/include \ $(TOP)/system/netd/include \ external/giflib \ @@ -230,6 +232,7 @@ LOCAL_STATIC_LIBRARIES := \ LOCAL_SHARED_LIBRARIES := \ libmemtrack \ libandroidfw \ + libappfuse \ libbase \ libexpat \ libnativehelper \ diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index cdaa4dce52ebb..1f810ac203ad2 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -201,6 +201,7 @@ extern int register_android_content_res_Configuration(JNIEnv* env); extern int register_android_animation_PropertyValuesHolder(JNIEnv *env); extern int register_com_android_internal_content_NativeLibraryHelper(JNIEnv *env); extern int register_com_android_internal_net_NetworkStatsFactory(JNIEnv *env); +extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env); extern int register_com_android_internal_os_PathClassLoaderFactory(JNIEnv* env); extern int register_com_android_internal_os_Zygote(JNIEnv *env); extern int register_com_android_internal_util_VirtualRefBasePtr(JNIEnv *env); @@ -1419,7 +1420,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_animation_PropertyValuesHolder), REG_JNI(register_com_android_internal_content_NativeLibraryHelper), REG_JNI(register_com_android_internal_net_NetworkStatsFactory), - + REG_JNI(register_com_android_internal_os_FuseAppLoop), }; /* diff --git a/core/jni/com_android_internal_os_FuseAppLoop.cpp b/core/jni/com_android_internal_os_FuseAppLoop.cpp new file mode 100644 index 0000000000000..92a6934b09a47 --- /dev/null +++ b/core/jni/com_android_internal_os_FuseAppLoop.cpp @@ -0,0 +1,164 @@ +/* + * 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. + */ + +#define LOG_TAG "FuseAppLoopJNI" +#define LOG_NDEBUG 0 + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "core_jni_helpers.h" + +namespace android { + +namespace { + +constexpr const char* CLASS_NAME = "com/android/internal/os/FuseAppLoop"; + +jclass gFuseAppLoopClass; +jmethodID gOnGetSizeMethod; +jmethodID gOnOpenMethod; +jmethodID gOnFsyncMethod; +jmethodID gOnReleaseMethod; +jmethodID gOnReadMethod; +jmethodID gOnWriteMethod; + +class Callback : public fuse::FuseAppLoopCallback { +private: + static constexpr size_t kBufferSize = std::max(fuse::kFuseMaxWrite, fuse::kFuseMaxRead); + static_assert(kBufferSize <= INT32_MAX, "kBufferSize should be fit in int32_t."); + + JNIEnv* const mEnv; + jobject const mSelf; + ScopedLocalRef mJniBuffer; + bool mActive; + + template + T checkException(T result) const { + if (mEnv->ExceptionCheck()) { + LOGE_EX(mEnv, nullptr); + mEnv->ExceptionClear(); + return -EIO; + } + return result; + } + +public: + Callback(JNIEnv* env, jobject self) : + mEnv(env), + mSelf(self), + mJniBuffer(env, nullptr), + mActive(true) {} + + bool Init() { + mJniBuffer.reset(mEnv->NewByteArray(kBufferSize)); + return mJniBuffer.get(); + } + + bool IsActive() override { + return mActive; + } + + int64_t OnGetSize(uint64_t inode) override { + return checkException(mEnv->CallLongMethod(mSelf, gOnGetSizeMethod, inode)); + } + + int32_t OnOpen(uint64_t inode) override { + return checkException(mEnv->CallIntMethod(mSelf, gOnOpenMethod, inode)); + } + + int32_t OnFsync(uint64_t inode) override { + return checkException(mEnv->CallIntMethod(mSelf, gOnFsyncMethod, inode)); + } + + int32_t OnRelease(uint64_t inode) override { + if (checkException(mEnv->CallIntMethod(mSelf, gOnReleaseMethod, inode)) == -1) { + mActive = false; + } + return fuse::kFuseSuccess; + } + + int32_t OnRead(uint64_t inode, uint64_t offset, uint32_t size, void* buffer) override { + CHECK_LE(size, static_cast(kBufferSize)); + const int32_t result = checkException(mEnv->CallIntMethod( + mSelf, gOnReadMethod, inode, offset, size, mJniBuffer.get())); + if (result <= 0) { + return result; + } + if (result > static_cast(size)) { + LOG(ERROR) << "Returned size is too large."; + return -EIO; + } + + mEnv->GetByteArrayRegion(mJniBuffer.get(), 0, result, static_cast(buffer)); + CHECK(!mEnv->ExceptionCheck()); + + return checkException(result); + } + + int32_t OnWrite(uint64_t inode, uint64_t offset, uint32_t size, const void* buffer) override { + CHECK_LE(size, static_cast(kBufferSize)); + + mEnv->SetByteArrayRegion(mJniBuffer.get(), 0, size, static_cast(buffer)); + CHECK(!mEnv->ExceptionCheck()); + + return checkException(mEnv->CallIntMethod( + mSelf, gOnWriteMethod, inode, offset, size, mJniBuffer.get())); + } +}; + +jboolean com_android_internal_os_FuseAppLoop_start_loop(JNIEnv* env, jobject self, jint jfd) { + base::unique_fd fd(jfd); + Callback callback(env, self); + + if (!callback.Init()) { + LOG(ERROR) << "Failed to init callback"; + return JNI_FALSE; + } + + return fuse::StartFuseAppLoop(fd.release(), &callback); +} + +const JNINativeMethod methods[] = { + { + "native_start_loop", + "(I)Z", + (void *) com_android_internal_os_FuseAppLoop_start_loop + } +}; + +} // namespace + +int register_com_android_internal_os_FuseAppLoop(JNIEnv* env) { + gFuseAppLoopClass = MakeGlobalRefOrDie(env, FindClassOrDie(env, CLASS_NAME)); + gOnGetSizeMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onGetSize", "(J)J"); + gOnOpenMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onOpen", "(J)I"); + gOnFsyncMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onFsync", "(J)I"); + gOnReleaseMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onRelease", "(J)I"); + gOnReadMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onRead", "(JJI[B)I"); + gOnWriteMethod = GetMethodIDOrDie(env, gFuseAppLoopClass, "onWrite", "(JJI[B)I"); + RegisterMethodsOrDie(env, CLASS_NAME, methods, NELEM(methods)); + return 0; +} + +} // namespace android