1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.os;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.os.Build;
23 import android.os.Handler;
24 import android.os.Message;
25 import android.os.ParcelFileDescriptor;
26 import android.os.ProxyFileDescriptorCallback;
27 import android.system.ErrnoException;
28 import android.system.OsConstants;
29 import android.util.Log;
30 import android.util.SparseArray;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.util.Preconditions;
34 
35 import java.util.HashMap;
36 import java.util.LinkedList;
37 import java.util.Map;
38 import java.util.Objects;
39 import java.util.concurrent.ThreadFactory;
40 
41 public class FuseAppLoop implements Handler.Callback {
42     private static final String TAG = "FuseAppLoop";
43     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
44     public static final int ROOT_INODE = 1;
45     private static final int MIN_INODE = 2;
46     private static final ThreadFactory sDefaultThreadFactory = new ThreadFactory() {
47         @Override
48         public Thread newThread(Runnable r) {
49             return new Thread(r, TAG);
50         }
51     };
52     private static final int FUSE_OK = 0;
53     private static final int ARGS_POOL_SIZE = 50;
54 
55     private final Object mLock = new Object();
56     private final int mMountPointId;
57     private final Thread mThread;
58 
59     @GuardedBy("mLock")
60     private final SparseArray<CallbackEntry> mCallbackMap = new SparseArray<>();
61 
62     @GuardedBy("mLock")
63     private final BytesMap mBytesMap = new BytesMap();
64 
65     @GuardedBy("mLock")
66     private final LinkedList<Args> mArgsPool = new LinkedList<>();
67 
68     /**
69      * Sequential number can be used as file name and inode in AppFuse.
70      * 0 is regarded as an error, 1 is mount point. So we start the number from 2.
71      */
72     @GuardedBy("mLock")
73     private int mNextInode = MIN_INODE;
74 
75     @GuardedBy("mLock")
76     private long mInstance;
77 
FuseAppLoop( int mountPointId, @NonNull ParcelFileDescriptor fd, @Nullable ThreadFactory factory)78     public FuseAppLoop(
79             int mountPointId, @NonNull ParcelFileDescriptor fd, @Nullable ThreadFactory factory) {
80         mMountPointId = mountPointId;
81         if (factory == null) {
82             factory = sDefaultThreadFactory;
83         }
84         mInstance = native_new(fd.detachFd());
85         mThread = factory.newThread(() -> {
86             native_start(mInstance);
87             synchronized (mLock) {
88                 native_delete(mInstance);
89                 mInstance = 0;
90                 mBytesMap.clear();
91             }
92         });
93         mThread.start();
94     }
95 
registerCallback(@onNull ProxyFileDescriptorCallback callback, @NonNull Handler handler)96     public int registerCallback(@NonNull ProxyFileDescriptorCallback callback,
97             @NonNull Handler handler) throws FuseUnavailableMountException {
98         synchronized (mLock) {
99             Objects.requireNonNull(callback);
100             Objects.requireNonNull(handler);
101             Preconditions.checkState(
102                     mCallbackMap.size() < Integer.MAX_VALUE - MIN_INODE, "Too many opened files.");
103             Preconditions.checkArgument(
104                     Thread.currentThread().getId() != handler.getLooper().getThread().getId(),
105                     "Handler must be different from the current thread");
106             if (mInstance == 0) {
107                 throw new FuseUnavailableMountException(mMountPointId);
108             }
109             int id;
110             while (true) {
111                 id = mNextInode;
112                 mNextInode++;
113                 if (mNextInode < 0) {
114                     mNextInode = MIN_INODE;
115                 }
116                 if (mCallbackMap.get(id) == null) {
117                     break;
118                 }
119             }
120             mCallbackMap.put(id, new CallbackEntry(
121                     callback, new Handler(handler.getLooper(), this)));
122             return id;
123         }
124     }
125 
126     public void unregisterCallback(int id) {
127         synchronized (mLock) {
128             mCallbackMap.remove(id);
129         }
130     }
131 
132     public int getMountPointId() {
133         return mMountPointId;
134     }
135 
136     // Defined in fuse.h
137     private static final int FUSE_LOOKUP = 1;
138     private static final int FUSE_GETATTR = 3;
139     private static final int FUSE_OPEN = 14;
140     private static final int FUSE_READ = 15;
141     private static final int FUSE_WRITE = 16;
142     private static final int FUSE_RELEASE = 18;
143     private static final int FUSE_FSYNC = 20;
144 
145     // Defined in FuseBuffer.h
146     private static final int FUSE_MAX_WRITE = 128 * 1024;
147 
148     @Override
149     public boolean handleMessage(Message msg) {
150         final Args args = (Args) msg.obj;
151         final CallbackEntry entry = args.entry;
152         final long inode = args.inode;
153         final long unique = args.unique;
154         final int size = args.size;
155         final long offset = args.offset;
156         final byte[] data = args.data;
157 
158         try {
159             switch (msg.what) {
160                 case FUSE_LOOKUP: {
161                     final long fileSize = entry.callback.onGetSize();
162                     synchronized (mLock) {
163                         if (mInstance != 0) {
164                             native_replyLookup(mInstance, unique, inode, fileSize);
165                         }
166                         recycleLocked(args);
167                     }
168                     break;
169                 }
170                 case FUSE_GETATTR: {
171                     final long fileSize = entry.callback.onGetSize();
172                     synchronized (mLock) {
173                         if (mInstance != 0) {
174                             native_replyGetAttr(mInstance, unique, inode, fileSize);
175                         }
176                         recycleLocked(args);
177                     }
178                     break;
179                 }
180                 case FUSE_READ:
181                     final int readSize = entry.callback.onRead(
182                             offset, size, data);
183                     synchronized (mLock) {
184                         if (mInstance != 0) {
185                             native_replyRead(mInstance, unique, readSize, data);
186                         }
187                         recycleLocked(args);
188                     }
189                     break;
190                 case FUSE_WRITE:
191                     final int writeSize = entry.callback.onWrite(offset, size, data);
192                     synchronized (mLock) {
193                         if (mInstance != 0) {
194                             native_replyWrite(mInstance, unique, writeSize);
195                         }
196                         recycleLocked(args);
197                     }
198                     break;
199                 case FUSE_FSYNC:
200                     entry.callback.onFsync();
201                     synchronized (mLock) {
202                         if (mInstance != 0) {
203                             native_replySimple(mInstance, unique, FUSE_OK);
204                         }
205                         recycleLocked(args);
206                     }
207                     break;
208                 case FUSE_RELEASE:
209                     entry.callback.onRelease();
210                     synchronized (mLock) {
211                         if (mInstance != 0) {
212                             native_replySimple(mInstance, unique, FUSE_OK);
213                         }
214                         mBytesMap.stopUsing(inode);
215                         recycleLocked(args);
216                     }
217                     break;
218                 default:
219                     throw new IllegalArgumentException("Unknown FUSE command: " + msg.what);
220             }
221         } catch (Exception error) {
222             synchronized (mLock) {
223                 Log.e(TAG, "", error);
224                 replySimpleLocked(unique, getError(error));
225                 recycleLocked(args);
226             }
227         }
228 
229         return true;
230     }
231 
232     // Called by JNI.
233     @SuppressWarnings("unused")
234     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
235     private void onCommand(int command, long unique, long inode, long offset, int size,
236             byte[] data) {
237         synchronized (mLock) {
238             try {
239                 final Args args;
240                 if (mArgsPool.size() == 0) {
241                     args = new Args();
242                 } else {
243                     args = mArgsPool.pop();
244                 }
245                 args.unique = unique;
246                 args.inode = inode;
247                 args.offset = offset;
248                 args.size = size;
249                 args.data = data;
250                 args.entry = getCallbackEntryOrThrowLocked(inode);
251                 if (!args.entry.handler.sendMessage(
252                         Message.obtain(args.entry.handler, command, 0, 0, args))) {
253                     throw new ErrnoException("onCommand", OsConstants.EBADF);
254                 }
255             } catch (Exception error) {
256                 replySimpleLocked(unique, getError(error));
257             }
258         }
259     }
260 
261     // Called by JNI.
262     @SuppressWarnings("unused")
263     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
264     private byte[] onOpen(long unique, long inode) {
265         synchronized (mLock) {
266             try {
267                 final CallbackEntry entry = getCallbackEntryOrThrowLocked(inode);
268                 if (entry.opened) {
269                     throw new ErrnoException("onOpen", OsConstants.EMFILE);
270                 }
271                 if (mInstance != 0) {
272                     native_replyOpen(mInstance, unique, /* fh */ inode);
273                     entry.opened = true;
274                     return mBytesMap.startUsing(inode);
275                 }
276             } catch (ErrnoException error) {
277                 replySimpleLocked(unique, getError(error));
278             }
279             return null;
280         }
281     }
282 
283     private static int getError(@NonNull Exception error) {
284         if (error instanceof ErrnoException) {
285             final int errno = ((ErrnoException) error).errno;
286             if (errno != OsConstants.ENOSYS) {
287                 return -errno;
288             }
289         }
290         return -OsConstants.EBADF;
291     }
292 
293     @GuardedBy("mLock")
294     private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException {
295         final CallbackEntry entry = mCallbackMap.get(checkInode(inode));
296         if (entry == null) {
297             throw new ErrnoException("getCallbackEntryOrThrowLocked", OsConstants.ENOENT);
298         }
299         return entry;
300     }
301 
302     @GuardedBy("mLock")
303     private void recycleLocked(Args args) {
304         if (mArgsPool.size() < ARGS_POOL_SIZE) {
305             mArgsPool.add(args);
306         }
307     }
308 
309     @GuardedBy("mLock")
310     private void replySimpleLocked(long unique, int result) {
311         if (mInstance != 0) {
312             native_replySimple(mInstance, unique, result);
313         }
314     }
315 
316     native long native_new(int fd);
317     native void native_delete(long ptr);
318     native void native_start(long ptr);
319 
320     native void native_replySimple(long ptr, long unique, int result);
321     native void native_replyOpen(long ptr, long unique, long fh);
322     native void native_replyLookup(long ptr, long unique, long inode, long size);
323     native void native_replyGetAttr(long ptr, long unique, long inode, long size);
324     native void native_replyWrite(long ptr, long unique, int size);
325     native void native_replyRead(long ptr, long unique, int size, byte[] bytes);
326 
327     private static int checkInode(long inode) {
328         Preconditions.checkArgumentInRange(inode, MIN_INODE, Integer.MAX_VALUE, "checkInode");
329         return (int) inode;
330     }
331 
332     public static class UnmountedException extends Exception {}
333 
334     private static class CallbackEntry {
335         final ProxyFileDescriptorCallback callback;
336         final Handler handler;
337         boolean opened;
338 
339         CallbackEntry(ProxyFileDescriptorCallback callback, Handler handler) {
340             this.callback = Objects.requireNonNull(callback);
341             this.handler = Objects.requireNonNull(handler);
342         }
343 
344         long getThreadId() {
345             return handler.getLooper().getThread().getId();
346         }
347     }
348 
349     /**
350      * Entry for bytes map.
351      */
352     private static class BytesMapEntry {
353         int counter = 0;
354         byte[] bytes = new byte[FUSE_MAX_WRITE];
355     }
356 
357     /**
358      * Map between inode and byte buffer.
359      */
360     private static class BytesMap {
361         final Map<Long, BytesMapEntry> mEntries = new HashMap<>();
362 
363         byte[] startUsing(long inode) {
364             BytesMapEntry entry = mEntries.get(inode);
365             if (entry == null) {
366                 entry = new BytesMapEntry();
367                 mEntries.put(inode, entry);
368             }
369             entry.counter++;
370             return entry.bytes;
371         }
372 
373         void stopUsing(long inode) {
374             final BytesMapEntry entry = mEntries.get(inode);
375             Objects.requireNonNull(entry);
376             entry.counter--;
377             if (entry.counter <= 0) {
378                 mEntries.remove(inode);
379             }
380         }
381 
382         void clear() {
383             mEntries.clear();
384         }
385     }
386 
387     private static class Args {
388         long unique;
389         long inode;
390         long offset;
391         int size;
392         byte[] data;
393         CallbackEntry entry;
394     }
395 }
396