1 /*
2  * Copyright (C) 2021 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.car.internal;
18 
19 import static android.system.OsConstants.PROT_READ;
20 
21 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.os.SharedMemory;
28 import android.system.ErrnoException;
29 import android.util.Log;
30 import android.util.Slog;
31 
32 import com.android.internal.annotations.GuardedBy;
33 
34 import java.io.Closeable;
35 import java.nio.ByteBuffer;
36 
37 /**
38  * Base class to allow passing {@code Parcelable} over binder directly or through shared memory if
39  * payload size is too big.
40  *
41  * <p>Child class should inherit this to use this or use {@link LargeParcelable} class.
42  *
43  * <p>Parcelized data will have following elements
44  * <ul>
45  * <li>@Nullable Parcelable
46  * <li>@Nullable SharedMemory which include serialized Parcelable if non-null. This will be set
47  * only when the previous Parcelable is null or this also can be null for no data case.
48  * </ul>
49  *
50  * @hide
51  */
52 public abstract class LargeParcelableBase implements Parcelable, Closeable {
53     /** Payload size bigger than this value will be passed over shared memory. */
54     public static final int MAX_DIRECT_PAYLOAD_SIZE = 4096;
55     private static final String TAG = LargeParcelable.class.getSimpleName();
56 
57     private static final boolean DBG_PAYLOAD = Log.isLoggable(TAG, Log.DEBUG);
58     private static final int DBG_DUMP_LENGTH = 16;
59 
60     private static final int NULL_PAYLOAD = 0;
61     private static final int NONNULL_PAYLOAD = 1;
62     private static final int FD_HEADER = 0;
63 
64     private final Object mLock = new Object();
65     @GuardedBy("mLock")
66     private @Nullable SharedMemory mSharedMemory;
67 
68     /**
69      * Serialize (=write Parcelable into given Parcel) a {@code Parcelable} child class wants to
70      * pass over binder call.
71      */
serialize(@onNull Parcel dest, int flags)72     protected abstract void serialize(@NonNull Parcel dest, int flags);
73 
74     /**
75      * Serialize null payload to the given {@code Parcel}. For {@code Parcelable}, this can be
76      * simply {@code dest.writeParcelable(null)} but non-Parcelable should have other way to
77      * mark that there is no payload.
78      */
serializeNullPayload(@onNull Parcel dest)79     protected abstract void serializeNullPayload(@NonNull Parcel dest);
80 
81     /**
82      * Read a {@code Parcelable} from the given {@code Parcel}.
83      */
deserialize(@onNull Parcel src)84     protected abstract void deserialize(@NonNull Parcel src);
85 
LargeParcelableBase()86     public LargeParcelableBase() {
87     }
88 
LargeParcelableBase(Parcel in)89     public LargeParcelableBase(Parcel in) {
90         // Make this compatible with stable AIDL
91         // payload size + Parcelable / payload + 1:has shared memory + 0 + file
92         //                                       0:no shared memory
93         // 0 + file makes it compatible with ParcelFileDescrpitor
94         // file contains:
95         // file size + Parcelable / payload + 0
96         int startPosition = in.dataPosition();
97         int totalPayloadSize = in.readInt();
98         deserialize(in);
99         int sharedMemoryPosition = in.dataPosition();
100         boolean hasSharedMemory = (in.readInt() != NULL_PAYLOAD);
101         if (hasSharedMemory) {
102             int fdHeader = in.readInt();
103             if (fdHeader != FD_HEADER) {
104                 throw new IllegalArgumentException(
105                         "Invalid data, wrong fdHeader, expected 0 while got " + fdHeader);
106             }
107             SharedMemory memory = SharedMemory.CREATOR.createFromParcel(in);
108             deserializeSharedMemoryAndClose(memory);
109         }
110         in.setDataPosition(startPosition + totalPayloadSize);
111         if (DBG_PAYLOAD) {
112             Slog.d(TAG, "Read, start:" + startPosition + " totalPayloadSize:" + totalPayloadSize
113                     + " sharedMemoryPosition:" + sharedMemoryPosition
114                     + " hasSharedMemory:" + hasSharedMemory + " dataAvail:" + in.dataAvail());
115         }
116     }
117 
118     @Override
writeToParcel(@onNull Parcel dest, int flags)119     public void writeToParcel(@NonNull Parcel dest, int flags) {
120         int startPosition = dest.dataPosition();
121         SharedMemory sharedMemory;
122         synchronized (mLock) {
123             sharedMemory = mSharedMemory;
124         }
125         int totalPayloadSize = 0;
126         if (sharedMemory != null) {
127             // optimized path for resending the same Parcelable multiple times with already
128             // created shared memory
129             totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dest, flags, sharedMemory);
130             if (DBG_PAYLOAD) {
131                 Slog.d(TAG, "Write, reusing shared memory, start:" + startPosition
132                         + " totalPayloadSize:" + totalPayloadSize);
133             }
134             return;
135         }
136 
137         // dataParcel is the parcel that would be serialized to the shared memory file.
138         Parcel dataParcel = Parcel.obtain();
139         totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dataParcel, flags, null);
140 
141         boolean noSharedMemory = totalPayloadSize <= MAX_DIRECT_PAYLOAD_SIZE;
142         boolean hasNonNullPayload = true;
143         if (noSharedMemory) {
144             if (DBG_PAYLOAD) {
145                 Slog.d(TAG, "not using shared memory");
146             }
147             dest.appendFrom(dataParcel, 0, totalPayloadSize);
148             dataParcel.recycle();
149         } else {
150             if (DBG_PAYLOAD) {
151                 Slog.d(TAG, "using shared memory");
152             }
153             sharedMemory = serializeParcelToSharedMemory(dataParcel);
154             dataParcel.recycle();
155             synchronized (mLock) {
156                 // If it is already set, let sharedMemory go and GV will close it later.
157                 // This is ok as this kind of race should not happen often.
158                 if (mSharedMemory != null) {
159                     mSharedMemory = sharedMemory;
160                 }
161             }
162 
163             totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dest, flags, sharedMemory);
164         }
165         if (DBG_PAYLOAD) {
166             Slog.d(TAG, "Write, start:" + startPosition + " totalPayloadSize:" + totalPayloadSize
167                     + " hasNonNullPayload:" + hasNonNullPayload
168                     + " hasSharedMemory:" + !noSharedMemory + " dataSize:" + dest.dataSize());
169         }
170     }
171 
updatePayloadSize(Parcel dest, int startPosition)172     private int updatePayloadSize(Parcel dest, int startPosition) {
173         int lastPosition = dest.dataPosition();
174         int totalPayloadSize = lastPosition - startPosition;
175         dest.setDataPosition(startPosition);
176         dest.writeInt(totalPayloadSize);
177         dest.setDataPosition(lastPosition);
178         dest.setDataSize(lastPosition);
179         return totalPayloadSize;
180     }
181 
182     // Write shared memory in compatible way with ParcelFileDescriptor
writeSharedMemoryCompatibleToParcel(Parcel dest, SharedMemory memory, int flags)183     private void writeSharedMemoryCompatibleToParcel(Parcel dest, SharedMemory memory, int flags) {
184         // dest.writeParcelable() adds class type which makes it incompatible with C++.
185         if (memory == null) {
186             dest.writeInt(NULL_PAYLOAD);
187             return;
188         }
189         // non-null case
190         dest.writeInt(NONNULL_PAYLOAD);
191         dest.writeInt(FD_HEADER); // additional header for ParcelFileDescriptor
192         memory.writeToParcel(dest, flags);
193     }
194 
195     @Override
196     @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
describeContents()197     public int describeContents() {
198         return 0;
199     }
200 
201     /**
202      * {@inheritDoc}
203      *
204      * <p>Close the underlying shared memory for this. This can be called multiple times safely.
205      * When this is not called explicitly, it will be closed when this instance is GCed.
206      * Calling this can be useful when many instances are created frequently.
207      *
208      * <p>If underlying payload is changed, the client should call this before sending it over
209      * binder as sending it over binder can keep shared memory generated from the previous binder
210      * call.
211      */
212     @Override
close()213     public void close() {
214         SharedMemory sharedMemory = null;
215         synchronized (mLock) {
216             sharedMemory = mSharedMemory;
217             mSharedMemory = null;
218         }
219         if (sharedMemory != null) {
220             sharedMemory.close();
221         }
222     }
223 
serializeParcelToSharedMemory(Parcel p)224     protected static SharedMemory serializeParcelToSharedMemory(Parcel p) {
225         SharedMemory memory = null;
226         ByteBuffer buffer = null;
227         int size = p.dataSize();
228         try {
229             memory = SharedMemory.create(LargeParcelableBase.class.getSimpleName(), size);
230             buffer = memory.mapReadWrite();
231             byte[] data = p.marshall();
232             buffer.put(data, 0, size);
233             if (DBG_PAYLOAD) {
234                 int dumpSize = Math.min(DBG_DUMP_LENGTH, data.length);
235                 StringBuilder bd = new StringBuilder();
236                 bd.append("marshalled:");
237                 for (int i = 0; i < dumpSize; i++) {
238                     bd.append(data[i]);
239                     if (i != dumpSize - 1) {
240                         bd.append(',');
241                     }
242                 }
243                 bd.append("=memory:");
244                 for (int i = 0; i < dumpSize; i++) {
245                     bd.append(buffer.get(i));
246                     if (i != dumpSize - 1) {
247                         bd.append(',');
248                     }
249                 }
250                 Slog.d(TAG, bd.toString());
251             }
252             if (!memory.setProtect(PROT_READ)) {
253                 memory.close();
254                 throw new SecurityException("Failed to set read-only protection on shared memory");
255             }
256         } catch (ErrnoException e) {
257             throw new IllegalArgumentException("Failed to use shared memory", e);
258         } catch (Exception e) {
259             throw new IllegalArgumentException("failed to serialize", e);
260         } finally {
261             if (buffer != null) {
262                 SharedMemory.unmap(buffer);
263             }
264         }
265 
266         return memory;
267     }
268 
copyFromSharedMemory(SharedMemory memory)269     protected static Parcel copyFromSharedMemory(SharedMemory memory) {
270         ByteBuffer buffer = null;
271         Parcel in = Parcel.obtain();
272         try {
273             buffer = memory.mapReadOnly();
274             // TODO(b/188781089) find way to avoid this additional copy
275             byte[] payload = new byte[buffer.limit()];
276             buffer.get(payload);
277             in.unmarshall(payload, 0, payload.length);
278             in.setDataPosition(0);
279             if (DBG_PAYLOAD) {
280                 int dumpSize = Math.min(DBG_DUMP_LENGTH, payload.length);
281                 StringBuilder bd = new StringBuilder();
282                 bd.append("unmarshalled:");
283                 int parcelStartPosition = in.dataPosition();
284                 byte[] fromParcel = in.marshall();
285                 for (int i = 0; i < dumpSize; i++) {
286                     bd.append(fromParcel[i]);
287                     if (i != dumpSize - 1) bd.append(',');
288                 }
289                 bd.append("=startPosition:");
290                 bd.append(parcelStartPosition);
291                 bd.append("=memory:");
292                 for (int i = 0; i < dumpSize; i++) {
293                     bd.append(buffer.get(i));
294                     if (i != dumpSize - 1) bd.append(',');
295                 }
296                 bd.append("=interim_payload:");
297                 for (int i = 0; i < dumpSize; i++) {
298                     bd.append(payload[i]);
299                     if (i != dumpSize - 1) bd.append(',');
300                 }
301                 Slog.d(TAG, bd.toString());
302                 in.setDataPosition(parcelStartPosition);
303             }
304         } catch (ErrnoException e) {
305             throw new IllegalArgumentException("cannot create Parcelable from SharedMemory", e);
306         } catch (Exception e) {
307             throw new IllegalArgumentException("failed to deserialize", e);
308         } finally {
309             if (buffer != null) {
310                 SharedMemory.unmap(buffer);
311             }
312         }
313         return in;
314     }
315 
deserializeSharedMemoryAndClose(SharedMemory memory)316     private void deserializeSharedMemoryAndClose(SharedMemory memory) {
317         // The shared memory file contains a serialized largeParcelable.
318         // size + payload + 0 (no shared memory).
319         Parcel in = null;
320         try {
321             in = copyFromSharedMemory(memory);
322             // Even if we don't need the file size, we have to read it from the parcel to advance
323             // the data position.
324             int fileSize = in.readInt();
325             if (DBG_PAYLOAD) {
326                 Slog.d(TAG, "file size in shared memory file: " + fileSize);
327             }
328             deserialize(in);
329             // There is an additional 0 in the parcel, but we ignore that.
330         } finally {
331             memory.close();
332             if (in != null) {
333                 in.recycle();
334             }
335         }
336     }
337 
338     // If sharedMemory is not null, serialize null payload and shared memory to parcel.
339     // Otherwise, serialize the actual payload to parcel.
serializeMemoryFdOrPayloadToParcel( Parcel dest, int flags, @Nullable SharedMemory sharedMemory)340     private int serializeMemoryFdOrPayloadToParcel(
341             Parcel dest, int flags, @Nullable SharedMemory sharedMemory) {
342         int startPosition = dest.dataPosition();
343         dest.writeInt(0); // payload size
344 
345         if (sharedMemory != null) {
346             serializeNullPayload(dest);
347             writeSharedMemoryCompatibleToParcel(dest, sharedMemory, flags);
348         } else {
349             serialize(dest, flags);
350             writeSharedMemoryCompatibleToParcel(dest, null, flags);
351         }
352 
353         return updatePayloadSize(dest, startPosition);
354     }
355 }
356