diff options
-rw-r--r-- | Android.bp | 18 | ||||
-rw-r--r-- | OWNERS | 1 | ||||
-rw-r--r-- | build/Android.bp | 33 | ||||
-rw-r--r-- | build/include/android-modules-utils/sdk_level.h | 48 | ||||
-rw-r--r-- | build/src/com/android/modules/utils/build/SdkLevel.java | 55 | ||||
-rw-r--r-- | os/Android.bp | 42 | ||||
-rw-r--r-- | os/java/com/android/modules/utils/BaseParceledListSlice.java | 210 | ||||
-rw-r--r-- | os/java/com/android/modules/utils/BasicShellCommandHandler.java | 341 | ||||
-rw-r--r-- | os/java/com/android/modules/utils/ParceledListSlice.aidl | 19 | ||||
-rw-r--r-- | os/java/com/android/modules/utils/ParceledListSlice.java | 87 | ||||
-rw-r--r-- | os/java/com/android/modules/utils/StringParceledListSlice.aidl | 19 | ||||
-rw-r--r-- | os/java/com/android/modules/utils/StringParceledListSlice.java | 81 | ||||
-rw-r--r-- | os/tests/Android.bp | 20 | ||||
-rw-r--r-- | os/tests/AndroidManifest.xml | 23 | ||||
-rw-r--r-- | os/tests/src/com/android/modules/utils/ParceledListSliceTest.java | 322 |
15 files changed, 1319 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..97f99c0 --- /dev/null +++ b/Android.bp @@ -0,0 +1,18 @@ +// Copyright (C) 2020 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. + +// Limit visibility of all modules in this repo by default. +package { + default_visibility: [":__subpackages__"], +} @@ -1,4 +1,5 @@ dariofreni@google.com hackbod@google.com +hansson@google.com narayan@google.com omakoto@google.com diff --git a/build/Android.bp b/build/Android.bp new file mode 100644 index 0000000..13b792a --- /dev/null +++ b/build/Android.bp @@ -0,0 +1,33 @@ +// +// Copyright (C) 2020 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. +// + +java_library { + name: "modules-utils-build", + srcs: ["src/**/*.java"], + sdk_version: "module_current", + min_sdk_version: "29", + visibility: ["//visibility:public"], + apex_available: ["//apex_available:anyapex"], +} + +cc_library_static { + name: "libmodules-utils-build", + min_sdk_version: "29", + export_include_dirs: ["include"], + static_libs: ["libbase"], + visibility: ["//visibility:public"], + apex_available: ["//apex_available:anyapex"], +} diff --git a/build/include/android-modules-utils/sdk_level.h b/build/include/android-modules-utils/sdk_level.h new file mode 100644 index 0000000..a373178 --- /dev/null +++ b/build/include/android-modules-utils/sdk_level.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 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. + */ + +#pragma once + +#include <android-base/properties.h> + +namespace android { +namespace modules { +namespace sdklevel { + +// Return true iff the running Android SDK is at least "R". +static inline bool IsAtLeastR() { + return android::base::GetIntProperty("ro.build.version.sdk", -1) >= 30; +} + +// Returns true iff the running Android SDK is pre-release "S" or "T", built +// based on "R" SDK. +// +// If new SDK versions are added > R, then this method needs to be updated to +// recognise them (e.g. if we add SDK version for R-QPR, the current +// implementation will not recognise pre-release "S" versions built on that). +static inline bool IsAtLeastS() { + // TODO(b/170831689) This should check SDK_INT >= S once S sdk finalised. + // Note that removing the current conditions may lead to issues in + // mainlinefood (and possibly public beta?). + std::string codename = + android::base::GetProperty("ro.build.version.codename", ""); + return android::base::GetIntProperty("ro.build.version.sdk", -1) == 30 && + (codename == "S" || codename == "T"); +} + +} // namespace utils +} // namespace modules +} // namespace android diff --git a/build/src/com/android/modules/utils/build/SdkLevel.java b/build/src/com/android/modules/utils/build/SdkLevel.java new file mode 100644 index 0000000..1f871dd --- /dev/null +++ b/build/src/com/android/modules/utils/build/SdkLevel.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 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.modules.utils.build; + +import android.os.Build; + +/** + * Utility class to check SDK level. + * + * @hide + */ +public class SdkLevel { + + private SdkLevel() {} + + /** Return true iff the running Android SDK is at least "R". */ + public static boolean isAtLeastR() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; + } + + /** + * Returns true iff the running Android SDK is pre-release "S" or "T", built based on "R" SDK. + * + * If new SDK versions are added > R, then this method needs to be updated to recognise them + * (e.g. if we add SDK version for R-QPR, the current implementation will not recognise + * pre-release "S" versions built on that). + */ + public static boolean isAtLeastS() { + // TODO(b/170831689) This should check SDK_INT >= S once S sdk finalised. Note that removing the + // current conditions may lead to issues in mainlinefood (and possibly public beta?). + + // While in development, builds will have R SDK_INT and "S" or "T" codename. + // We don't accept SDK_INT > R for now, since R and S may have non-consecutive values. + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R + && ("S".equals(Build.VERSION.CODENAME) || "T".equals(Build.VERSION.CODENAME))) { + return true; + } + + return false; + } +} diff --git a/os/Android.bp b/os/Android.bp new file mode 100644 index 0000000..802ae89 --- /dev/null +++ b/os/Android.bp @@ -0,0 +1,42 @@ +// +// Copyright (C) 2020 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. + +java_library { + name: "modules-utils-os", + srcs: [ + "java/**/*.aidl", + "java/**/*.java", + ], + sdk_version: "module_current", + min_sdk_version: "30", + visibility: ["//visibility:public"], + apex_available: [ + "//apex_available:anyapex", + "//apex_available:platform", + ], +} + +filegroup { + name: "module-utils-os-aidls", + srcs: [ + "java/com/android/modules/utils/ParceledListSlice.aidl", + "java/com/android/modules/utils/StringParceledListSlice.aidl", + ], + path: "java", + visibility: [ + "//frameworks/base/wifi", + "//packages/modules/Wifi/framework", + ], +} diff --git a/os/java/com/android/modules/utils/BaseParceledListSlice.java b/os/java/com/android/modules/utils/BaseParceledListSlice.java new file mode 100644 index 0000000..86f06be --- /dev/null +++ b/os/java/com/android/modules/utils/BaseParceledListSlice.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2011 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.modules.utils; + +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Transfer a large list of Parcelable objects across an IPC. Splits into + * multiple transactions if needed. + * + * Caveat: for efficiency and security, all elements must be the same concrete type. + * In order to avoid writing the class name of each object, we must ensure that + * each object is the same type, or else unparceling then reparceling the data may yield + * a different result if the class name encoded in the Parcelable is a Base type. + * See b/17671747. + */ +abstract class BaseParceledListSlice<T> implements Parcelable { + private static String TAG = "ParceledListSlice"; + private static boolean DEBUG = false; + + /* + * TODO get this number from somewhere else. For now set it to a quarter of + * the 1MB limit. + */ + private static final int MAX_IPC_SIZE = IBinder.getSuggestedMaxIpcSizeBytes(); + + private final List<T> mList; + + private int mInlineCountLimit = Integer.MAX_VALUE; + + public BaseParceledListSlice(List<T> list) { + mList = list; + } + + @SuppressWarnings("unchecked") + BaseParceledListSlice(Parcel p, ClassLoader loader) { + final int N = p.readInt(); + mList = new ArrayList<T>(N); + if (DEBUG) Log.d(TAG, "Retrieving " + N + " items"); + if (N <= 0) { + return; + } + + Parcelable.Creator<?> creator = readParcelableCreator(p, loader); + Class<?> listElementClass = null; + + int i = 0; + while (i < N) { + if (p.readInt() == 0) { + break; + } + + final T parcelable = readCreator(creator, p, loader); + if (listElementClass == null) { + listElementClass = parcelable.getClass(); + } else { + verifySameType(listElementClass, parcelable.getClass()); + } + + mList.add(parcelable); + + if (DEBUG) Log.d(TAG, "Read inline #" + i + ": " + mList.get(mList.size()-1)); + i++; + } + if (i >= N) { + return; + } + final IBinder retriever = p.readStrongBinder(); + while (i < N) { + if (DEBUG) Log.d(TAG, "Reading more @" + i + " of " + N + ": retriever=" + retriever); + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInt(i); + try { + retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0); + } catch (RemoteException e) { + Log.w(TAG, "Failure retrieving array; only received " + i + " of " + N, e); + return; + } + while (i < N && reply.readInt() != 0) { + final T parcelable = readCreator(creator, reply, loader); + verifySameType(listElementClass, parcelable.getClass()); + + mList.add(parcelable); + + if (DEBUG) Log.d(TAG, "Read extra #" + i + ": " + mList.get(mList.size()-1)); + i++; + } + reply.recycle(); + data.recycle(); + } + } + + private T readCreator(Parcelable.Creator<?> creator, Parcel p, ClassLoader loader) { + if (creator instanceof Parcelable.ClassLoaderCreator<?>) { + Parcelable.ClassLoaderCreator<?> classLoaderCreator = + (Parcelable.ClassLoaderCreator<?>) creator; + return (T) classLoaderCreator.createFromParcel(p, loader); + } + return (T) creator.createFromParcel(p); + } + + private static void verifySameType(final Class<?> expected, final Class<?> actual) { + if (!actual.equals(expected)) { + throw new IllegalArgumentException("Can't unparcel type " + + (actual == null ? null : actual.getName()) + " in list of type " + + (expected == null ? null : expected.getName())); + } + } + + public List<T> getList() { + return mList; + } + + /** + * Set a limit on the maximum number of entries in the array that will be included + * inline in the initial parcelling of this object. + */ + public void setInlineCountLimit(int maxCount) { + mInlineCountLimit = maxCount; + } + + /** + * Write this to another Parcel. Note that this discards the internal Parcel + * and should not be used anymore. This is so we can pass this to a Binder + * where we won't have a chance to call recycle on this. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + final int N = mList.size(); + final int callFlags = flags; + dest.writeInt(N); + if (DEBUG) Log.d(TAG, "Writing " + N + " items"); + if (N > 0) { + final Class<?> listElementClass = mList.get(0).getClass(); + writeParcelableCreator(mList.get(0), dest); + int i = 0; + while (i < N && i < mInlineCountLimit && dest.dataSize() < MAX_IPC_SIZE) { + dest.writeInt(1); + + final T parcelable = mList.get(i); + verifySameType(listElementClass, parcelable.getClass()); + writeElement(parcelable, dest, callFlags); + + if (DEBUG) Log.d(TAG, "Wrote inline #" + i + ": " + mList.get(i)); + i++; + } + if (i < N) { + dest.writeInt(0); + Binder retriever = new Binder() { + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + if (code != FIRST_CALL_TRANSACTION) { + return super.onTransact(code, data, reply, flags); + } + int i = data.readInt(); + if (DEBUG) Log.d(TAG, "Writing more @" + i + " of " + N); + while (i < N && reply.dataSize() < MAX_IPC_SIZE) { + reply.writeInt(1); + + final T parcelable = mList.get(i); + verifySameType(listElementClass, parcelable.getClass()); + writeElement(parcelable, reply, callFlags); + + if (DEBUG) Log.d(TAG, "Wrote extra #" + i + ": " + mList.get(i)); + i++; + } + if (i < N) { + if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N); + reply.writeInt(0); + } + return true; + } + }; + if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N + ": retriever=" + retriever); + dest.writeStrongBinder(retriever); + } + } + } + + protected abstract void writeElement(T parcelable, Parcel reply, int callFlags); + + protected abstract void writeParcelableCreator(T parcelable, Parcel dest); + + protected abstract Parcelable.Creator<?> readParcelableCreator(Parcel from, ClassLoader loader); +} diff --git a/os/java/com/android/modules/utils/BasicShellCommandHandler.java b/os/java/com/android/modules/utils/BasicShellCommandHandler.java new file mode 100644 index 0000000..32e6f79 --- /dev/null +++ b/os/java/com/android/modules/utils/BasicShellCommandHandler.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2019 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.modules.utils; + +import android.util.Log; +import android.os.Binder; + +import java.io.BufferedInputStream; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; + +/** + * Helper for implementing {@link Binder#onShellCommand Binder.onShellCommand}. This is meant to + * be copied into mainline modules, so this class must not use any hidden APIs. + */ +public abstract class BasicShellCommandHandler { + protected static final String TAG = "ShellCommand"; + protected static final boolean DEBUG = false; + + private Binder mTarget; + private FileDescriptor mIn; + private FileDescriptor mOut; + private FileDescriptor mErr; + private String[] mArgs; + + private String mCmd; + private int mArgPos; + private String mCurArgData; + + private FileInputStream mFileIn; + private FileOutputStream mFileOut; + private FileOutputStream mFileErr; + + private PrintWriter mOutPrintWriter; + private PrintWriter mErrPrintWriter; + private InputStream mInputStream; + + public void init(Binder target, FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, int firstArgPos) { + mTarget = target; + mIn = in; + mOut = out; + mErr = err; + mArgs = args; + mCmd = null; + mArgPos = firstArgPos; + mCurArgData = null; + mFileIn = null; + mFileOut = null; + mFileErr = null; + mOutPrintWriter = null; + mErrPrintWriter = null; + mInputStream = null; + } + + public int exec(Binder target, FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args) { + String cmd; + int start; + if (args != null && args.length > 0) { + cmd = args[0]; + start = 1; + } else { + cmd = null; + start = 0; + } + init(target, in, out, err, args, start); + mCmd = cmd; + + if (DEBUG) { + RuntimeException here = new RuntimeException("here"); + here.fillInStackTrace(); + Log.d(TAG, "Starting command " + mCmd + " on " + mTarget, here); + Log.d(TAG, "Calling uid=" + Binder.getCallingUid() + + " pid=" + Binder.getCallingPid()); + } + int res = -1; + try { + res = onCommand(mCmd); + if (DEBUG) Log.d(TAG, "Executed command " + mCmd + " on " + mTarget); + } catch (Throwable e) { + // Unlike usual calls, in this case if an exception gets thrown + // back to us we want to print it back in to the dump data, since + // that is where the caller expects all interesting information to + // go. + PrintWriter eout = getErrPrintWriter(); + eout.println(); + eout.println("Exception occurred while executing '" + mCmd + "':"); + e.printStackTrace(eout); + } finally { + if (DEBUG) Log.d(TAG, "Flushing output streams on " + mTarget); + if (mOutPrintWriter != null) { + mOutPrintWriter.flush(); + } + if (mErrPrintWriter != null) { + mErrPrintWriter.flush(); + } + if (DEBUG) Log.d(TAG, "Sending command result on " + mTarget); + } + if (DEBUG) Log.d(TAG, "Finished command " + mCmd + " on " + mTarget); + return res; + } + + /** + * Return the raw FileDescriptor for the output stream. + */ + public FileDescriptor getOutFileDescriptor() { + return mOut; + } + + /** + * Return direct raw access (not buffered) to the command's output data stream. + */ + public OutputStream getRawOutputStream() { + if (mFileOut == null) { + mFileOut = new FileOutputStream(mOut); + } + return mFileOut; + } + + /** + * Return a PrintWriter for formatting output to {@link #getRawOutputStream()}. + */ + public PrintWriter getOutPrintWriter() { + if (mOutPrintWriter == null) { + mOutPrintWriter = new PrintWriter(getRawOutputStream()); + } + return mOutPrintWriter; + } + + /** + * Return the raw FileDescriptor for the error stream. + */ + public FileDescriptor getErrFileDescriptor() { + return mErr; + } + + /** + * Return direct raw access (not buffered) to the command's error output data stream. + */ + public OutputStream getRawErrorStream() { + if (mFileErr == null) { + mFileErr = new FileOutputStream(mErr); + } + return mFileErr; + } + + /** + * Return a PrintWriter for formatting output to {@link #getRawErrorStream()}. + */ + public PrintWriter getErrPrintWriter() { + if (mErr == null) { + return getOutPrintWriter(); + } + if (mErrPrintWriter == null) { + mErrPrintWriter = new PrintWriter(getRawErrorStream()); + } + return mErrPrintWriter; + } + + /** + * Return the raw FileDescriptor for the input stream. + */ + public FileDescriptor getInFileDescriptor() { + return mIn; + } + + /** + * Return direct raw access (not buffered) to the command's input data stream. + */ + public InputStream getRawInputStream() { + if (mFileIn == null) { + mFileIn = new FileInputStream(mIn); + } + return mFileIn; + } + + /** + * Return buffered access to the command's {@link #getRawInputStream()}. + */ + public InputStream getBufferedInputStream() { + if (mInputStream == null) { + mInputStream = new BufferedInputStream(getRawInputStream()); + } + return mInputStream; + } + + /** + * Return the next option on the command line -- that is an argument that + * starts with '-'. If the next argument is not an option, null is returned. + */ + public String getNextOption() { + if (mCurArgData != null) { + String prev = mArgs[mArgPos - 1]; + throw new IllegalArgumentException("No argument expected after \"" + prev + "\""); + } + if (mArgPos >= mArgs.length) { + return null; + } + String arg = mArgs[mArgPos]; + if (!arg.startsWith("-")) { + return null; + } + mArgPos++; + if (arg.equals("--")) { + return null; + } + if (arg.length() > 1 && arg.charAt(1) != '-') { + if (arg.length() > 2) { + mCurArgData = arg.substring(2); + return arg.substring(0, 2); + } else { + mCurArgData = null; + return arg; + } + } + mCurArgData = null; + return arg; + } + + /** + * Return the next argument on the command line, whatever it is; if there are + * no arguments left, return null. + */ + public String getNextArg() { + if (mCurArgData != null) { + String arg = mCurArgData; + mCurArgData = null; + return arg; + } else if (mArgPos < mArgs.length) { + return mArgs[mArgPos++]; + } else { + return null; + } + } + + public String peekNextArg() { + if (mCurArgData != null) { + return mCurArgData; + } else if (mArgPos < mArgs.length) { + return mArgs[mArgPos]; + } else { + return null; + } + } + + /** + * @return all the remaining arguments in the command without moving the current position. + */ + public String[] peekRemainingArgs() { + int remaining = getRemainingArgsCount(); + String[] args = new String[remaining]; + for (int pos = mArgPos; pos < mArgs.length; pos++) { + args[pos - mArgPos] = mArgs[pos]; + } + return args; + } + + /** + * Returns number of arguments that haven't been processed yet. + */ + public int getRemainingArgsCount() { + if (mArgPos >= mArgs.length) { + return 0; + } + return mArgs.length - mArgPos; + } + + /** + * Return the next argument on the command line, whatever it is; if there are + * no arguments left, throws an IllegalArgumentException to report this to the user. + */ + public String getNextArgRequired() { + String arg = getNextArg(); + if (arg == null) { + String prev = mArgs[mArgPos - 1]; + throw new IllegalArgumentException("Argument expected after \"" + prev + "\""); + } + return arg; + } + + public int handleDefaultCommands(String cmd) { + if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) { + onHelp(); + } else { + getOutPrintWriter().println("Unknown command: " + cmd); + } + return -1; + } + + public Binder getTarget() { + return mTarget; + } + + public String[] getAllArgs() { + return mArgs; + } + + /** + * Implement parsing and execution of a command. If it isn't a command you understand, + * call {@link #handleDefaultCommands(String)} and return its result as a last resort. + * Use {@link #getNextOption()}, {@link #getNextArg()}, and {@link #getNextArgRequired()} + * to process additional command line arguments. Command output can be written to + * {@link #getOutPrintWriter()} and errors to {@link #getErrPrintWriter()}. + * + * <p class="caution">Note that no permission checking has been done before entering this + * function, so you need to be sure to do your own security verification for any commands you + * are executing. The easiest way to do this is to have the ShellCommand contain + * only a reference to your service's aidl interface, and do all of your command + * implementations on top of that -- that way you can rely entirely on your executing security + * code behind that interface.</p> + * + * @param cmd The first command line argument representing the name of the command to execute. + * @return Return the command result; generally 0 or positive indicates success and + * negative values indicate error. + */ + public abstract int onCommand(String cmd); + + /** + * Implement this to print help text about your command to {@link #getOutPrintWriter()}. + */ + public abstract void onHelp(); +} diff --git a/os/java/com/android/modules/utils/ParceledListSlice.aidl b/os/java/com/android/modules/utils/ParceledListSlice.aidl new file mode 100644 index 0000000..bbe8e2c --- /dev/null +++ b/os/java/com/android/modules/utils/ParceledListSlice.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2011, 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.modules.utils; + +parcelable ParceledListSlice<T>; diff --git a/os/java/com/android/modules/utils/ParceledListSlice.java b/os/java/com/android/modules/utils/ParceledListSlice.java new file mode 100644 index 0000000..24a85d8 --- /dev/null +++ b/os/java/com/android/modules/utils/ParceledListSlice.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011 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.modules.utils; + +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Collections; +import java.util.List; + +/** + * Transfer a large list of Parcelable objects across an IPC. Splits into + * multiple transactions if needed. + * + * @see BaseParceledListSlice + */ +public class ParceledListSlice<T extends Parcelable> extends BaseParceledListSlice<T> { + public ParceledListSlice(List<T> list) { + super(list); + } + + private ParceledListSlice(Parcel in, ClassLoader loader) { + super(in, loader); + } + + public static <T extends Parcelable> ParceledListSlice<T> emptyList() { + return new ParceledListSlice<T>(Collections.<T> emptyList()); + } + + @Override + public int describeContents() { + int contents = 0; + final List<T> list = getList(); + for (int i=0; i<list.size(); i++) { + contents |= list.get(i).describeContents(); + } + return contents; + } + + @Override + protected void writeElement(T parcelable, Parcel dest, int callFlags) { + parcelable.writeToParcel(dest, callFlags); + } + + @Override + protected void writeParcelableCreator(T parcelable, Parcel dest) { + dest.writeParcelableCreator((Parcelable) parcelable); + } + + @Override + protected Parcelable.Creator<?> readParcelableCreator(Parcel from, ClassLoader loader) { + return from.readParcelableCreator(loader); + } + +// @SuppressWarnings("unchecked") + public static final Parcelable.ClassLoaderCreator<ParceledListSlice> CREATOR = + new Parcelable.ClassLoaderCreator<ParceledListSlice>() { + public ParceledListSlice createFromParcel(Parcel in) { + return new ParceledListSlice(in, null); + } + + @Override + public ParceledListSlice createFromParcel(Parcel in, ClassLoader loader) { + return new ParceledListSlice(in, loader); + } + + @Override + public ParceledListSlice[] newArray(int size) { + return new ParceledListSlice[size]; + } + }; +} diff --git a/os/java/com/android/modules/utils/StringParceledListSlice.aidl b/os/java/com/android/modules/utils/StringParceledListSlice.aidl new file mode 100644 index 0000000..83b9711 --- /dev/null +++ b/os/java/com/android/modules/utils/StringParceledListSlice.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2017, 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.modules.utils; + +parcelable StringParceledListSlice; diff --git a/os/java/com/android/modules/utils/StringParceledListSlice.java b/os/java/com/android/modules/utils/StringParceledListSlice.java new file mode 100644 index 0000000..4c688a8 --- /dev/null +++ b/os/java/com/android/modules/utils/StringParceledListSlice.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2017 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.modules.utils; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Collections; +import java.util.List; + +/** + * Transfer a large list of Parcelable objects across an IPC. Splits into + * multiple transactions if needed. + * + * @see BaseParceledListSlice + */ +public class StringParceledListSlice extends BaseParceledListSlice<String> { + public StringParceledListSlice(List<String> list) { + super(list); + } + + private StringParceledListSlice(Parcel in, ClassLoader loader) { + super(in, loader); + } + + public static StringParceledListSlice emptyList() { + return new StringParceledListSlice(Collections.<String> emptyList()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + protected void writeElement(String parcelable, Parcel reply, int callFlags) { + reply.writeString(parcelable); + } + + @Override + protected void writeParcelableCreator(String parcelable, Parcel dest) { + return; + } + + @Override + protected Parcelable.Creator<?> readParcelableCreator(Parcel from, ClassLoader loader) { + return Parcel.STRING_CREATOR; + } + + @SuppressWarnings("unchecked") + public static final Parcelable.ClassLoaderCreator<StringParceledListSlice> CREATOR = + new Parcelable.ClassLoaderCreator<StringParceledListSlice>() { + public StringParceledListSlice createFromParcel(Parcel in) { + return new StringParceledListSlice(in, null); + } + + @Override + public StringParceledListSlice createFromParcel(Parcel in, ClassLoader loader) { + return new StringParceledListSlice(in, loader); + } + + @Override + public StringParceledListSlice[] newArray(int size) { + return new StringParceledListSlice[size]; + } + }; +} diff --git a/os/tests/Android.bp b/os/tests/Android.bp new file mode 100644 index 0000000..5434d0d --- /dev/null +++ b/os/tests/Android.bp @@ -0,0 +1,20 @@ +android_test { + name: "modules-utils-os-tests", + + sdk_version: "module_current", + min_sdk_version: "30", + + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "modules-utils-os", + "androidx.test.runner", + "androidx.test.rules", + ], + + libs: [ + "android.test.runner", + ], +} diff --git a/os/tests/AndroidManifest.xml b/os/tests/AndroidManifest.xml new file mode 100644 index 0000000..8fdfba3 --- /dev/null +++ b/os/tests/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.modules.utils.tests"> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.modules.utils.tests" + android:label="Modules-Utils Tests" /> +</manifest> diff --git a/os/tests/src/com/android/modules/utils/ParceledListSliceTest.java b/os/tests/src/com/android/modules/utils/ParceledListSliceTest.java new file mode 100644 index 0000000..c1457d6 --- /dev/null +++ b/os/tests/src/com/android/modules/utils/ParceledListSliceTest.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2020 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.modules.utils; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.test.filters.LargeTest; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@LargeTest +public class ParceledListSliceTest extends TestCase { + + public void testSmallList() throws Exception { + final int objectCount = 100; + List<SmallObject> list = new ArrayList<SmallObject>(); + for (int i = 0; i < objectCount; i++) { + list.add(new SmallObject(i * 2, (i * 2) + 1)); + } + + ParceledListSlice<SmallObject> slice; + + Parcel parcel = Parcel.obtain(); + try { + parcel.writeParcelable(new ParceledListSlice<SmallObject>(list), 0); + parcel.setDataPosition(0); + slice = parcel.readParcelable(getClass().getClassLoader()); + } finally { + parcel.recycle(); + } + + assertNotNull(slice); + assertNotNull(slice.getList()); + assertEquals(objectCount, slice.getList().size()); + + for (int i = 0; i < objectCount; i++) { + assertEquals(i * 2, slice.getList().get(i).mFieldA); + assertEquals((i * 2) + 1, slice.getList().get(i).mFieldB); + } + } + + private static int measureLargeObject() { + Parcel p = Parcel.obtain(); + try { + new LargeObject(0, 0, 0, 0, 0).writeToParcel(p, 0); + return p.dataPosition(); + } finally { + p.recycle(); + } + } + + /** + * Test that when the list is large, the data is successfully parceled + * and unparceled (the implementation will send pieces of the list in + * separate round-trips to avoid the IPC limit). + */ + public void testLargeList() throws Exception { + final int thresholdBytes = 256 * 1024; + final int objectCount = thresholdBytes / measureLargeObject(); + + List<LargeObject> list = new ArrayList<LargeObject>(); + for (int i = 0; i < objectCount; i++) { + list.add(new LargeObject( + i * 5, + (i * 5) + 1, + (i * 5) + 2, + (i * 5) + 3, + (i * 5) + 4 + )); + } + + ParceledListSlice<LargeObject> slice; + + Parcel parcel = Parcel.obtain(); + try { + parcel.writeParcelable(new ParceledListSlice<LargeObject>(list), 0); + parcel.setDataPosition(0); + slice = parcel.readParcelable(getClass().getClassLoader()); + } finally { + parcel.recycle(); + } + + assertNotNull(slice); + assertNotNull(slice.getList()); + assertEquals(objectCount, slice.getList().size()); + + for (int i = 0; i < objectCount; i++) { + assertEquals(i * 5, slice.getList().get(i).mFieldA); + assertEquals((i * 5) + 1, slice.getList().get(i).mFieldB); + assertEquals((i * 5) + 2, slice.getList().get(i).mFieldC); + assertEquals((i * 5) + 3, slice.getList().get(i).mFieldD); + assertEquals((i * 5) + 4, slice.getList().get(i).mFieldE); + } + } + + private void sendParcelStringList(List<String> list) { + StringParceledListSlice slice; + Parcel parcel = Parcel.obtain(); + + try { + parcel.writeParcelable(new StringParceledListSlice(list), 0); + parcel.setDataPosition(0); + slice = parcel.readParcelable(getClass().getClassLoader()); + } finally { + parcel.recycle(); + } + + assertNotNull(slice); + assertNotNull(slice.getList()); + assertEquals(list, slice.getList()); + } + + public void testStringList() throws Exception { + final int objectCount = 400; + List<String> list = new ArrayList<String>(); + for (long i = 0; i < objectCount; i++) { + list.add(Long.toString(i * (6 - i))); + } + + sendParcelStringList(list); + } + + public void testLargeStringList() throws Exception { + final int thresholdBytes = 256 * 1024; + final String value = Long.toString(Long.MAX_VALUE); + final int objectCount = 2 * thresholdBytes / value.length(); + final List<String> list = Collections.nCopies(objectCount, value); + + sendParcelStringList(list); + } + + + /** + * Test that only homogeneous elements may be unparceled. + */ + public void testHomogeneousElements() throws Exception { + List<BaseObject> list = new ArrayList<BaseObject>(); + list.add(new LargeObject(0, 1, 2, 3, 4)); + list.add(new SmallObject(5, 6)); + list.add(new SmallObject(7, 8)); + + Parcel parcel = Parcel.obtain(); + try { + writeEvilParceledListSlice(parcel, list); + parcel.setDataPosition(0); + try { + ParceledListSlice.CREATOR.createFromParcel(parcel, getClass().getClassLoader()); + assertTrue("Unparceled heterogeneous ParceledListSlice", false); + } catch (IllegalArgumentException e) { + // Success, we're not allowed to process heterogeneous + // elements in a ParceledListSlice. + } + } finally { + parcel.recycle(); + } + } + + /** + * Write a ParcelableListSlice that uses the BaseObject base class as the Creator. + * This is dangerous, as it may affect how the data is unparceled, then later parceled + * by the system, leading to a self-modifying data security vulnerability. + */ + private static <T extends BaseObject> void writeEvilParceledListSlice(Parcel dest, + List<T> list) { + final int listCount = list.size(); + + // Number of items. + dest.writeInt(listCount); + + // The type/creator to use when unparceling. Here we use the base class + // to simulate an attack on ParceledListSlice. + dest.writeString(BaseObject.class.getName()); + + for (int i = 0; i < listCount; i++) { + // 1 means the item is present. + dest.writeInt(1); + list.get(i).writeToParcel(dest, 0); + } + } + + public abstract static class BaseObject implements Parcelable { + protected static final int TYPE_SMALL = 0; + protected static final int TYPE_LARGE = 1; + + protected void writeToParcel(Parcel dest, int flags, int type) { + dest.writeInt(type); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * This is *REALLY* bad, but we're doing it in the test to ensure that we handle + * the possible exploit when unparceling an object with the BaseObject written as + * Creator. + */ + public static final Creator<BaseObject> CREATOR = new Creator<BaseObject>() { + @Override + public BaseObject createFromParcel(Parcel source) { + switch (source.readInt()) { + case TYPE_SMALL: + return SmallObject.createFromParcelBody(source); + case TYPE_LARGE: + return LargeObject.createFromParcelBody(source); + default: + throw new IllegalArgumentException("Unknown type"); + } + } + + @Override + public BaseObject[] newArray(int size) { + return new BaseObject[size]; + } + }; + } + + public static class SmallObject extends BaseObject { + public int mFieldA; + public int mFieldB; + + public SmallObject(int a, int b) { + mFieldA = a; + mFieldB = b; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags, TYPE_SMALL); + dest.writeInt(mFieldA); + dest.writeInt(mFieldB); + } + + public static SmallObject createFromParcelBody(Parcel source) { + return new SmallObject(source.readInt(), source.readInt()); + } + + public static final Creator<SmallObject> CREATOR = new Creator<SmallObject>() { + @Override + public SmallObject createFromParcel(Parcel source) { + // Consume the type (as it is always written out). + source.readInt(); + return createFromParcelBody(source); + } + + @Override + public SmallObject[] newArray(int size) { + return new SmallObject[size]; + } + }; + } + + public static class LargeObject extends BaseObject { + public int mFieldA; + public int mFieldB; + public int mFieldC; + public int mFieldD; + public int mFieldE; + + public LargeObject(int a, int b, int c, int d, int e) { + mFieldA = a; + mFieldB = b; + mFieldC = c; + mFieldD = d; + mFieldE = e; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags, TYPE_LARGE); + dest.writeInt(mFieldA); + dest.writeInt(mFieldB); + dest.writeInt(mFieldC); + dest.writeInt(mFieldD); + dest.writeInt(mFieldE); + } + + public static LargeObject createFromParcelBody(Parcel source) { + return new LargeObject( + source.readInt(), + source.readInt(), + source.readInt(), + source.readInt(), + source.readInt() + ); + } + + public static final Creator<LargeObject> CREATOR = new Creator<LargeObject>() { + @Override + public LargeObject createFromParcel(Parcel source) { + // Consume the type (as it is always written out). + source.readInt(); + return createFromParcelBody(source); + } + + @Override + public LargeObject[] newArray(int size) { + return new LargeObject[size]; + } + }; + } +} |