diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-14 16:38:14 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-14 16:38:14 +0000 |
commit | ccfe25a703ffeebb362d0c658cc0181f573c86dd (patch) | |
tree | 6e7ed8a11417623174cebe8164bdfab92e738b93 | |
parent | c25139786bc3b9d76d896bf6f23fc90168d33af9 (diff) | |
parent | 4d7d96a648941d07ef912c6becf1bbab822c41dd (diff) | |
download | platform_testing-aml_tz4_332714010.tar.gz |
Snap for 11219529 from 4d7d96a648941d07ef912c6becf1bbab822c41dd to mainline-tzdata4-releaseaml_tz4_332714050aml_tz4_332714010aml_tz4_332714010
Change-Id: I7b752d5d13adedfe82ef7290ae178511528bd1d5
60 files changed, 3917 insertions, 204 deletions
diff --git a/libraries/audio-test-harness/client-lib/Android.bp b/libraries/audio-test-harness/client-lib/Android.bp index 5b14d101b..554e901e3 100644 --- a/libraries/audio-test-harness/client-lib/Android.bp +++ b/libraries/audio-test-harness/client-lib/Android.bp @@ -65,7 +65,6 @@ java_library { java_test { name: "audiotestharness-client-grpclib-tests", - test_suites: ["general-tests"], host_supported: true, srcs: [ "src/test/java/com/android/media/audiotestharness/client/grpc/*.java", @@ -81,6 +80,6 @@ java_test { ], sdk_version: "current", test_options: { - unit_test: true, + unit_test: false, }, } diff --git a/libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java b/libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java index b8e458fbf..65e5c8fcb 100644 --- a/libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java +++ b/libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java @@ -18,36 +18,49 @@ package com.android.sts.common; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assume.assumeThat; +import static org.junit.Assume.assumeTrue; import android.app.Instrumentation; +import android.os.Handler; +import android.os.HandlerThread; import com.android.compatibility.common.util.SettingsUtils; -import java.io.IOException; import java.util.Optional; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; public class SystemUtil { + private static final String TAG = "SystemUtil"; + public static final long DEFAULT_MAX_POLL_TIME_MS = 30_000L; + public static final long DEFAULT_POLL_TIME_MS = 100L; /** * Set the value of a device setting and set it back to old value upon closing. * * @param instrumentation {@link Instrumentation} instance, obtained from a test running in - * instrumentation framework + * instrumentation framework * @param namespace "system", "secure", or "global" * @param key setting key to set * @param value setting value to set to * @return AutoCloseable that resets the setting back to existing value upon closing. */ - public static AutoCloseable withSetting(Instrumentation instrumentation, final String namespace, - final String key, String value) { + public static AutoCloseable withSetting( + Instrumentation instrumentation, + final String namespace, + final String key, + String value) { String getSettingRes = SettingsUtils.get(namespace, key); final Optional<String> oldSetting = Optional.ofNullable(getSettingRes); SettingsUtils.set(namespace, key, value); String getSettingCurrent = SettingsUtils.get(namespace, key); Optional<String> currSetting = Optional.ofNullable(getSettingCurrent); - assumeThat(String.format("Could not set %s:%s to %s", namespace, key, value), - currSetting.isPresent() ? currSetting.get().trim() : null, equalTo(value)); + assumeThat( + String.format("Could not set %s:%s to %s", namespace, key, value), + currSetting.isPresent() ? currSetting.get().trim() : null, + equalTo(value)); return new AutoCloseable() { @Override @@ -61,10 +74,69 @@ public class SystemUtil { String.format("could not reset '%s' back to '%s'", key, oldValue); String getSettingCurrent = SettingsUtils.get(namespace, key); Optional<String> currSetting = Optional.ofNullable(getSettingCurrent); - assumeThat(failMsg, currSetting.isPresent() ? currSetting.get().trim() : null, + assumeThat( + failMsg, + currSetting.isPresent() ? currSetting.get().trim() : null, equalTo(oldValue)); } } }; } + + /** + * Poll on a condition supplied by the user. + * + * @param waitCondition returns true when the polling condition is met, false otherwise. + * @return boolean value of {@code waitCondition}. + * @throws IllegalArgumentException when {@code pollingTime} is not a positive ineteger and is + * not less than {@code maxPollingTime}. + * @throws InterruptedException if the current thread is interrupted. + */ + public static boolean poll(BooleanSupplier waitCondition) + throws IllegalArgumentException, InterruptedException { + return poll(waitCondition, DEFAULT_POLL_TIME_MS, DEFAULT_MAX_POLL_TIME_MS); + } + + /** + * Poll on a condition supplied by the user. + * + * @param waitCondition returns true when the polling condition is met, false otherwise. + * @param pollingTime wait between successive calls to fetch value of {@code waitCondition} in + * milliseconds + * @param maxPollingTime maximum waiting time before return. + * @return boolean value of {@code waitCondition}. + * @throws IllegalArgumentException when {@code pollingTime} is not a positive ineteger and is + * not less than {@code maxPollingTime}. + * @throws InterruptedException if the current thread is interrupted. + */ + public static boolean poll(BooleanSupplier waitCondition, long pollingTime, long maxPollingTime) + throws IllegalArgumentException, InterruptedException { + // The value of pollingTime should be a positive integer + if (pollingTime <= 0) { + throw new IllegalArgumentException("pollingTime should be a positive integer"); + } + + // The value of pollingTime should be less than maxPollingTime + if (pollingTime >= maxPollingTime) { + throw new IllegalArgumentException("pollingTime should be less than maxPollingTime"); + } + + // Use handlerThread to run task in a separate thread. + final HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + final Semaphore semaphore = new Semaphore(0); + final long startTime = System.currentTimeMillis(); + do { + // Check for the status. + if (waitCondition.getAsBoolean()) { + return true; + } + + // Wait before checking status again. + handler.postDelayed(() -> semaphore.release(), pollingTime); + assumeTrue(semaphore.tryAcquire(maxPollingTime, TimeUnit.MILLISECONDS)); + } while (System.currentTimeMillis() - startTime <= maxPollingTime); + return waitCondition.getAsBoolean(); + } } diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java new file mode 100644 index 000000000..e4997d913 --- /dev/null +++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2023 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.sts.common; + +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; + +import java.util.Arrays; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** Util to parse dumpsys */ +public class DumpsysUtils { + + /** + * Fetch dumpsys for the service + * + * @param device the device {@link ITestDevice} to use. + * @param args the arguments {@link String} to filter output + * @return the raw output without newline character + * @throws Exception + */ + public static String getRawDumpsys(ITestDevice device, String args) throws Exception { + CommandResult output = device.executeShellV2Command("dumpsys " + args); + if (output.getStatus() != CommandStatus.SUCCESS) { + throw new IllegalStateException( + String.format( + "Failed to get dumpsys for %s details, due to : %s", + args, output.toString())); + } + return output.getStdout(); + } + + /** + * Parse the dumpsys for the service using pattern + * + * @param device the device {@link ITestDevice} to use. + * @param service the service name {@link String} to check status. + * @param args the argument {@link Map} to filter output + * @param pattern the pattern {@link String} to parse the dumpsys output + * @param matcherFlag the flags {@link String} to look while parsing the dumpsys output + * @return the required value. + * @throws Exception + */ + public static Matcher getParsedDumpsys( + ITestDevice device, + String service, + Map<String, String> args, + String pattern, + int matcherFlag) + throws Exception { + String arguments = + args == null + ? "" + : args.entrySet().stream() + .map(arg -> String.format("%s %s", arg.getKey(), arg.getValue())) + .collect(Collectors.joining(" ")); + String rawOutput = getRawDumpsys(device, String.format("%s %s", service, arguments)); + rawOutput = + String.join( + "", + Arrays.stream(rawOutput.split("\n")) + .map(e -> e.trim()) + .toArray(String[]::new)); + return Pattern.compile(pattern, matcherFlag).matcher(rawOutput); + } + + /** + * Parse the dumpsys for the service using pattern + * + * @param device the device {@link ITestDevice} to use. + * @param service the service name {@link String} to check status. + * @param pattern the pattern {@link String} to parse the dumpsys output + * @param matcherFlag the flags {@link String} to look while parsing the dumpsys output + * @return the required value. + * @throws Exception + */ + public static Matcher getParsedDumpsys( + ITestDevice device, String service, String pattern, int matcherFlag) throws Exception { + return getParsedDumpsys(device, service, null /* args */, pattern, matcherFlag); + } + + /** + * Check if output contains mResumed=true for the activity + * + * @param device the device {@link ITestDevice} to use. + * @param activityName the activity name {@link String} to check status. + * @return true, if mResumed=true. Else false. + * @throws Exception + */ + public static boolean hasActivityResumed(ITestDevice device, String activityName) + throws Exception { + return getParsedDumpsys( + device, + "activity" /* service */, + Map.of("-a", activityName) /* args */, + "mResumed=true" /* pattern */, + Pattern.CASE_INSENSITIVE /* matcherFlag */) + .find(); + } + + /** + * Check if output contains mVisible=true for the activity + * + * @param device the device {@link ITestDevice} to use. + * @param activityName the activity name {@link String} to check status. + * @return true, if mVisible=true. Else false. + * @throws Exception + */ + public static boolean isActivityVisible(ITestDevice device, String activityName) + throws Exception { + return getParsedDumpsys( + device, + "activity" /* service */, + Map.of("-a", activityName) /* args */, + "mVisible=true" /* pattern */, + Pattern.CASE_INSENSITIVE /* matcherFlag */) + .find(); + } + + /** + * Fetch the role-holder-name for the role-name under the userid + * + * @param device the device {@link ITestDevice} to use. + * @param roleName the role name {@link String} to fetch role holder's name. + * @param userId the userid {@link int} to fetch role holder's name for the user. + * @return holder name, if exits. Else null. + * @throws Exception + */ + public static String getRoleHolder(ITestDevice device, String roleName, int userId) + throws Exception { + // Fetch roles for the user + Matcher rolesMatcher = + getParsedDumpsys( + device, + "role" /* service */, + String.format("user_id=%d.+?roles=(?<roles>\\[.+?])", userId) /* pattern */, + Pattern.CASE_INSENSITIVE); + if (!rolesMatcher.find()) { + return null; + } + + // Fetch the holder's name for the role + Matcher holderMatcher = + Pattern.compile( + String.format("\\{name=%sholders=(?<holders>.+?)}", roleName), + Pattern.CASE_INSENSITIVE) + .matcher(rolesMatcher.group("roles")); + if (!holderMatcher.find()) { + return null; + } + return holderMatcher.group("holders").trim(); + } + + /** + * Fetch the role-holder-name for the role-name + * + * @param device the device {@link ITestDevice} to use. + * @param roleName the role name {@link String} to fetch role holder's name for the current + * user. + * @return holder name, if exits. Else null. + * @throws Exception + */ + public static String getRoleHolder(ITestDevice device, String roleName) throws Exception { + return getRoleHolder(device, roleName, device.getCurrentUser()); + } +} diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java index 12665b2fb..214ccc0d6 100644 --- a/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java +++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java @@ -17,9 +17,6 @@ package com.android.sts.common; import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeNoException; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.ITestDevice; @@ -48,71 +45,89 @@ public class MallocDebug implements AutoCloseable { Pattern.compile("^.*HAS INVALID TAG.*$", Pattern.MULTILINE), }; - private ITestDevice device; - private String processName; - private AutoCloseable setMallocDebugOptionsProperty; - private AutoCloseable setAttachedProgramProperty; - private AutoCloseable killProcess; + private ITestDevice mDevice; + private String mProcessName; + private boolean mWasAdbRoot; + private AutoCloseable mSetMallocDebugOptionsProperty; + private AutoCloseable mSetAttachedProgramProperty; + private AutoCloseable mProcessKill; private MallocDebug( ITestDevice device, String mallocDebugOption, String processName, boolean isService) throws DeviceNotAvailableException, TimeoutException, ProcessUtil.KillException { - this.device = device; - this.processName = processName; - - // It's an error if this is called while something else is also doing malloc debug. - assertNull( - MALLOC_DEBUG_OPTIONS_PROP + " is already set!", - device.getProperty(MALLOC_DEBUG_OPTIONS_PROP)); - CommandUtil.runAndCheck(device, "logcat -c"); + mDevice = device; + mProcessName = processName; + mWasAdbRoot = device.isAdbRoot(); + + String previousProperty = device.getProperty(MALLOC_DEBUG_OPTIONS_PROP); + if (previousProperty != null) { + // log if this is called while something else is also doing malloc debug. + CLog.w("%s is already set! <%s>", MALLOC_DEBUG_OPTIONS_PROP, previousProperty); + } try { - this.setMallocDebugOptionsProperty = + mSetMallocDebugOptionsProperty = SystemUtil.withProperty(device, MALLOC_DEBUG_OPTIONS_PROP, mallocDebugOption); - this.setAttachedProgramProperty = + mSetAttachedProgramProperty = SystemUtil.withProperty(device, MALLOC_DEBUG_PROGRAM_PROP, processName); + CommandUtil.runAndCheck(device, "logcat -c"); + // Kill and wait for the process to come back if we're attaching to a service - this.killProcess = null; + mProcessKill = null; if (isService) { - this.killProcess = ProcessUtil.withProcessKill(device, processName, null); + mProcessKill = ProcessUtil.withProcessKill(device, processName, null); ProcessUtil.waitProcessRunning(device, processName); } } catch (Throwable e1) { try { - if (setMallocDebugOptionsProperty != null) { - setMallocDebugOptionsProperty.close(); + if (mSetMallocDebugOptionsProperty != null) { + mSetMallocDebugOptionsProperty.close(); } - if (setAttachedProgramProperty != null) { - setAttachedProgramProperty.close(); + if (mSetAttachedProgramProperty != null) { + mSetAttachedProgramProperty.close(); } } catch (Exception e2) { CLog.e(e2); - fail( + throw new IllegalStateException( "Could not enable malloc debug. Additionally, there was an" + " exception while trying to reset device state. Tests after" - + " this may not work as expected!\n" - + e2); + + " this may not work as expected!", + e2); } - assumeNoException("Could not enable malloc debug", e1); + throw new IllegalStateException("Could not enable malloc debug", e1); } } @Override public void close() throws Exception { - device.waitForDeviceAvailable(); - setMallocDebugOptionsProperty.close(); - setAttachedProgramProperty.close(); - if (killProcess != null) { + mDevice.waitForDeviceAvailable(); + boolean isAdbRoot = mDevice.isAdbRoot(); + if (mWasAdbRoot) { + // regain root permissions to teardown + mDevice.enableAdbRoot(); + } + try { + mSetMallocDebugOptionsProperty.close(); + mSetAttachedProgramProperty.close(); + } catch (Exception e) { + throw new IllegalStateException("Could not disable malloc debug", e); + } + if (mProcessKill != null) { try { - killProcess.close(); - ProcessUtil.waitProcessRunning(device, processName); + mProcessKill.close(); + ProcessUtil.waitProcessRunning(mDevice, mProcessName); } catch (TimeoutException e) { - assumeNoException( - "Could not restart '" + processName + "' after disabling malloc debug", e); + throw new IllegalStateException( + "Could not restart '" + mProcessName + "' after disabling malloc debug", e); } } - String logcat = CommandUtil.runAndCheck(device, "logcat -d *:S malloc_debug:V").getStdout(); + String logcat = + CommandUtil.runAndCheck(mDevice, "logcat -d *:S malloc_debug:V").getStdout(); + if (!isAdbRoot) { + // restore nonroot status if the try-with-resources body unrooted + mDevice.disableAdbRoot(); + } assertNoMallocDebugErrors(logcat); } @@ -129,7 +144,7 @@ public class MallocDebug implements AutoCloseable { public static AutoCloseable withLibcMallocDebugOnService( ITestDevice device, String mallocDebugOptions, String processName) throws DeviceNotAvailableException, IllegalArgumentException, TimeoutException, - ProcessUtil.KillException { + ProcessUtil.KillException { if (processName == null || processName.isEmpty()) { throw new IllegalArgumentException("Service processName can't be empty"); } @@ -149,7 +164,7 @@ public class MallocDebug implements AutoCloseable { public static AutoCloseable withLibcMallocDebugOnNewProcess( ITestDevice device, String mallocDebugOptions, String processName) throws DeviceNotAvailableException, IllegalArgumentException, TimeoutException, - ProcessUtil.KillException { + ProcessUtil.KillException { if (processName == null || processName.isEmpty()) { throw new IllegalArgumentException("processName can't be empty"); } diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java index 87551a658..d8fd05fda 100644 --- a/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java +++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java @@ -16,7 +16,6 @@ package com.android.sts.common; - import com.android.ddmlib.Log; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.IFileEntry; @@ -30,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -59,6 +59,9 @@ public final class ProcessUtil { public static final long PROCESS_WAIT_TIMEOUT_MS = 10_000; public static final long PROCESS_POLL_PERIOD_MS = 250; + public static final String[] INTENT_QUERY_CMDS = { + "resolve-activity", "query-activities", "query-services", "query-receivers" + }; private ProcessUtil() {} @@ -180,8 +183,7 @@ public final class ProcessUtil { * @param pid the id of the process to wait until exited */ public static void waitPidExited(ITestDevice device, int pid) - throws TimeoutException, DeviceNotAvailableException, - KillException { + throws TimeoutException, DeviceNotAvailableException, KillException { waitPidExited(device, pid, PROCESS_WAIT_TIMEOUT_MS); } @@ -194,8 +196,7 @@ public final class ProcessUtil { * @param timeoutMs how long to wait before throwing a TimeoutException */ public static void waitPidExited(ITestDevice device, int pid, long timeoutMs) - throws TimeoutException, DeviceNotAvailableException, - KillException { + throws TimeoutException, DeviceNotAvailableException, KillException { long endTime = System.currentTimeMillis() + timeoutMs; CommandResult res = null; while (true) { @@ -230,8 +231,7 @@ public final class ProcessUtil { * @param timeoutMs how long to wait before throwing a TimeoutException */ public static void killPid(ITestDevice device, int pid, long timeoutMs) - throws DeviceNotAvailableException, TimeoutException, - KillException { + throws DeviceNotAvailableException, TimeoutException, KillException { killPid(device, pid, 9, timeoutMs); } @@ -244,10 +244,8 @@ public final class ProcessUtil { * @param timeoutMs how long to wait before throwing a TimeoutException */ public static void killPid(ITestDevice device, int pid, int signal, long timeoutMs) - throws DeviceNotAvailableException, TimeoutException, - KillException { - CommandResult res = - device.executeShellV2Command(String.format("kill -%d %d", signal, pid)); + throws DeviceNotAvailableException, TimeoutException, KillException { + CommandResult res = device.executeShellV2Command(String.format("kill -%d %d", signal, pid)); if (res.getStatus() != CommandStatus.SUCCESS) { String err = res.getStderr(); if (err.contains("invalid signal specification")) { @@ -272,8 +270,7 @@ public final class ProcessUtil { * @return whether any processes were killed */ public static boolean killAll(ITestDevice device, String pgrepRegex, long timeoutMs) - throws DeviceNotAvailableException, TimeoutException, - KillException { + throws DeviceNotAvailableException, TimeoutException, KillException { return killAll(device, pgrepRegex, timeoutMs, true); } @@ -289,8 +286,7 @@ public final class ProcessUtil { */ public static boolean killAll( ITestDevice device, String pgrepRegex, long timeoutMs, boolean expectExist) - throws DeviceNotAvailableException, TimeoutException, - KillException { + throws DeviceNotAvailableException, TimeoutException, KillException { Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex); if (!pids.isPresent()) { // no pids to kill @@ -350,8 +346,9 @@ public final class ProcessUtil { { try { if (!killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false)) { - Log.d(LOG_TAG, - String.format("did not kill any processes for %s", pgrepRegex)); + Log.d( + LOG_TAG, + String.format("did not kill any processes for %s", pgrepRegex)); } } catch (KillException e) { Log.d(LOG_TAG, "failed to kill a process"); @@ -432,22 +429,63 @@ public final class ProcessUtil { * * @param device device to be run on * @param process pgrep pattern of process to look for - * @param filenameSubstr part of file name/path loaded by the process + * @param filenamePattern the filename pattern to find * @return an Opotional of IFileEntry of the path of the file on the device if exists. */ public static Optional<IFileEntry> findFileLoadedByProcess( - ITestDevice device, String process, String filenameSubstr) + ITestDevice device, String process, Pattern filenamePattern) throws DeviceNotAvailableException { Optional<Integer> pid = ProcessUtil.pidOf(device, process); if (pid.isPresent()) { - String cmd = "lsof -p " + pid.get().toString() + " | awk '{print $NF}'"; + String cmd = "lsof -p " + pid.get().toString() + " | grep -o '/.*'"; String[] openFiles = CommandUtil.runAndCheck(device, cmd).getStdout().split("\n"); for (String f : openFiles) { - if (f.contains(filenameSubstr)) { + if (f.contains("Permission denied")) { + throw new IllegalStateException("no permission to read open files for process"); + } + if (filenamePattern.matcher(f).find()) { return Optional.of(device.getFileEntry(f.trim())); } } } return Optional.empty(); } + + /* + * To get application process pids of all applications that can handle the target intent + * @param queryCmd Query command to be used. One of the values present in INTENT_QUERY_CMDS + * @param intentOptions Map of intent option to value for target intent + * @param device device to be run on + * @return Optional Map of pid to process name of application processes that can handle the + target intent + */ + public static Optional<Map<Integer, String>> getAllProcessIdsFromComponents( + String queryCmd, Map<String, String> intentOptions, ITestDevice device) + throws DeviceNotAvailableException, RuntimeException { + if (!Arrays.asList(INTENT_QUERY_CMDS).contains(queryCmd)) { + throw new RuntimeException("Unknown command " + queryCmd); + } + String cmd = "pm " + queryCmd + " "; + for (Map.Entry<String, String> entry : intentOptions.entrySet()) { + cmd += entry.getKey() + " " + entry.getValue() + " "; + } + CommandResult result = device.executeShellV2Command(cmd); + String resultString = result.getStdout(); + Log.i(LOG_TAG, String.format("Executed cmd: %s \nOutput: %s", cmd, resultString)); + + // As target string (process name) is coming from system itself, regex here only checks for + // presence of valid characters in process name and not for the actual order of characters + Pattern processNamePattern = Pattern.compile("processName=(?<name>[a-zA-Z0-9_\\.:]+)"); + Matcher matcher = processNamePattern.matcher(resultString); + Map<Integer, String> pidNameMap = new HashMap<Integer, String>(); + while (matcher.find()) { + String process = matcher.group("name"); + pidsOf(device, process) + .ifPresent( + (pids) -> { + pidNameMap.putAll(pids); + }); + } + return pidNameMap.isEmpty() ? Optional.empty() : Optional.of(pidNameMap); + } } diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java new file mode 100644 index 000000000..7f8d8bc86 --- /dev/null +++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2023 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.sts.common; + +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; + +import java.util.HashMap; +import java.util.Map; + +/** Util to manage secondary user */ +public class UserUtils { + + public static class SecondaryUser { + private ITestDevice mDevice; + private String mName; // Name of the new user + private boolean mIsDemo; // User type : --demo + private boolean mIsEphemeral; // User type : --ephemeral + private boolean mIsForTesting; // User type : --for-testing + private boolean mIsGuest; // User type : --guest + private boolean mIsManaged; // User type : --managed + private boolean mIsPreCreateOnly; // User type : --pre-created-only + private boolean mIsRestricted; // User type : --restricted + private boolean mSwitch; // Switch to newly created user + private int mProfileOf; // Userid associated with managed user + private int mTestUserId; + private Map<String, String> mUserRestrictions; // Map of user-restrictions for new user + + /** + * Create an instance of secondary user. + * + * @param device the device {@link ITestDevice} to use. + * @throws Exception + */ + public SecondaryUser(ITestDevice device) throws Exception { + // Device should not be null + if (device == null) { + throw new IllegalArgumentException("Device should not be null"); + } + + // Check if device supports multiple users + if (!device.isMultiUserSupported()) { + throw new IllegalStateException("Device does not support multiple users"); + } + + mDevice = device; + mName = "testUser"; /* Default username */ + mUserRestrictions = new HashMap<String, String>(); + + // Set default value for all flags as false + mIsDemo = false; + mIsEphemeral = false; + mIsForTesting = false; + mIsGuest = false; + mIsManaged = false; + mIsPreCreateOnly = false; + mIsRestricted = false; + mSwitch = false; + } + + /** + * Set the user type as demo. + * + * @return this object for method chaining. + */ + public SecondaryUser demo() { + mIsDemo = true; + return this; + } + + /** + * Set the user type as ephemeral. + * + * @return this object for method chaining. + */ + public SecondaryUser ephemeral() { + mIsEphemeral = true; + return this; + } + + /** + * Set the user type as for-testing. + * + * @return this object for method chaining. + */ + public SecondaryUser forTesting() { + mIsForTesting = true; + return this; + } + + /** + * Set the user type as guest. + * + * @return this object for method chaining. + */ + public SecondaryUser guest() { + mIsGuest = true; + return this; + } + + /** + * Set the user type as managed. + * + * @param profileOf value is set as the userid associated with managed user. + * @return this object for method chaining. + */ + public SecondaryUser managed(int profileOf) { + mIsManaged = true; + mProfileOf = profileOf; + return this; + } + + /** + * Set the user type as pre-created-only. + * + * @return this object for method chaining. + */ + public SecondaryUser preCreateOnly() { + mIsPreCreateOnly = true; + return this; + } + + /** + * Set the user type as restricted. + * + * @return this object for method chaining. + */ + public SecondaryUser restricted() { + mIsRestricted = true; + return this; + } + + /** + * Set the name of the new user. + * + * @param name value is set to name of the user. + * @return this object for method chaining. + * @throws IllegalArgumentException when {@code name} is null. + */ + public SecondaryUser name(String name) throws IllegalArgumentException { + // The argument 'name' should not be null + if (mName == null) { + throw new IllegalArgumentException("The name of the user should not be null"); + } + + mName = name; + return this; + } + + /** + * Set if switching to newly created user is required. + * + * @return this object for method chaining. + */ + public SecondaryUser doSwitch() { + mSwitch = true; + return this; + } + + /** + * Set user-restrictions on newly created secondary user. + * Note: Setting user-restrictions requires enabling root. + * + * @return this object for method chaining. + */ + public SecondaryUser withUserRestrictions(Map<String, String> restrictions) { + mUserRestrictions.putAll(restrictions); + return this; + } + + /** + * Create a secondary user and if required, switch to it. Returns an Autocloseable that + * removes the secondary user. + * + * @return AutoCloseable that switches back to the caller user if required, and removes the + * secondary user. + * @throws Exception + */ + public AutoCloseable withUser() throws Exception { + // Fetch the caller's user id + final int callerUserId = mDevice.getCurrentUser(); + + // Command to create user + String command = + "pm create-user " + + (mIsDemo ? "--demo " : "") + + (mIsEphemeral ? "--ephemeral " : "") + + (mIsGuest ? "--guest " : "") + + (mIsManaged ? ("--profileOf " + mProfileOf + " --managed ") : "") + + (mIsPreCreateOnly ? "--pre-create-only " : "") + + (mIsRestricted ? "--restricted " : "") + + (mIsForTesting && mDevice.getApiLevel() >= 34 ? "--for-testing " : "") + + mName; + + // Create a new user + final CommandResult output = mDevice.executeShellV2Command(command); + if (output.getStatus() != CommandStatus.SUCCESS) { + throw new IllegalStateException( + String.format("Failed to create user, due to : %s", output.toString())); + } + final String outputStdout = output.getStdout(); + mTestUserId = + Integer.parseInt(outputStdout.substring(outputStdout.lastIndexOf(" ")).trim()); + + AutoCloseable asSecondaryUser = + () -> { + // Switch back to the caller user if required and the user type is + // neither managed nor pre-created-only + if (mSwitch && !mIsManaged && !mIsPreCreateOnly) { + mDevice.switchUser(callerUserId); + } + + // Stop and remove the user if user type is not ephemeral + if (!mIsEphemeral) { + mDevice.stopUser(mTestUserId); + mDevice.removeUser(mTestUserId); + } + }; + + // Start the user + if (!mDevice.startUser(mTestUserId, true /* waitFlag */)) { + // Remove the user + asSecondaryUser.close(); + throw new IllegalStateException( + String.format("Failed to start the user: %s", mTestUserId)); + } + + // Add user-restrictions to newly created secondary user + if (!mUserRestrictions.isEmpty()) { + if (!mDevice.isAdbRoot()) { + throw new IllegalStateException("Setting user-restriction requires root"); + } + + for (Map.Entry<String, String> entry : mUserRestrictions.entrySet()) { + final CommandResult cmdOutput = + mDevice.executeShellV2Command( + String.format( + "pm set-user-restriction --user %d %s %s", + mTestUserId, entry.getKey(), entry.getValue())); + if (cmdOutput.getStatus() != CommandStatus.SUCCESS) { + asSecondaryUser.close(); + throw new IllegalStateException( + String.format( + "Failed to set user restriction %s value %s with" + + " message %s", + entry.getKey(), entry.getValue(), cmdOutput.toString())); + } + } + } + + // Switch to the user if required and the user type is neither managed nor + // pre-created-only + if (mSwitch && !mIsManaged && !mIsPreCreateOnly && !mDevice.switchUser(mTestUserId)) { + // Stop and remove the user + asSecondaryUser.close(); + throw new IllegalStateException( + String.format("Failed to switch the user: %s", mTestUserId)); + } + return asSecondaryUser; + } + } +} diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java new file mode 100644 index 000000000..25252d12a --- /dev/null +++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 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.sts.common.util; + +import com.android.sts.common.CommandUtil; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.util.CommandResult; + +/** Tools for comparing kernel versions */ +public final class KernelVersionHost { + + private KernelVersionHost() {} + + /** + * Get the device kernel version + * + * @param device The device to collect the kernel version from + */ + public static KernelVersion getKernelVersion(ITestDevice device) + throws DeviceNotAvailableException { + // https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md + // uname is part of Android since 6.0 Marshmallow + CommandResult res = CommandUtil.runAndCheck(device, "uname -r"); + return KernelVersion.parse(res.getStdout()); + } + + /** + * Helper for BusinessLogic + * + * @param device The device to test against + * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel" + */ + public static boolean isKernelVersionEqualTo(ITestDevice device, String testVersion) + throws DeviceNotAvailableException { + KernelVersion deviceKernelVersion = getKernelVersion(device); + KernelVersion testKernelVersion = KernelVersion.parse(testVersion); + return deviceKernelVersion.compareTo(testKernelVersion) == 0; + } + + /** + * Helper for BusinessLogic + * + * @param device The device to test against + * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel" + */ + public static boolean isKernelVersionLessThan(ITestDevice device, String testVersion) + throws DeviceNotAvailableException { + KernelVersion deviceKernelVersion = getKernelVersion(device); + KernelVersion testKernelVersion = KernelVersion.parse(testVersion); + return deviceKernelVersion.compareTo(testKernelVersion) < 0; + } + + /** + * Helper for BusinessLogic + * + * @param device The device to test against + * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel" + */ + public static boolean isKernelVersionLessThanEqualTo(ITestDevice device, String testVersion) + throws DeviceNotAvailableException { + KernelVersion deviceKernelVersion = getKernelVersion(device); + KernelVersion testKernelVersion = KernelVersion.parse(testVersion); + return deviceKernelVersion.compareTo(testKernelVersion) <= 0; + } + + /** + * Helper for BusinessLogic + * + * @param device The device to test against + * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel" + */ + public static boolean isKernelVersionGreaterThan(ITestDevice device, String testVersion) + throws DeviceNotAvailableException { + KernelVersion deviceKernelVersion = getKernelVersion(device); + KernelVersion testKernelVersion = KernelVersion.parse(testVersion); + return deviceKernelVersion.compareTo(testKernelVersion) > 0; + } + + /** + * Helper for BusinessLogic + * + * @param device The device to test against + * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel" + */ + public static boolean isKernelVersionGreaterThanEqualTo(ITestDevice device, String testVersion) + throws DeviceNotAvailableException { + KernelVersion deviceKernelVersion = getKernelVersion(device); + KernelVersion testKernelVersion = KernelVersion.parse(testVersion); + return deviceKernelVersion.compareTo(testKernelVersion) >= 0; + } +} diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java index 03b19341d..f82a90756 100644 --- a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java +++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java @@ -16,9 +16,16 @@ package com.android.sts.common; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,6 +52,26 @@ public class MallocDebugTest extends BaseHostJUnit4Test { } } + @Before + public void setUp() throws Exception { + assertWithMessage("libc.debug.malloc.options not empty before test") + .that(getDevice().getProperty("libc.debug.malloc.options")) + .isNull(); + assertWithMessage("libc.debug.malloc.programs not empty before test") + .that(getDevice().getProperty("libc.debug.malloc.programs")) + .isNull(); + } + + @After + public void tearDown() throws Exception { + assertWithMessage("libc.debug.malloc.options not empty after test") + .that(getDevice().getProperty("libc.debug.malloc.options")) + .isNull(); + assertWithMessage("libc.debug.malloc.programs not empty after test") + .that(getDevice().getProperty("libc.debug.malloc.programs")) + .isNull(); + } + @Test(expected = Test.None.class /* no exception expected */) public void testMallocDebugNoErrors() throws Exception { MallocDebug.assertNoMallocDebugErrors(logcatWithoutErrors); @@ -54,4 +81,63 @@ public class MallocDebugTest extends BaseHostJUnit4Test { public void testMallocDebugWithErrors() throws Exception { MallocDebug.assertNoMallocDebugErrors(logcatWithErrors); } + + @Test(expected = IllegalStateException.class) + public void testMallocDebugAutocloseableNonRoot() throws Exception { + assertTrue(getDevice().disableAdbRoot()); + try (AutoCloseable mallocDebug = + MallocDebug.withLibcMallocDebugOnNewProcess( + getDevice(), "backtrace guard", "native-poc")) { + // empty + } + } + + @Test + public void testMallocDebugAutocloseableRoot() throws Exception { + assertTrue("must test with rootable device", getDevice().enableAdbRoot()); + try (AutoCloseable mallocDebug = + MallocDebug.withLibcMallocDebugOnNewProcess( + getDevice(), "backtrace guard", "native-poc")) { + // empty + } + } + + @Test + public void testMallocDebugAutocloseableNonRootCleanup() throws Exception { + assertTrue("must test with rootable device", getDevice().enableAdbRoot()); + try (AutoCloseable mallocDebug = + MallocDebug.withLibcMallocDebugOnNewProcess( + getDevice(), "backtrace guard", "native-poc")) { + assertTrue("could not disable root", getDevice().disableAdbRoot()); + } + assertFalse( + "device should not be root after autoclose if the body unrooted", + getDevice().isAdbRoot()); + } + + @Test + public void testMallocDebugAutoseablePriorValueNoException() throws Exception { + assertTrue("must test with rootable device", getDevice().enableAdbRoot()); + final String oldValue = "TEST_VALUE_OLD"; + final String newValue = "TEST_VALUE_NEW"; + assertTrue( + "could not set libc.debug.malloc.options", + getDevice().setProperty("libc.debug.malloc.options", oldValue)); + assertWithMessage("test property was not properly set on device") + .that(getDevice().getProperty("libc.debug.malloc.options")) + .isEqualTo(oldValue); + try (AutoCloseable mallocDebug = + MallocDebug.withLibcMallocDebugOnNewProcess(getDevice(), newValue, "native-poc")) { + assertWithMessage("new property was not set during malloc debug body") + .that(getDevice().getProperty("libc.debug.malloc.options")) + .isEqualTo(newValue); + } + String afterValue = getDevice().getProperty("libc.debug.malloc.options"); + assertTrue( + "could not clear libc.debug.malloc.options", + getDevice().setProperty("libc.debug.malloc.options", "")); + assertWithMessage("prior property was not restored after test") + .that(afterValue) + .isEqualTo(oldValue); + } } diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java new file mode 100644 index 000000000..c6f4c6963 --- /dev/null +++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 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.sts.common; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertTrue; + +import com.android.tradefed.device.IFileEntry; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Optional; +import java.util.regex.Pattern; + +/** Unit tests for {@link ProcessUtil}. */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class ProcessUtilTest extends BaseHostJUnit4Test { + + @Before + public void setUp() throws Exception { + assertTrue("could not unroot", getDevice().disableAdbRoot()); + } + + @After + public void tearDown() throws Exception { + assertTrue("could not unroot", getDevice().disableAdbRoot()); + } + + @Test(expected = IllegalStateException.class) + public void testFindLoadedByProcessNonRoot() throws Exception { + // expect failure because the shell user has no permission to read process info of other + // users + ProcessUtil.findFileLoadedByProcess( + getDevice(), "system_server", Pattern.compile(Pattern.quote("libc.so"))); + } + + @Test(expected = IllegalArgumentException.class) + public void testFindLoadedByProcessMultipleProcesses() throws Exception { + // pattern 'android' has multiple (android.hardware.drm, android.hardware.gnss, etc) + ProcessUtil.findFileLoadedByProcess(getDevice(), "android", null); + } + + @Test + public void testFindLoadedByProcessUtilRoot() throws Exception { + assertTrue("must test with rootable device", getDevice().enableAdbRoot()); + Optional<IFileEntry> fileEntryOptional = + ProcessUtil.findFileLoadedByProcess( + getDevice(), "system_server", Pattern.compile(Pattern.quote("libc.so"))); + assertWithMessage("file entry should not be empty") + .that(fileEntryOptional.isPresent()) + .isTrue(); + IFileEntry fileEntry = fileEntryOptional.get(); + assertWithMessage("file entry should be a path to libc.so") + .that(fileEntry.getFullPath()) + .contains("libc.so"); + } + + @Test + public void testFindLoadedByProcessUtilNoMatch() throws Exception { + assertTrue("must test with rootable device", getDevice().enableAdbRoot()); + Optional<IFileEntry> fileEntryOptional = + ProcessUtil.findFileLoadedByProcess( + getDevice(), + "system_server", + Pattern.compile(Pattern.quote("doesnotexist.foobar"))); + assertWithMessage("file entry should be empty if no matches") + .that(fileEntryOptional.isPresent()) + .isFalse(); + } +} diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java new file mode 100644 index 000000000..aa58f1022 --- /dev/null +++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 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.sts.common; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +/** Unit tests for {@link UserUtils}. */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class UserUtilsTest extends BaseHostJUnit4Test { + private static final String TEST_USER_NAME = "TestUserForUserUtils"; + private static final String CMD_PM_LIST_USERS = "pm list users"; + + @Before + public void setUp() throws Exception { + assertWithMessage("device already has the test user") + .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout()) + .doesNotContain(TEST_USER_NAME); + } + + @After + public void tearDown() throws Exception { + assertWithMessage("did not clean up the test user") + .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout()) + .doesNotContain(TEST_USER_NAME); + } + + @Test + public void testUserUtilsNonRoot() throws Exception { + assertTrue(getDevice().disableAdbRoot()); + try (AutoCloseable user = + new UserUtils.SecondaryUser(getDevice()).name(TEST_USER_NAME).withUser()) { + assertFalse( + "device should not implicitly root to create a user", getDevice().isAdbRoot()); + assertWithMessage("did not create the test user") + .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout()) + .contains(TEST_USER_NAME); + } + assertFalse("device should not implicitly root to cleanup", getDevice().isAdbRoot()); + } + + @Test + public void testUserUtilsRoot() throws Exception { + assertTrue("must test with rootable device", getDevice().enableAdbRoot()); + try (AutoCloseable user = + new UserUtils.SecondaryUser(getDevice()).name(TEST_USER_NAME).withUser()) { + assertTrue( + "device should still be root after user creation if started with root", + getDevice().isAdbRoot()); + assertWithMessage("did not create the test user") + .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout()) + .contains(TEST_USER_NAME); + } + assertTrue( + "device should still be root after cleanup if started with root", + getDevice().isAdbRoot()); + } + + @Test + public void testUserUtilsUserRestriction() throws Exception { + assertTrue("must test with rootable device", getDevice().enableAdbRoot()); + try (AutoCloseable user = + new UserUtils.SecondaryUser(getDevice()) + .name(TEST_USER_NAME) + .withUserRestrictions(Map.of("test_restriction", "1")) + .withUser()) { + // Exception is thrown if any error occurs while setting user restriction above + } + assertTrue( + "device should still be root after cleanup if started with root", + getDevice().isAdbRoot()); + } +} diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java new file mode 100644 index 000000000..3a4603978 --- /dev/null +++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 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.sts.common.util; + +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DeviceJUnit4ClassRunner.class) +public class KernelVersionHostTest extends BaseHostJUnit4Test { + + @Test + public final void testGetKernelVersion() throws Exception { + KernelVersionHost.getKernelVersion(getDevice()); + } +} diff --git a/libraries/sts-common-util/sts-sdk/package/README.md b/libraries/sts-common-util/sts-sdk/package/README.md index 663994aa3..41b399a14 100644 --- a/libraries/sts-common-util/sts-sdk/package/README.md +++ b/libraries/sts-common-util/sts-sdk/package/README.md @@ -1,2 +1,2 @@ -See https://source.android.com/docs/security/test/sts-sdK for instructions and +See https://source.android.com/docs/security/test/sts-sdk for instructions and documentation. diff --git a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java index 2e27305ef..fe824cfd4 100644 --- a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java +++ b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java @@ -16,17 +16,30 @@ package android.security.sts; -import static com.android.sts.common.CommandUtil.runAndCheck; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; +import com.android.sts.common.CommandUtil; +import com.android.sts.common.MallocDebug; import com.android.sts.common.NativePoc; +import com.android.sts.common.NativePocCrashAsserter; import com.android.sts.common.NativePocStatusAsserter; +import com.android.sts.common.ProcessUtil; +import com.android.sts.common.RegexUtils; +import com.android.sts.common.SystemUtil; +import com.android.sts.common.UserUtils; import com.android.sts.common.tradefed.testtype.NonRootSecurityTestCase; +import com.android.sts.common.util.TombstoneUtils; +import com.android.tradefed.device.IFileEntry; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Optional; +import java.util.regex.Pattern; + @RunWith(DeviceJUnit4ClassRunner.class) public class StsHostSideTestCase extends NonRootSecurityTestCase { @@ -34,32 +47,126 @@ public class StsHostSideTestCase extends NonRootSecurityTestCase { static final String TEST_PKG = "android.security.sts.sts_test_app_package"; static final String TEST_CLASS = TEST_PKG + "." + "DeviceTest"; + /** An app test, which uses this host Java test to launch an Android instrumented test */ @Test public void testWithApp() throws Exception { - // Note: this test is for CVE-2020-0215 ITestDevice device = getDevice(); - device.enableAdbRoot(); + assertTrue("could not disable root", device.disableAdbRoot()); uninstallPackage(device, TEST_PKG); - runAndCheck(device, "input keyevent KEYCODE_WAKEUP"); - runAndCheck(device, "input keyevent KEYCODE_MENU"); - runAndCheck(device, "input keyevent KEYCODE_HOME"); - installPackage(TEST_APP); runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod"); } + /** + * A native PoC test, which uses this host Java test to push an executable with resources and + * execute with environment variables and more. This API uses a "NativePocAsserter" that handles + * the most common ways to retrieve data from the native PoC. It can be overloaded to handle the + * specific side-effect that your PoC generates. It also demonstrates how to add extra memory + * checking with Malloc Debug. + */ @Test public void testWithNativePoc() throws Exception { NativePoc.builder() + // the name of the PoC .pocName("native-poc") + // extra files pushed to the device .resources("res.txt") + // command-line arguments for the PoC .args("res.txt", "arg2") + // other options allow different linker paths for library shims .useDefaultLdLibraryPath(true) + // test ends with ASSUMPTION_FAILURE if not EXIT_OK .assumePocExitSuccess(true) + // run code after the PoC is executed for cleanup or other .after(r -> getDevice().executeShellV2Command("ls -l /")) - .asserter(NativePocStatusAsserter.assertNotVulnerableExitCode()) // not 113 + // fail if the poc returns exit status 113 + .asserter(NativePocStatusAsserter.assertNotVulnerableExitCode()) + .build() + .run(this); + } + + /** Run native PoCs with Malloc Debug memory checking enabled */ + @Test + public void testWithMallocDebug() throws Exception { + // Set up Malloc Debug for this test, which may be required if the vulnerability needs + // memory checking to crash. This is useful when an ASan/HWASan/MTE build is not available. + // https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md + try (AutoCloseable mallocDebug = + MallocDebug.withLibcMallocDebugOnNewProcess( + getDevice(), + "backtrace guard", // malloc debug options + "native-poc" // process name + )) { + // run a native PoC + NativePoc.builder() + .pocName("native-poc") + .build() // add more as needed + .run(this); + } + } + + /** Run code after applying device settings */ + @Test + public void testWithSetting() throws Exception { + // allow reflection, which is not a security boundary + try (AutoCloseable setting = + SystemUtil.withSetting(getDevice(), "global", "hidden_api_policy", "1")) { + // run app + installPackage(TEST_APP); + runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod"); + } + } + + /** Link a native PoC against a vulnerable system library */ + @Test + public void testWithVulnerableLibrary() throws Exception { + // get the path of the vulnerable library + Optional<IFileEntry> libFileEntry = + ProcessUtil.findFileLoadedByProcess( + getDevice(), "media.metrics", Pattern.quote("libmediametrics.so")); + assumeTrue("shared library not loaded by target process", libFileEntry.isPresent()); + + // attack the service + NativePoc.builder() + .pocName("native-poc") + // pass the library path to the PoC + .args(libFileEntry.get().getFullPath()) + .asserter( + NativePocCrashAsserter.assertNoCrash( + new TombstoneUtils.Config() + // Because the vulnerability is in the shared library, the + // process crash is the PoC. + .setProcessPatterns(Pattern.compile("native-poc")))) .build() .run(this); } + + /** Match a log against a known vulnerable pattern regex */ + @Test + public void testWithLogMessage() throws Exception { + // this is only for dmesg/logcat messages that are not controlled by the test. + + // attack the device, which can be native poc, echo to socket, send intent, app, etc + NativePoc.builder() + .pocName("native-poc") + .build() // add more as needed + .run(this); + + String dmesg = CommandUtil.runAndCheck(getDevice(), "dmesg -c").getStdout(); + + // It's preferred to use this for matching text because the regex has a timeout to + // protect against catastrophic backtracking. It also formats the test assert message. + RegexUtils.assertNotContainsMultiline( + "Call trace:.*?__arm_lpae_unmap.*?kgsl_iommu_unmap", dmesg); + } + + /** Install and run an app as a secondary user */ + @Test + public void testWithSecondaryUser() throws Exception { + try (AutoCloseable su = new UserUtils.SecondaryUser(getDevice()).restricted().withUser()) { + installPackage(TEST_APP); + runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod"); + } + } } diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml index b7f8ac87e..a16eccb9d 100644 --- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml +++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml @@ -32,13 +32,5 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <receiver android:name=".PocReceiver" - android:exported="true"> - <intent-filter> - <action android:name="com.android.nfc.handover.action.ALLOW_CONNECT" /> - <action android:name="com.android.nfc.handover.action.DENY_CONNECT" /> - <action android:name="com.android.nfc.handover.action.TIMEOUT_CONNECT" /> - </intent-filter> - </receiver> </application> </manifest> diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java index da1f7bf47..a218e81f6 100644 --- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java +++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java @@ -18,56 +18,82 @@ package android.security.sts.sts_test_app_package; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assume.assumeNoException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.BroadcastReceiver; import android.content.Context; -import android.content.SharedPreferences; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; -import androidx.annotation.IntegerRes; -import androidx.annotation.StringRes; -import androidx.test.runner.AndroidJUnit4; -import androidx.test.uiautomator.UiDevice; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * An example device test that starts an activity and uses broadcasts to wait for the artifact + * proving vulnerability + */ @RunWith(AndroidJUnit4.class) public class DeviceTest { + private static final String TAG = DeviceTest.class.getSimpleName(); + Context mContext; - Context mAppContext; + /** Test broadcast action */ + public static final String ACTION_BROADCAST = "action_security_test_broadcast"; + /** Broadcast intent extra name for artifacts */ + public static final String INTENT_ARTIFACT = "artifact"; - int getInteger(@IntegerRes int resId) { - return mAppContext.getResources().getInteger(resId); - } + /** Device test */ + @Test + public void testDeviceSideMethod() throws Exception { + mContext = getApplicationContext(); - String getString(@StringRes int resId) { - return mAppContext.getResources().getString(resId); - } + AtomicReference<String> actual = new AtomicReference<>(); + final Semaphore broadcastReceived = new Semaphore(0); + BroadcastReceiver broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + try { + if (!intent.getAction().equals(ACTION_BROADCAST)) { + Log.i( + TAG, + String.format( + "got a broadcast that we didn't expect: %s", + intent.getAction())); + } + actual.set(intent.getStringExtra(INTENT_ARTIFACT)); + broadcastReceived.release(); + } catch (Exception e) { + Log.e(TAG, "got an exception when handling broadcast", e); + } + } + }; + IntentFilter filter = new IntentFilter(); // see if there's a shorthand + filter.addAction(ACTION_BROADCAST); // what does this return? + mContext.registerReceiver(broadcastReceiver, filter); - @Test - public void testDeviceSideMethod() { + // start the target app try { - mAppContext = getApplicationContext(); - UiDevice device = UiDevice.getInstance(getInstrumentation()); - device.executeShellCommand( - "am start -n com.android.nfc/.handover.ConfirmConnectActivity"); - long startTime = System.currentTimeMillis(); - while ((System.currentTimeMillis() - startTime) - < getInteger(R.integer.MAX_WAIT_TIME_MS)) { - SharedPreferences sh = - mAppContext.getSharedPreferences( - getString(R.string.SHARED_PREFERENCE), Context.MODE_APPEND); - int result = - sh.getInt(getString(R.string.RESULT_KEY), getInteger(R.integer.DEFAULT)); - assertNotEquals( - "NFC Android App broadcasts Bluetooth device information!", - result, - getInteger(R.integer.FAIL)); - Thread.sleep(getInteger(R.integer.SLEEP_TIME_MS)); - } - } catch (Exception e) { - assumeNoException(e); + Log.d(TAG, "starting local activity"); + Intent newActivityIntent = new Intent(mContext, PocActivity.class); + newActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // this could be startActivityForResult, but is generic for illustrative purposes + mContext.startActivity(newActivityIntent); + } finally { + getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); } + assertTrue( + "Timed out when getting result from other activity", + broadcastReceived.tryAcquire(/* TIMEOUT_MS */ 5000, TimeUnit.MILLISECONDS)); + assertEquals("The target artifact should have been 'secure'", "secure", actual.get()); } } diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java index 27d682d19..daeb76c8b 100644 --- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java +++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java @@ -17,13 +17,26 @@ package android.security.sts.sts_test_app_package; import android.app.Activity; +import android.content.Intent; import android.os.Bundle; +import android.util.Log; public class PocActivity extends Activity { + private static final String TAG = PocActivity.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + Log.d(TAG, "poc activity started"); + + // Collect the artifact representing vulnerability here. + // Change this to whatever type best fits the vulnerable artifact; consider using a bundle + // if there are multiple artifacts necessary to prove the security vulnerability. + String artifact = "vulnerable"; + + Intent vulnerabilityDescriptionIntent = new Intent(DeviceTest.ACTION_BROADCAST); + vulnerabilityDescriptionIntent.putExtra(DeviceTest.INTENT_ARTIFACT, artifact); + this.sendBroadcast(vulnerabilityDescriptionIntent); } } diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java deleted file mode 100644 index ac879258e..000000000 --- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2022 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.security.sts.sts_test_app_package; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; - -public class PocReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - SharedPreferences sh = - context.getSharedPreferences( - context.getResources().getString(R.string.SHARED_PREFERENCE), - Context.MODE_PRIVATE); - SharedPreferences.Editor edit = sh.edit(); - edit.putInt( - context.getResources().getString(R.string.RESULT_KEY), - context.getResources().getInteger(R.integer.FAIL)); - edit.commit(); - } -} diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml deleted file mode 100644 index acdcd84b6..000000000 --- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2022 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. - --> - -<resources> - <integer name="DEFAULT">0</integer> - <integer name="FAIL">1</integer> - <integer name="SLEEP_TIME_MS">500</integer> - <integer name="MAX_WAIT_TIME_MS">10000</integer> -</resources> diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml deleted file mode 100644 index 286e6fd69..000000000 --- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2022 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. - --> - -<resources> - <string name="RESULT_KEY">result</string> - <string name="SHARED_PREFERENCE">sts_test_app_failure</string> -</resources> diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java new file mode 100644 index 000000000..c9329b969 --- /dev/null +++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2021 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.sts.common.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Tools for parsing kernel version strings */ +public final class KernelVersion implements Comparable<KernelVersion> { + public final int version; + public final int patchLevel; + public final int subLevel; + + public KernelVersion(int version, int patchLevel, int subLevel) { + this.version = version; + this.patchLevel = patchLevel; + this.subLevel = subLevel; + } + + /** + * Parse a kernel version string in the format "version.patchlevel.sublevel" - "5.4.123". + * Trailing values are ignored so `uname -r` can be parsed properly. + * + * @param versionString The version string to parse + */ + public static KernelVersion parse(String versionString) { + Pattern kernelReleasePattern = + Pattern.compile("(?<version>\\d+)\\.(?<patchLevel>\\d+)\\.(?<subLevel>\\d+)(.*)"); + Matcher matcher = kernelReleasePattern.matcher(versionString); + if (matcher.find()) { + return new KernelVersion( + Integer.parseInt(matcher.group("version")), + Integer.parseInt(matcher.group("patchLevel")), + Integer.parseInt(matcher.group("subLevel"))); + } + throw new IllegalArgumentException( + String.format("Could not parse kernel version string (%s)", versionString)); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + // 2147483647 (INT_MAX) + // vvppppssss + return version * 10000000 + patchLevel * 10000 + subLevel; + } + + /** Compare by version, patchlevel, and sublevel in that order. */ + public int compareTo(KernelVersion o) { + if (version != o.version) { + return Integer.compare(version, o.version); + } + if (patchLevel != o.patchLevel) { + return Integer.compare(patchLevel, o.patchLevel); + } + return Integer.compare(subLevel, o.subLevel); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (o instanceof KernelVersion) { + return this.compareTo((KernelVersion) o) == 0; + } + return false; + } + + /** Format as "version.patchlevel.sublevel" */ + @Override + public String toString() { + return String.format("%d.%d.%d", version, patchLevel, subLevel); + } + + /** Format as "version.patchlevel" */ + public String toStringShort() { + return String.format("%d.%d", version, patchLevel); + } +} diff --git a/tests/automotive/health/rules/src/android/platform/scenario/ColdAppStartupRunRule.java b/tests/automotive/health/rules/src/android/platform/scenario/ColdAppStartupRunRule.java new file mode 100644 index 000000000..5f227f012 --- /dev/null +++ b/tests/automotive/health/rules/src/android/platform/scenario/ColdAppStartupRunRule.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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.platform.test.scenario; + +import android.platform.helpers.IAppHelper; +import android.platform.test.rule.DropCachesRule; +import android.platform.test.rule.KillAppsRule; + +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class ColdAppStartupRunRule<T extends IAppHelper> implements TestRule { + private final RuleChain mRuleChain; + + public ColdAppStartupRunRule(T appHelper) { + mRuleChain = + RuleChain.outerRule(new KillAppsRule(appHelper.getPackage())) + .around(new DropCachesRule()) + .around(new SleepAtTestStartRule(3000)) + .around(new SleepAtTestFinishRule(3000)); + } + + public Statement apply(final Statement base, final Description description) { + return mRuleChain.apply(base, description); + } +} diff --git a/tests/automotive/health/rules/src/android/platform/scenario/HotAppStartupRunRule.java b/tests/automotive/health/rules/src/android/platform/scenario/HotAppStartupRunRule.java new file mode 100644 index 000000000..273a3b28c --- /dev/null +++ b/tests/automotive/health/rules/src/android/platform/scenario/HotAppStartupRunRule.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 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.platform.test.scenario; + +import android.platform.helpers.IAppHelper; +import android.platform.test.rule.TestWatcher; +import android.util.Log; + +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class HotAppStartupRunRule<T extends IAppHelper> implements TestRule { + private final RuleChain mRuleChain; + + public HotAppStartupRunRule(T appHelper) { + mRuleChain = + RuleChain.outerRule(new SwitchOutAppRule()) + .around(new SleepAtTestStartRule(2000)) + .around(new SleepAtTestFinishRule(3000)); + } + + public Statement apply(final Statement base, final Description description) { + return mRuleChain.apply(base, description); + } + + // Custom rule to move away from app under test + private static class SwitchOutAppRule extends TestWatcher { + private static final String GO_HOME_PARAM_NAME = "go-home"; + private static final String GO_HOME_DEFAULT = "true"; + private static final String LAUNCHER_PARAM_NAME = "app-package"; + private static final String CARLAUNCHER_PACKAGE = "com.android.car.carlauncher"; + private static final String APP_ACTIVITY_PARAM_NAME = "app-activity"; + private static final String APP_GRID_ACTIVITY = + "com.android.car.carlauncher.AppGridActivity"; + private static final String LOG_TAG = SwitchOutAppRule.class.getSimpleName(); + + private boolean mGoHome; + private String mAppPackage; + private String mAppActivity; + + @Override + protected void starting(Description description) { + mGoHome = + Boolean.parseBoolean( + getArguments().getString(GO_HOME_PARAM_NAME, GO_HOME_DEFAULT)); + mAppPackage = getArguments().getString(LAUNCHER_PARAM_NAME, CARLAUNCHER_PACKAGE); + mAppActivity = getArguments().getString(APP_ACTIVITY_PARAM_NAME, APP_GRID_ACTIVITY); + + // Default behavior is to press home + if (mGoHome) { + Log.v(LOG_TAG, "Pressing home"); + getUiDevice().pressHome(); + } else { + Log.i(LOG_TAG, String.format("Starting %s/%s", mAppPackage, mAppActivity)); + String openAppGridCommand = + String.format("am start -n %s/%s", mAppPackage, mAppActivity); + executeShellCommand(openAppGridCommand); + } + } + } +} diff --git a/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestStartRule.java b/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestStartRule.java new file mode 100644 index 000000000..8d8f8c540 --- /dev/null +++ b/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestStartRule.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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.platform.test.scenario; + +import android.os.SystemClock; +import android.platform.test.rule.TestWatcher; +import android.util.Log; + +import org.junit.runner.Description; + +/** This rule will sleep for a given amount of time at the start of each test method. */ +public class SleepAtTestStartRule extends TestWatcher { + private static final String LOG_TAG = SleepAtTestStartRule.class.getSimpleName(); + + private final long mMillis; + + public SleepAtTestStartRule(long millis) { + mMillis = millis; + } + + @Override + protected void starting(Description description) { + Log.v(LOG_TAG, String.format("Sleeping for %d ms", mMillis)); + SystemClock.sleep(mMillis); + Log.v(LOG_TAG, String.format("Done sleeping for %d ms", mMillis)); + } +} diff --git a/utils/shell-as/Android.bp b/utils/shell-as/Android.bp new file mode 100644 index 000000000..96dc1c9d5 --- /dev/null +++ b/utils/shell-as/Android.bp @@ -0,0 +1,107 @@ +// Copyright (C) 2023 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. + +cc_binary { + name: "shell-as", + cflags: [ + "-Wall", + "-Werror", + "-Wextra", + ], + srcs: [ + "*.cpp", + ":shell-as-test-app-apk-cpp", + ], + header_libs: ["libcutils_headers"], + static_executable: true, + static_libs: [ + "libbase", + "libcap", + "liblog", + "libseccomp_policy", + "libselinux", + ], + arch: { + arm: { + srcs: ["shell-code/*-arm.S"] + }, + arm64: { + srcs: ["shell-code/*-arm64.S"] + }, + x86: { + srcs: ["shell-code/*-x86.S"] + }, + x86_64: { + srcs: ["shell-code/*-x86_64.S"] + } + } +} + +// A simple app that requests all non-system permissions and contains no other +// functionality. This can be used as a target for shell-as to emulate the +// security context of the most privileged possible non-system app. +android_app { + name: "shell-as-test-app", + manifest: ":shell-as-test-app-manifest", + srcs: ["app/**/*.java"], + sdk_version: "9", + certificate: ":shell-as-test-app-cert", +} + +// https://source.android.com/docs/core/ota/sign_builds#release-keys +// Generated by running: +// $ANDROID_BUILD_TOP/development/tools/make_key \ +// shell-as-test-app-key \ +// '/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com +android_app_certificate { + name: "shell-as-test-app-cert", + certificate: "shell-as-test-app-key", +} + +genrule { + name: "shell-as-test-app-manifest", + srcs: [ + ":permission-list-normal", + "AndroidManifest.xml.template" + ], + cmd: "$(location gen-manifest.sh) " + + "$(location AndroidManifest.xml.template) " + + "$(location :permission-list-normal) " + + "$(out)", + out: ["AndroidManifest.xml"], + tool_files: ["gen-manifest.sh"], +} + +// A source file that contains the contents of the above shell-as-test-app APK +// embedded as an array. +cc_genrule { + name: "shell-as-test-app-apk-cpp", + srcs: [":shell-as-test-app"], + cmd: "(" + + " echo '#include <stddef.h>';" + + " echo '#include <stdint.h>';" + + " echo '';" + + " echo 'namespace shell_as {';" + + " echo 'const uint8_t kTestAppApk[] = {';" + + " $(location toybox) xxd -i < $(in);" + + " echo '};';" + + " echo 'void GetTestApk(uint8_t **apk, size_t *length) {';" + + " echo ' *apk = (uint8_t*) kTestAppApk;';" + + " echo ' *length = sizeof(kTestAppApk);';" + + " echo '}';" + + " echo '} // namespace shell_as';" + + ") > $(out)", + out: ["test-app-apk.cpp"], + tools: ["toybox"] +} diff --git a/utils/shell-as/AndroidManifest.xml.template b/utils/shell-as/AndroidManifest.xml.template new file mode 100644 index 000000000..07e89b186 --- /dev/null +++ b/utils/shell-as/AndroidManifest.xml.template @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2023 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.google.tools.security.shell_as"> + + PERMISSIONS + + <application + android:allowBackup="true" + android:label="Shell-As Test App"> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/utils/shell-as/OWNERS b/utils/shell-as/OWNERS new file mode 100644 index 000000000..431db9920 --- /dev/null +++ b/utils/shell-as/OWNERS @@ -0,0 +1,4 @@ +# Code owners for shell-as + +willcoster@google.com +cdombroski@google.com diff --git a/utils/shell-as/README.md b/utils/shell-as/README.md new file mode 100644 index 000000000..e0f6f93f2 --- /dev/null +++ b/utils/shell-as/README.md @@ -0,0 +1,33 @@ +# shell-as + +shell-as is a utility that can be used to execute a binary in a less privileged +security context. This can be useful for verifying the capabilities of a process +on a running device or testing PoCs with different privilege levels. + +## Usage + +The security context can either be supplied explicitly, inferred from a process +running on the device, or set to a predefined profile. + +For example, the following are equivalent and execute `/system/bin/id` in the +context of the init process. + +```shell +shell-as \ + --uid 0 \ + --gid 0 \ + --selinux u:r:init:s0 \ + --seccomp system \ + /system/bin/id +``` + +```shell +shell-as --pid 1 /system/bin/id +``` + +The "untrusted-app" profile can be used to execute a binary with all the +possible privileges attainable by an untrusted app: + +```shell +shell-as --profile untrusted-app /system/bin/id +``` diff --git a/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java b/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java new file mode 100644 index 000000000..d5d178c2f --- /dev/null +++ b/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 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.google.tools.security.shell_as; + +import android.app.Activity; +import android.os.Bundle; + +/** An empty activity for the shell-as test app. */ +public class MainActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } +} diff --git a/utils/shell-as/command-line.cpp b/utils/shell-as/command-line.cpp new file mode 100644 index 000000000..9a893c375 --- /dev/null +++ b/utils/shell-as/command-line.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./command-line.h" + +#include <getopt.h> + +#include <iostream> +#include <string> + +#include "./context.h" +#include "./string-utils.h" + +namespace shell_as { + +namespace { +const std::string kUsage = + R"(Usage: shell-as [options] [<program> <arguments>...] + +shell-as executes a program in a specified Android security context. The default +program that is executed if none is specified is `/bin/system/sh`. + +The following options can be used to define the target security context. + +--verbose, -v Enables verbose logging. +--uid <uid>, -u <uid> The target real and effective user ID. +--gid <gid>, -g <gid> The target real and effective group ID. +--groups <gid1,2,..>, -G <1,2,..> A comma separated list of supplementary group + IDs. +--nogroups Specifies that all supplementary groups should + be cleared. +--selinux <context>, -s <context> The target SELinux context. +--seccomp <filter>, -f <filter> The target seccomp filter. Valid values of + filter are 'none', 'uid-inferred', 'app', + 'app-zygote', and 'system'. +--caps <capabilities> A libcap textual expression that describes + the desired capability sets. The only + capability set that matters is the permitted + set, the other sets are ignored. + + Examples: + + "=" - Clear all capabilities + "=p" - Raise all capabilities + "23,CAP_SYS_ADMIN+p" - Raise CAP_SYS_ADMIN + and capability 23. + + For a full description of the possible values + see `man 3 cap_from_text` (the libcap-dev + package provides this man page). +--pid <pid>, -p <pid> Infer the target security context from a + running process with the given process ID. + This option implies --seccomp uid_inferred. + This option infers the capability from the + target process's permitted capability set. +--profile <profile>, -P <profile> Infer the target security context from a + predefined security profile. Using this + option will install and execute a test app on + the device. Currently, the only valid profile + is 'untrusted-app' which corresponds to an + untrusted app which has been granted every + non-system permission. + +Options are evaluated in the order that they are given. For example, the +following will set the target context to that of process 1234 but override the +user ID to 0: + + shell-as --pid 1234 --uid 0 +)"; + +const char* kShellExecvArgs[] = {"/system/bin/sh", nullptr}; + +bool ParseGroups(char* line, std::vector<gid_t>* ids) { + // Allow a null line as a valid input since this method is used to handle both + // --groups and --nogroups. + if (line == nullptr) { + return true; + } + return SplitIdsAndSkip(line, ",", /*num_to_skip=*/0, ids); +} +} // namespace + +bool ParseOptions(const int argc, char* const argv[], bool* verbose, + SecurityContext* context, char* const* execv_args[]) { + char short_options[] = "+s:hp:u:g:G:f:c:vP:"; + struct option long_options[] = { + {"selinux", true, nullptr, 's'}, {"help", false, nullptr, 'h'}, + {"uid", true, nullptr, 'u'}, {"gid", true, nullptr, 'g'}, + {"pid", true, nullptr, 'p'}, {"verbose", false, nullptr, 'v'}, + {"groups", true, nullptr, 'G'}, {"nogroups", false, nullptr, 'G'}, + {"seccomp", true, nullptr, 'f'}, {"caps", true, nullptr, 'c'}, + {"profile", true, nullptr, 'P'}, + }; + int option; + bool infer_seccomp_filter = false; + SecurityContext working_context; + std::vector<gid_t> supplementary_group_ids; + uint32_t working_id = 0; + while ((option = getopt_long(argc, argv, short_options, long_options, + nullptr)) != -1) { + switch (option) { + case 'v': + *verbose = true; + break; + case 'h': + std::cerr << kUsage; + return false; + case 'u': + if (!StringToUInt32(optarg, &working_id)) { + return false; + } + working_context.user_id = working_id; + break; + case 'g': + if (!StringToUInt32(optarg, &working_id)) { + return false; + } + working_context.group_id = working_id; + break; + case 'c': + working_context.capabilities = cap_from_text(optarg); + if (working_context.capabilities.value() == nullptr) { + std::cerr << "Unable to parse capabilities" << std::endl; + return false; + } + break; + case 'G': + supplementary_group_ids.clear(); + if (!ParseGroups(optarg, &supplementary_group_ids)) { + std::cerr << "Unable to parse supplementary groups" << std::endl; + return false; + } + working_context.supplementary_group_ids = supplementary_group_ids; + break; + case 's': + working_context.selinux_context = optarg; + break; + case 'f': + infer_seccomp_filter = false; + if (strcmp(optarg, "uid-inferred") == 0) { + infer_seccomp_filter = true; + } else if (strcmp(optarg, "app") == 0) { + working_context.seccomp_filter = kAppFilter; + } else if (strcmp(optarg, "app-zygote") == 0) { + working_context.seccomp_filter = kAppZygoteFilter; + } else if (strcmp(optarg, "system") == 0) { + working_context.seccomp_filter = kSystemFilter; + } else if (strcmp(optarg, "none") == 0) { + working_context.seccomp_filter.reset(); + } else { + std::cerr << "Invalid value for --seccomp: " << optarg << std::endl; + return false; + } + break; + case 'p': + if (!SecurityContextFromProcess(atoi(optarg), &working_context)) { + return false; + } + infer_seccomp_filter = true; + break; + case 'P': + if (strcmp(optarg, "untrusted-app") == 0) { + if (!SecurityContextFromTestApp(&working_context)) { + return false; + } + } else { + std::cerr << "Invalid value for --profile: " << optarg << std::endl; + return false; + } + infer_seccomp_filter = true; + break; + default: + std::cerr << "Unknown option '" << (char)optopt << "'" << std::endl; + return false; + } + } + + if (infer_seccomp_filter) { + if (!working_context.user_id.has_value()) { + std::cerr << "No user ID; unable to infer appropriate seccomp filter." + << std::endl; + return false; + } + working_context.seccomp_filter = + SeccompFilterFromUserId(working_context.user_id.value()); + } + + *context = working_context; + if (optind < argc) { + *execv_args = argv + optind; + } else { + *execv_args = (char**)kShellExecvArgs; + } + return true; +} + +} // namespace shell_as diff --git a/utils/shell-as/command-line.h b/utils/shell-as/command-line.h new file mode 100644 index 000000000..4bf495f42 --- /dev/null +++ b/utils/shell-as/command-line.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef SHELL_AS_COMMAND_LINE_H_ +#define SHELL_AS_COMMAND_LINE_H_ + +#include "./context.h" + +namespace shell_as { + +// Parse command line options into a target security context and arguments that +// can be passed to ExecuteInContext. +// +// The value of execv_args will either point to a sub-array of argv or to a +// statically allocated default value. In both cases the caller should /not/ +// free the memory. +// +// Returns true on success and false if there is a problem parsing options. +bool ParseOptions(const int argc, char* const argv[], bool* verbose, + SecurityContext* context, char* const* execv_args[]); +} // namespace shell_as + +#endif // SHELL_AS_COMMAND_LINE_H_ diff --git a/utils/shell-as/context.cpp b/utils/shell-as/context.cpp new file mode 100644 index 000000000..ea7979bab --- /dev/null +++ b/utils/shell-as/context.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./context.h" + +#include <private/android_filesystem_config.h> // For AID_APP_START. +#include <stdio.h> +#include <stdlib.h> + +#include <iostream> +#include <string> + +#include "./string-utils.h" +#include "./test-app.h" + +namespace shell_as { + +namespace { + +bool ParseIdFromProcStatusLine(char* line, uid_t* id) { + // The user and group ID lines of the status file look like: + // + // Uid: <real> <effective> <saved> <filesystem> + // Gid: <real> <effective> <saved> <filesystem> + std::vector<uid_t> ids; + if (!SplitIdsAndSkip(line, "\t\n ", /*num_to_skip=*/1, &ids) || + ids.size() < 1) { + return false; + } + *id = ids[0]; + return true; +} + +bool ParseGroupsFromProcStatusLine(char* line, std::vector<gid_t>* ids) { + // The supplementary groups line of the status file looks like: + // + // Groups: <group1> <group2> <group3> ... + return SplitIdsAndSkip(line, "\t\n ", /*num_to_skip=*/1, ids); +} + +bool ParseProcStatusFile(const pid_t process_id, uid_t* real_user_id, + gid_t* real_group_id, + std::vector<gid_t>* supplementary_group_ids) { + std::string proc_status_path = + std::string("/proc/") + std::to_string(process_id) + "/status"; + FILE* status_file = fopen(proc_status_path.c_str(), "r"); + if (status_file == nullptr) { + std::cerr << "Unable to open '" << proc_status_path << "'" << std::endl; + } + bool parsed_user = false; + bool parsed_group = false; + bool parsed_supplementary_groups = false; + while (true) { + size_t line_length = 0; + char* line = nullptr; + if (getline(&line, &line_length, status_file) < 0) { + free(line); + break; + } + if (strncmp("Uid:", line, 4) == 0) { + parsed_user = ParseIdFromProcStatusLine(line, real_user_id); + } else if (strncmp("Gid:", line, 4) == 0) { + parsed_group = ParseIdFromProcStatusLine(line, real_group_id); + } else if (strncmp("Groups:", line, 7) == 0) { + parsed_supplementary_groups = + ParseGroupsFromProcStatusLine(line, supplementary_group_ids); + } + free(line); + } + fclose(status_file); + return parsed_user && parsed_group && parsed_supplementary_groups; +} + +} // namespace + +bool SecurityContextFromProcess(const pid_t process_id, + SecurityContext* context) { + char* selinux_context; + if (getpidcon(process_id, &selinux_context) != 0) { + std::cerr << "Unable to obtain SELinux context from process " << process_id + << std::endl; + return false; + } + + cap_t capabilities = cap_get_pid(process_id); + if (capabilities == nullptr) { + std::cerr << "Unable to obtain capability set from process " << process_id + << std::endl; + return false; + } + + uid_t user_id = 0; + gid_t group_id = 0; + std::vector<gid_t> supplementary_group_ids; + if (!ParseProcStatusFile(process_id, &user_id, &group_id, + &supplementary_group_ids)) { + std::cerr << "Unable to obtain user and group IDs from process " + << process_id << std::endl; + return false; + } + + context->selinux_context = selinux_context; + context->user_id = user_id; + context->group_id = group_id; + context->supplementary_group_ids = supplementary_group_ids; + context->capabilities = capabilities; + return true; +} + +bool SecurityContextFromTestApp(SecurityContext* context) { + pid_t test_app_pid = 0; + if (!SetupAndStartTestApp(&test_app_pid)) { + std::cerr << "Unable to install test app." << std::endl; + return false; + } + return SecurityContextFromProcess(test_app_pid, context); +} + +SeccompFilter SeccompFilterFromUserId(uid_t user_id) { + // Copied from: + // frameworks/base/core/jni/com_android_internal_os_Zygote.cpp + return user_id >= AID_APP_START ? kAppFilter : kSystemFilter; +} + +} // namespace shell_as diff --git a/utils/shell-as/context.h b/utils/shell-as/context.h new file mode 100644 index 000000000..17a8cca85 --- /dev/null +++ b/utils/shell-as/context.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef SHELL_AS_CONTEXT_H_ +#define SHELL_AS_CONTEXT_H_ + +#include <selinux/selinux.h> +#include <sys/capability.h> + +#include <memory> +#include <optional> +#include <vector> + +namespace shell_as { + +// Enumeration of the possible seccomp filters that Android may apply to a +// process. +// +// This should be kept in sync with the policies defined in: +// bionic/libc/seccomp/include/seccomp_policy.h +enum SeccompFilter { + kAppFilter = 0, + kAppZygoteFilter = 1, + kSystemFilter = 2, +}; + +typedef struct SecurityContext { + std::optional<uid_t> user_id; + std::optional<gid_t> group_id; + std::optional<std::vector<gid_t>> supplementary_group_ids; + std::optional<char *> selinux_context; + std::optional<SeccompFilter> seccomp_filter; + std::optional<cap_t> capabilities; +} SecurityContext; + +// Infers the appropriate seccomp filter from a user ID. +// +// This mimics the behavior of the zygote process and provides a sane default +// method of picking a filter. However, it is not 100% accurate since it does +// not assign the app zygote filter and would not return an appropriate value +// for processes not started by the zygote. +SeccompFilter SeccompFilterFromUserId(uid_t user_id); + +// Derives a complete security context from a given process. +// +// If unable to determine any field of the context this method will return false +// and not modify the given context. +bool SecurityContextFromProcess(pid_t process_id, SecurityContext* context); + +// Derives a complete security context from the bundled test app. +// +// If unable to determine any field of the context this method will return false +// and not modify the given context. +bool SecurityContextFromTestApp(SecurityContext* context); + +} // namespace shell_as + +#endif // SHELL_AS_CONTEXT_H_ diff --git a/utils/shell-as/elf-utils.cpp b/utils/shell-as/elf-utils.cpp new file mode 100644 index 000000000..8a82555be --- /dev/null +++ b/utils/shell-as/elf-utils.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 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. + */ + +#include <elf.h> +#include <stdio.h> + +#include <iostream> +#include <string> + +#include "./elf.h" + +namespace shell_as { + +namespace { +// The base address of a PIE binary when loaded with ASLR disabled. +#if defined(__arm__) || defined(__aarch64__) +constexpr uint64_t k32BitImageBase = 0xAAAAA000; +constexpr uint64_t k64BitImageBase = 0x5555555000; +#else +constexpr uint64_t k32BitImageBase = 0x56555000; +constexpr uint64_t k64BitImageBase = 0x555555554000; +#endif +} // namespace + +bool GetElfEntryPoint(const pid_t process_id, uint64_t* entry_address, + bool* is_arm_mode) { + uint8_t elf_header_buffer[sizeof(Elf64_Ehdr)]; + std::string exe_path = "/proc/" + std::to_string(process_id) + "/exe"; + FILE* exe_file = fopen(exe_path.c_str(), "rb"); + if (exe_file == nullptr) { + std::cerr << "Unable to open executable of process " << process_id + << std::endl; + return false; + } + + int read_size = + fread(elf_header_buffer, sizeof(elf_header_buffer), 1, exe_file); + fclose(exe_file); + if (read_size <= 0) { + std::cerr << "Unable to read executable of process " << process_id + << std::endl; + return false; + } + + const Elf32_Ehdr* file_header_32 = (Elf32_Ehdr*)elf_header_buffer; + const Elf64_Ehdr* file_header_64 = (Elf64_Ehdr*)elf_header_buffer; + // The first handful of bytes of a header do not depend on whether the file is + // 32bit vs 64bit. + const bool is_pie_binary = file_header_32->e_type == ET_DYN; + + if (file_header_32->e_ident[EI_CLASS] == ELFCLASS32) { + *entry_address = + file_header_32->e_entry + (is_pie_binary ? k32BitImageBase : 0); + } else if (file_header_32->e_ident[EI_CLASS] == ELFCLASS64) { + *entry_address = + file_header_64->e_entry + (is_pie_binary ? k64BitImageBase : 0); + } else { + return false; + } + + *is_arm_mode = false; +#if defined(__arm__) + if ((*entry_address & 1) == 0) { + *is_arm_mode = true; + } + // The entry address for ARM Elf binaries is branched to using a BX + // instruction. The low bit of these instructions indicates the instruction + // set of the code that is being jumped to. A low bit of 1 indicates thumb + // mode while a low bit of 0 indicates ARM mode. + *entry_address &= ~1; +#endif + + return true; +} + +} // namespace shell_as diff --git a/utils/shell-as/elf-utils.h b/utils/shell-as/elf-utils.h new file mode 100644 index 000000000..eba40f303 --- /dev/null +++ b/utils/shell-as/elf-utils.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef SHELL_AS_ELF_H_ +#define SHELL_AS_ELF_H_ + +#include <sys/types.h> + +namespace shell_as { + +// Sets entry_address to the process's entry point. +// +// This method assumes that PIE binaries are executing with ADDR_NO_RANDOMIZE. +// +// The is_arm_mode flag is set to true IFF the architecture is 32bit ARM and the +// expected instruction set for code located at the entry address is not-thumb. +// It is false for all other cases. +bool GetElfEntryPoint(const pid_t process_id, uint64_t* entry_address, + bool* is_arm_mode); +} // namespace shell_as + +#endif // SHELL_AS_ELF_H_ diff --git a/utils/shell-as/execute.cpp b/utils/shell-as/execute.cpp new file mode 100644 index 000000000..3ef529252 --- /dev/null +++ b/utils/shell-as/execute.cpp @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./execute.h" + +#include <linux/securebits.h> +#include <linux/uio.h> +#include <seccomp_policy.h> +#include <sys/capability.h> +#include <sys/personality.h> +#include <sys/prctl.h> +#include <sys/ptrace.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <iostream> +#include <memory> + +#include "./elf-utils.h" +#include "./registers.h" +#include "./shell-code.h" + +namespace shell_as { + +namespace { + +// Capabilities are implemented as a 64-bit bit-vector. Therefore the maximum +// number of capabilities supported by a kernel is 64. +constexpr cap_value_t kMaxCapabilities = 64; + +bool DropPreExecPrivileges(const shell_as::SecurityContext* context) { + // The ordering here is important: + // (1) The platform's seccomp filters disallow setresgiud, so it must come + // before the seccomp drop. + // (2) Adding seccomp filters must happen before setresuid because setresuid + // drops some capabilities which are required for seccomp. + if (context->group_id.has_value() && + setresgid(context->group_id.value(), context->group_id.value(), + context->group_id.value()) != 0) { + std::cerr << "Unable to set group id: " << context->group_id.value() + << std::endl; + return false; + } + if (context->supplementary_group_ids.has_value() && + setgroups(context->supplementary_group_ids.value().size(), + context->supplementary_group_ids.value().data()) != 0) { + std::cerr << "Unable to set supplementary groups." << std::endl; + return false; + } + + if (context->seccomp_filter.has_value()) { + switch (context->seccomp_filter.value()) { + case shell_as::kAppFilter: + set_app_seccomp_filter(); + break; + case shell_as::kAppZygoteFilter: + set_app_zygote_seccomp_filter(); + break; + case shell_as::kSystemFilter: + set_system_seccomp_filter(); + break; + } + } + + // This must be set prior to setresuid, otherwise that call will drop the + // permitted set of capabilities. + if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) != 0) { + std::cerr << "Unable to set keep capabilities." << std::endl; + return false; + } + + if (context->user_id.has_value() && + setresuid(context->user_id.value(), context->user_id.value(), + context->user_id.value()) != 0) { + std::cerr << "Unable to set user id: " << context->user_id.value() + << std::endl; + return false; + } + + // Capabilities must be reacquired after setresuid since it still modifies + // capabilities, but it leaves the permitted set intact. + if (context->capabilities.has_value()) { + // The first step is to raise all the capabilities possible in all sets + // including the inheritable set. This defines the superset of possible + // capabilities that can be passed on after calling execve. + // + // The reason that all capabilities are raised in the inheritable set is due + // to a limitation of libcap. libcap may not contain a capability definition + // for all capabilities supported by the kernel. If this occurs, it will + // silently ignore requests to raise unknown capabilities via cap_set_flag. + // + // However, when parsing a cap_t from a text value, libcap will treat "all" + // as all possible 64 capability bits as set. + cap_t all_capabilities = cap_from_text("all+pie"); + if (cap_set_proc(all_capabilities) != 0) { + std::cerr << "Unable to raise inheritable capability set." << std::endl; + cap_free(all_capabilities); + return false; + } + cap_free(all_capabilities); + + // The second step is to raise the /desired/ capability subset in the + // ambient capability set. These are the capabilities that will actually be + // passed to the process after execve. + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) != 0) { + std::cerr << "Unable to clear ambient capabilities." << std::endl; + return false; + } + cap_t desired_capabilities = context->capabilities.value(); + for (cap_value_t cap = 0; cap < kMaxCapabilities; cap++) { + // Skip capability values not supported by the kernel. + if (!CAP_IS_SUPPORTED(cap)) { + continue; + } + cap_flag_value_t value = CAP_CLEAR; + if (cap_get_flag(desired_capabilities, cap, CAP_PERMITTED, &value) == 0 && + value == CAP_SET) { + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0) != 0) { + std::cerr << "Unable to raise capability " << cap + << " in the ambient set." << std::endl; + return false; + } + } + } + + // The final step is to raise the SECBIT_NOROOT flag. The kernel has special + // case logic that treats root calling execve differently than other users. + // + // By default all bits in the permitted set prior to calling execve will be + // raised after calling execve. This would ignore the work above and result + // in the process to have all capabilities. + // + // Setting the SECBIT_NOROOT disables this special casing for root and + // causes the kernel to treat it as any other UID. + int64_t secure_bits = prctl(PR_GET_SECUREBITS, 0, 0, 0, 0); + if (secure_bits < 0 || + prctl(PR_SET_SECUREBITS, secure_bits | SECBIT_NOROOT, 0, 0, 0) != 0) { + std::cerr << "Unable to raise SECBIT_NOROOT." << std::endl; + return false; + } + } + return true; +} + +uint8_t ReadChildByte(const pid_t process, const uintptr_t address) { + uintptr_t data = ptrace(PTRACE_PEEKDATA, process, address, nullptr); + return ((uint8_t*)&data)[0]; +} + +void WriteChildByte(const pid_t process, const uintptr_t address, + const uint8_t value) { + // This is not the most efficient way to write data to a process. However, it + // reduces code complexity of handling different word sizes and reading and + // writing memory that is not a multiple of the native word size. + uintptr_t data = ptrace(PTRACE_PEEKDATA, process, address, nullptr); + ((uint8_t*)&data)[0] = value; + ptrace(PTRACE_POKEDATA, process, address, data); +} + +void ReadChildMemory(const pid_t process, uintptr_t process_address, + uint8_t* bytes, size_t byte_count) { + for (; byte_count != 0; byte_count--, bytes++, process_address++) { + *bytes = ReadChildByte(process, process_address); + } +} + +void WriteChildMemory(const pid_t process, uintptr_t process_address, + uint8_t const* bytes, size_t byte_count) { + for (; byte_count != 0; byte_count--, bytes++, process_address++) { + WriteChildByte(process, process_address, *bytes); + } +} + +// Executes shell code in a target process. +// +// The following assumptions are made: +// * The process is currently being ptraced and that the process has already +// stopped. +// * The shell code will raise SIGSTOP when it has finished as signal that +// control flow should be handed back to the original code. +// * The shell code only alters registers and pushes values onto the stack. +// +// Execution is performed by overwriting the memory under the current +// instruction pointer with the shell code. After the shell code signals +// completion the original register state and memory are restored. +// +// If the above assumptions are met, then this function will leave the process +// in a stopped state that is equivalent to the original state. +bool ExecuteShellCode(const pid_t process, const uint8_t* shell_code, + const size_t shell_code_size) { + REGISTER_STRUCT registers; + struct iovec registers_iovec; + registers_iovec.iov_base = ®isters; + registers_iovec.iov_len = sizeof(REGISTER_STRUCT); + ptrace(PTRACE_GETREGSET, process, 1, ®isters_iovec); + + std::unique_ptr<uint8_t[]> memory_backup(new uint8_t[shell_code_size]); + ReadChildMemory(process, PROGRAM_COUNTER(registers), memory_backup.get(), + shell_code_size); + WriteChildMemory(process, PROGRAM_COUNTER(registers), shell_code, + shell_code_size); + + // Execute the shell code and wait for the signal that it has finished. + ptrace(PTRACE_CONT, process, NULL, NULL); + int status; + waitpid(process, &status, 0); + if (status >> 8 != SIGSTOP) { + std::cerr << "Failed to execute SELinux shellcode." << std::endl; + return false; + } + + ptrace(PTRACE_SETREGSET, process, 1, ®isters_iovec); + WriteChildMemory(process, PROGRAM_COUNTER(registers), memory_backup.get(), + shell_code_size); + return true; +} + +bool SetProgramCounter(const pid_t process_id, uint64_t program_counter) { + REGISTER_STRUCT registers; + struct iovec registers_iovec; + registers_iovec.iov_base = ®isters; + registers_iovec.iov_len = sizeof(REGISTER_STRUCT); + if (ptrace(PTRACE_GETREGSET, process_id, 1, ®isters_iovec) != 0) { + return false; + } + PROGRAM_COUNTER(registers) = program_counter; + if ((ptrace(PTRACE_SETREGSET, process_id, 1, ®isters_iovec)) != 0) { + return false; + } + return true; +} + +bool StepToEntryPoint(const pid_t process_id) { + bool is_arm_mode; + uint64_t entry_address; + if (!GetElfEntryPoint(process_id, &entry_address, &is_arm_mode)) { + std::cerr << "Not able to determine Elf entry point." << std::endl; + return false; + } + if (is_arm_mode) { + // TODO(willcoster): If there is a need to handle ARM mode instructions in + // addition to thumb instructions update this with ARM mode shell code. + std::cerr << "Attempting to run an ARM-mode binary. " + << "shell-as currently only supports thumb-mode. " + << "Bug willcoster@ if you run into this error." << std::endl; + return false; + } + + int expected_signal = 0; + size_t trap_code_size = 0; + std::unique_ptr<uint8_t[]> trap_code = + GetTrapShellCode(&expected_signal, &trap_code_size); + std::unique_ptr<uint8_t[]> backup(new uint8_t[trap_code_size]); + + // Set a break point at the entry point declared by the Elf file. When a + // statically linked binary is executed this will be the first instruction + // executed. + // + // When a dynamically linked binary is executed, the dynamic linker is + // executed first. This brings .so files into memory and resolves shared + // symbols. Once this process is finished, it jumps to the entry point + // declared in the Elf file. + ReadChildMemory(process_id, entry_address, backup.get(), trap_code_size); + WriteChildMemory(process_id, entry_address, trap_code.get(), trap_code_size); + ptrace(PTRACE_CONT, process_id, NULL, NULL); + int status; + waitpid(process_id, &status, 0); + if (status >> 8 != expected_signal) { + std::cerr << "Program exited unexpectedly while stepping to entry point." + << std::endl; + std::cerr << "Expected status " << expected_signal << " but encountered " + << (status >> 8) << std::endl; + return false; + } + + if (!SetProgramCounter(process_id, entry_address)) { + return false; + } + WriteChildMemory(process_id, entry_address, backup.get(), trap_code_size); + return true; +} + +} // namespace + +bool ExecuteInContext(char* const executable_and_args[], + const shell_as::SecurityContext* context) { + // Getting an executable running in a lower privileged context is tricky with + // SELinux. The recommended approach in the documentation is to use setexeccon + // which sets the context on the next execve call. + // + // However, this doesn't work for unprivileged processes like untrusted apps + // in Android because they are not allowed to execute most binaries. + // + // To work around this, ptrace is used to inject shell code into the new + // process just after it has executed an execve syscall. This shell code then + // sets the desired SELinux context. + pid_t child = fork(); + if (child == 0) { + // Disabling ASLR makes it easier to determine the entry point of the target + // executable. + personality(ADDR_NO_RANDOMIZE); + + // Drop the privileges that can be dropped before executing the new binary + // and exit early if there is an issue. + if (!DropPreExecPrivileges(context)) { + exit(1); + } + + ptrace(PTRACE_TRACEME, 0, NULL, NULL); + raise(SIGSTOP); // Wait for the parent process to attach. + execv(executable_and_args[0], executable_and_args); + } else { + // Wait for the child to reach the SIGSTOP line above. + int status; + waitpid(child, &status, 0); + if ((status >> 8) != SIGSTOP) { + // If the first status is not SIGSTOP, then the child aborted early + // because it was not able to set the user and group IDs. + return false; + } + + // Break inside the child's execv call. + ptrace(PTRACE_SETOPTIONS, child, NULL, + PTRACE_O_TRACEEXEC | PTRACE_O_EXITKILL); + ptrace(PTRACE_CONT, child, NULL, NULL); + waitpid(child, &status, 0); + if (status >> 8 != (SIGTRAP | PTRACE_EVENT_EXEC << 8)) { + std::cerr << "Failed to execute " << executable_and_args[0] << std::endl; + return false; + } + + // Allow the dynamic linker to run before dropping to a lower SELinux + // context. This is required for executing in some very constrained domains + // like mediacodec. + // + // If the context was dropped before the dynamic linker runs, then when the + // linker attempts to read /proc/self/exe to determine dynamic symbol + // information, SELinux will kill the binary if the domain is not allowed to + // read the binary's executable file. + // + // This happens for example, when attempting to run any toybox binary (id, + // sh, etc) as mediacodec. + if (!StepToEntryPoint(child)) { + std::cerr << "Something bad happened stepping to the entry point." + << std::endl; + return false; + } + + // Run the SELinux shellcode in the child process before the child can + // execute any instructions in the newly loaded executable. + if (context->selinux_context.has_value()) { + size_t shell_code_size; + std::unique_ptr<uint8_t[]> shell_code = GetSELinuxShellCode( + context->selinux_context.value(), &shell_code_size); + bool success = ExecuteShellCode(child, shell_code.get(), shell_code_size); + if (!success) { + return false; + } + } + + // Resume and detach from the child now that the SELinux context has been + // updated. + ptrace(PTRACE_DETACH, child, NULL, NULL); + waitpid(child, nullptr, 0); + } + return true; +} + +} // namespace shell_as diff --git a/utils/shell-as/execute.h b/utils/shell-as/execute.h new file mode 100644 index 000000000..2e8f51189 --- /dev/null +++ b/utils/shell-as/execute.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef SHELL_AS_EXECUTE_H_ +#define SHELL_AS_EXECUTE_H_ + +#include "context.h" + +namespace shell_as { + +// Executes a command in the given security context. +// +// The executable_and_args parameter must contain at least two values. The first +// value is the path to the executable to run and the last value must be null. +// Additional arguments are passed to the executable as command line options. +// +// Returns true if the executable was run and false otherwise. +bool ExecuteInContext(char* const executable_and_args[], + const SecurityContext* context); +} // namespace shell_as + +#endif // SHELL_AS_EXECUTE_H_ diff --git a/utils/shell-as/gen-manifest.sh b/utils/shell-as/gen-manifest.sh new file mode 100755 index 000000000..9dc4d1142 --- /dev/null +++ b/utils/shell-as/gen-manifest.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# Copyright (C) 2023 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. + +# Generates an AndroidManifest.xml file from a template by replacing the line +# containing the substring, 'PERMISSIONS', with a list of permissions defined in +# another text file. + +set -e + +if [ "$#" != 3 ]; +then + echo "usage: gen-manifest.sh AndroidManifest.xml.template" \ + "permissions.txt AndroidManifest.xml" + exit 1 +fi + +readonly template="$1" +readonly permissions="$2" +readonly output="$3" + +echo "template = $1" + +# Print the XML template file before the line containing PERMISSIONS. +sed -e '/PERMISSIONS/,$d' "$template" > "$output" + +# Print the permissions formatted as XML. +sed -r 's!(.*)! <uses-permission android:name="\1"/>!g' "$permissions" >> "$output" + +# Print the XML template file after the line containing PERMISSIONS. +sed -e '1,/PERMISSIONS/d' "$template" >> "$output" diff --git a/utils/shell-as/registers.h b/utils/shell-as/registers.h new file mode 100644 index 000000000..6f7af6c54 --- /dev/null +++ b/utils/shell-as/registers.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef SHELL_AS_REGISTERS_H_ +#define SHELL_AS_REGISTERS_H_ + +#if defined(__aarch64__) + +#define REGISTER_STRUCT struct user_pt_regs +#define PROGRAM_COUNTER(regs) (regs.pc) + +#elif defined(__i386__) + +#include "sys/user.h" +#define REGISTER_STRUCT struct user_regs_struct +#define PROGRAM_COUNTER(regs) (regs.eip) + +#elif defined(__x86_64__) + +#include "sys/user.h" +#define REGISTER_STRUCT struct user_regs_struct +#define PROGRAM_COUNTER(regs) (regs.rip) + +#elif defined(__arm__) + +#define REGISTER_STRUCT struct user_regs +#define PROGRAM_COUNTER(regs) (regs.ARM_pc) + +#endif + +#endif // SHELL_AS_REGISTERS_H_ diff --git a/utils/shell-as/shell-as-main.cpp b/utils/shell-as/shell-as-main.cpp new file mode 100644 index 000000000..880cf1c91 --- /dev/null +++ b/utils/shell-as/shell-as-main.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 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. + */ + +#include <iostream> +#include <memory> +#include <string> + +#include "./command-line.h" +#include "./context.h" +#include "./execute.h" + +int main(const int argc, char* const argv[]) { + bool verbose = false; + auto context = std::make_unique<shell_as::SecurityContext>(); + char* const* execute_arguments = nullptr; + if (!shell_as::ParseOptions(argc, argv, &verbose, context.get(), + &execute_arguments)) { + return 1; + } + + if (verbose) { + std::cerr << "Dropping privileges to:" << std::endl; + std::cerr << "\tuser ID = " + << (context->user_id.has_value() + ? std::to_string(context->user_id.value()) + : "<no value>") + << std::endl; + + std::cerr << "\tgroup ID = " + << (context->group_id.has_value() + ? std::to_string(context->group_id.value()) + : "<no value>") + << std::endl; + + std::cerr << "\tsupplementary group IDs = "; + if (!context->supplementary_group_ids.has_value()) { + std::cerr << "<no value>"; + } else { + for (auto& id : context->supplementary_group_ids.value()) { + std::cerr << id << " "; + } + } + std::cerr << std::endl; + + std::cerr << "\tSELinux = " + << (context->selinux_context.has_value() + ? context->selinux_context.value() + : "<no value>") + << std::endl; + + std::cerr << "\tseccomp = "; + if (!context->seccomp_filter.has_value()) { + std::cerr << "<no value>"; + } else { + switch (context->seccomp_filter.value()) { + case shell_as::kAppFilter: + std::cerr << "app"; + break; + case shell_as::kAppZygoteFilter: + std::cerr << "app-zygote"; + break; + case shell_as::kSystemFilter: + std::cerr << "system"; + break; + } + } + std::cerr << std::endl; + + std::cerr << "\tcapabilities = "; + if (!context->capabilities.has_value()) { + std::cerr << "<no value>"; + } else { + std::cerr << "'" << cap_to_text(context->capabilities.value(), nullptr) + << "'"; + } + std::cerr << std::endl; + } + + return !shell_as::ExecuteInContext(execute_arguments, context.get()); +} diff --git a/utils/shell-as/shell-as-test-app-key.pk8 b/utils/shell-as/shell-as-test-app-key.pk8 Binary files differnew file mode 100644 index 000000000..df9254543 --- /dev/null +++ b/utils/shell-as/shell-as-test-app-key.pk8 diff --git a/utils/shell-as/shell-as-test-app-key.x509.pem b/utils/shell-as/shell-as-test-app-key.x509.pem new file mode 100644 index 000000000..4e5efc980 --- /dev/null +++ b/utils/shell-as/shell-as-test-app-key.x509.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIECzCCAvOgAwIBAgIUNyI1+/ZDui4r+jp6uy/aVRBpeR0wDQYJKoZIhvcNAQEL +BQAwgZQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy +b2lkMRAwDgYDVQQDDAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFu +ZHJvaWQuY29tMB4XDTE5MTIwNTIyNDEwMloXDTQ3MDQyMjIyNDEwMlowgZQxCzAJ +BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFp +biBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRyb2lkMRAwDgYD +VQQDDAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29t +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyzHGxhfDk4VzImAGQyFV +5+tb7Dgl9TTg57t8/LwMQX4abjB9o6tPwtZl757m4oLP8HCpjbI/kX5Wk4hLmNQ/ +I4AHG+LhCJGlz3nqBAJJxYoM//+3tUSLrq0ypuHMXNDPI5HGgE3hhzZbA9iWuGNB +7in+bHuhPFUq8e5og6piy3s3f77GB8QXzJEKyO2FhQR1Do8t4UdRji7TWR+USHqw +WBj/CyrpLJMwbr4Mx4YRN0JXUlFX1X/66ENonX4QZXofeiWDv5qgwFbbzgu9FLFN +imDeeCzU0mtYEKQpmZOdEaWclkT8IzUPwPMdawEq3Wj8nutoma5CztYl+OO9BgJC +LQIDAQABo1MwUTAdBgNVHQ4EFgQUqyxwI0Khq+xKbEGG3NCpN01wsaMwHwYDVR0j +BBgwFoAUqyxwI0Khq+xKbEGG3NCpN01wsaMwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEAIx4k1g1jDQs3ekseXMvz0V+O9AArWOEmwkIcA6EISvfC +dJ0DpmgRbZyvi0FowzOGYIZJ0Uwh4uwxETTHBQkvKoFdByukaasfX0p8axYVslT1 +87RrQDSA8fDp9K7d4kG3iXX16H5WJ0O/sI3UkZevZzVjXcoqSHA2CltGZv/EXPAh +dwGL5OupiiJcCV4ISSgh9PHswH1tGASdg3nqFqQLZrCYZE3pyLdsiDTQADlBMpZ4 +dH7kbh8McSA/OM2Fp1y05oecYVzKOzJ/I4SLhbSGLRLHvSg9fNiJPoKm46leQtFV +OVtzzBt6TKITRIhA8VVo45U0gVGUwlj/4BCKQLsJpA== +-----END CERTIFICATE----- diff --git a/utils/shell-as/shell-code.cpp b/utils/shell-as/shell-code.cpp new file mode 100644 index 000000000..bdadf6c5a --- /dev/null +++ b/utils/shell-as/shell-code.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./shell-code.h" + +#include <sys/mman.h> +#include <sys/types.h> + +#define PAGE_START(addr) ((uintptr_t)addr & ~(PAGE_SIZE - 1)) + +// Shell code that sets the SELinux context of the current process. +// +// The shell code expects a null-terminated SELinux context string to be placed +// immediately after it in memory. After the SELinux context has been changed +// the shell code will stop the current process with SIGSTOP. +// +// This shell code must be self-contained and position-independent. +extern "C" void __setcon_shell_code_start(); +extern "C" void __setcon_shell_code_end(); + +// Shell code that stops execution of the current process by raising a signal. +// The specific signal that is raised is given in __trap_shell_code_signal. +// +// This shell code can be used to inject break points into a traced process. +// +// The shell code must not modify any registers other than the program counter. +extern "C" void __trap_shell_code_start(); +extern "C" void __trap_shell_code_end(); +extern "C" int __trap_shell_code_signal; + +namespace shell_as { + +namespace { +void EnsureShellcodeReadable(void (*start)(), void (*end)()) { + mprotect((void*)PAGE_START(start), + PAGE_START(end) - PAGE_START(start) + PAGE_SIZE, + PROT_READ | PROT_EXEC); +} +} // namespace + +std::unique_ptr<uint8_t[]> GetSELinuxShellCode( + char* selinux_context, size_t* total_size) { + EnsureShellcodeReadable(&__setcon_shell_code_start, &__setcon_shell_code_end); + + size_t shell_code_size = (uintptr_t)&__setcon_shell_code_end - + (uintptr_t)&__setcon_shell_code_start; + size_t selinux_context_size = strlen(selinux_context) + 1 /* null byte */; + *total_size = shell_code_size + selinux_context_size; + + std::unique_ptr<uint8_t[]> shell_code(new uint8_t[*total_size]); + memcpy(shell_code.get(), (void*)&__setcon_shell_code_start, shell_code_size); + memcpy(shell_code.get() + shell_code_size, selinux_context, + selinux_context_size); + return shell_code; +} + +std::unique_ptr<uint8_t[]> GetTrapShellCode(int* expected_signal, + size_t* total_size) { + EnsureShellcodeReadable(&__trap_shell_code_start, &__trap_shell_code_end); + + *expected_signal = __trap_shell_code_signal; + + *total_size = + (uintptr_t)&__trap_shell_code_end - (uintptr_t)&__trap_shell_code_start; + std::unique_ptr<uint8_t[]> shell_code(new uint8_t[*total_size]); + memcpy(shell_code.get(), (void*)&__trap_shell_code_start, *total_size); + return shell_code; +} +} // namespace shell_as diff --git a/utils/shell-as/shell-code.h b/utils/shell-as/shell-code.h new file mode 100644 index 000000000..9c88d165a --- /dev/null +++ b/utils/shell-as/shell-code.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef SHELL_AS_SHELL_CODE_H_ +#define SHELL_AS_SHELL_CODE_H_ + +#include <selinux/selinux.h> +#include <sys/types.h> + +#include <memory> + +#include "context.h" + +namespace shell_as { + +// Returns shell code that when executed will set the current process's SElinux +// context to the given value and then SIGSTOP itself. +std::unique_ptr<uint8_t[]> GetSELinuxShellCode( + char* selinux_context, size_t* total_size); + +// Returns shell code that when executed will halt the current process and raise +// a signal. The specific signal is returned in the expected_signal argument. +std::unique_ptr<uint8_t[]> GetTrapShellCode(int* expected_signal, + size_t* total_size); +} // namespace shell_as + +#endif // SHELL_AS_SHELL_CODE_H_ diff --git a/utils/shell-as/shell-code/constants-arm.S b/utils/shell-as/shell-code/constants-arm.S new file mode 100644 index 000000000..10db630d9 --- /dev/null +++ b/utils/shell-as/shell-code/constants-arm.S @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 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. + */ + +// Arm specific constants. + +.equ SYS_OPEN, 0x000005 +.equ SYS_CLOSE, 0x000006 +.equ SYS_WRITE, 0x000004 +.equ SYS_KILL, 0x000025 +.equ SYS_GETPID, 0x000014 +.equ SYS_MPROTECT, 0x00007d diff --git a/utils/shell-as/shell-code/constants-arm64.S b/utils/shell-as/shell-code/constants-arm64.S new file mode 100644 index 000000000..a9dec758b --- /dev/null +++ b/utils/shell-as/shell-code/constants-arm64.S @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 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. + */ + +// Arm64 specific constants. + +.equ SYS_OPENAT, 0x38 +.equ SYS_CLOSE, 0x39 +.equ SYS_WRITE, 0x40 +.equ SYS_KILL, 0x81 +.equ SYS_GETPID, 0xAC +.equ SYS_MPROTECT, 0xE2 diff --git a/utils/shell-as/shell-code/constants-x86.S b/utils/shell-as/shell-code/constants-x86.S new file mode 100644 index 000000000..afa9d1472 --- /dev/null +++ b/utils/shell-as/shell-code/constants-x86.S @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 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. + */ + +// x86 specific constants. + +.equ SYS_WRITE, 0x04 +.equ SYS_OPEN, 0x05 +.equ SYS_CLOSE, 0x06 +.equ SYS_GETPID, 0x14 +.equ SYS_KILL, 0x25 +.equ SYS_MPROTECT, 0x7d diff --git a/utils/shell-as/shell-code/constants-x86_64.S b/utils/shell-as/shell-code/constants-x86_64.S new file mode 100644 index 000000000..0bf95cc75 --- /dev/null +++ b/utils/shell-as/shell-code/constants-x86_64.S @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 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. + */ + +// x86-64 specific constants. + +.equ SYS_WRITE, 0x01 +.equ SYS_OPEN, 0x02 +.equ SYS_CLOSE, 0x03 +.equ SYS_GETPID, 0x27 +.equ SYS_KILL, 0x3e +.equ SYS_MPROTECT, 0x0a diff --git a/utils/shell-as/shell-code/constants.S b/utils/shell-as/shell-code/constants.S new file mode 100644 index 000000000..9e2a238d5 --- /dev/null +++ b/utils/shell-as/shell-code/constants.S @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 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. + */ + +// Architecture independent constants. + +.equ AT_FDCWD, -100 +.equ O_WRONLY, 1 + +// Possible memory access modes for mprotect. +.equ PROT_NONE, 0 +.equ PROT_READ, 1 +.equ PROT_WRITE, 2 +.equ PROT_EXEC, 4 + +.equ SIGILL, 4 +.equ SIGTRAP, 5 +.equ SIGSTOP, 19 diff --git a/utils/shell-as/shell-code/selinux-arm.S b/utils/shell-as/shell-code/selinux-arm.S new file mode 100644 index 000000000..0c9480f9b --- /dev/null +++ b/utils/shell-as/shell-code/selinux-arm.S @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 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. + */ + +// Shell code that sets the current SELinux context to a given string. +// +// The desired SELinux context is appended to the payload as a null-terminated +// string. +// +// After the SELinux context has been updated the current process will raise +// SIGSTOP. + +#include "./shell-code/constants.S" +#include "./shell-code/constants-arm.S" + +.thumb + +.globl __setcon_shell_code_start +.globl __setcon_shell_code_end + +__setcon_shell_code_start: + // Ensure that the context and SELinux /proc file are readable. This assumes + // that the max length of these two strings is shorter than 0x1000. + // + // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC) + mov r7, SYS_MPROTECT + adr r0, context + movw r2, 0xF000 + movt r2, 0xFFFF + and r0, r0, r2 + mov r1, 0x2000 + mov r2, (PROT_READ | PROT_EXEC) + swi 0 + + // r10 = open("/proc/self/attr/current", O_WRONLY, O_WRONLY) + mov r7, SYS_OPEN + adr r0, selinux_proc_file + mov r1, O_WRONLY + mov r2, O_WRONLY + swi 0 + mov r10, r0 + + // r11 = strlen(context) + mov r11, 0 + adr r0, context +strlen_start: + ldrb r1, [r0, r11] + cmp r1, 0 + beq strlen_done + add r11, r11, 1 + b strlen_start +strlen_done: + + // write(r10, context, r11) + mov r7, SYS_WRITE + mov r0, r10 + adr r1, context + mov r2, r11 + swi 0 + + // close(r10) + mov r7, SYS_CLOSE + mov r0, r10 + swi 0 + + // r0 = getpid() + mov r7, SYS_GETPID + swi 0 + + // kill(r0, SIGSTOP) + mov r7, SYS_KILL + mov r1, SIGSTOP + swi 0 + +selinux_proc_file: + .asciz "/proc/thread-self/attr/current" + +context: +__setcon_shell_code_end: diff --git a/utils/shell-as/shell-code/selinux-arm64.S b/utils/shell-as/shell-code/selinux-arm64.S new file mode 100644 index 000000000..4e8c49296 --- /dev/null +++ b/utils/shell-as/shell-code/selinux-arm64.S @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 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. + */ + +// Shell code that sets the current SELinux context to a given string. +// +// The desired SELinux context is appended to the payload as a null-terminated +// string. +// +// After the SELinux context has been updated the current process will raise +// SIGSTOP. + +#include "./shell-code/constants.S" +#include "./shell-code/constants-arm64.S" + +.globl __setcon_shell_code_start +.globl __setcon_shell_code_end + +__setcon_shell_code_start: + // Ensure that the context and SELinux /proc file are readable. This assumes + // that the max length of these two strings is shorter than 0x1000. + // + // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC) + mov x8, SYS_MPROTECT + adr X0, __setcon_shell_code_end + and x0, x0, ~0xFFF + mov x1, 0x2000 + mov x2, (PROT_READ | PROT_EXEC) + svc 0 + + // x10 = openat(AT_FDCWD, "/proc/self/attr/current", O_WRONLY, O_WRONLY) + mov x8, SYS_OPENAT + mov x0, AT_FDCWD + adr x1, selinux_proc_file + mov x2, O_WRONLY + mov x3, O_WRONLY + svc 0 + mov x10, x0 + + // x11 = strlen(context) + mov x11, 0 + adr x0, context +strlen_start: + ldrb w1, [x0, x11] + cmp w1, 0 + b.eq strlen_done + add x11, x11, 1 + b strlen_start +strlen_done: + + // write(x10, context, x11) + mov x8, SYS_WRITE + mov x0, x10 + adr x1, context + mov x2, x11 + svc 0 + + // close(x10) + mov x8, SYS_CLOSE + mov x0, x10 + svc 0 + + // x0 = getpid() + mov x8, SYS_GETPID + svc 0 + + // kill(x0, SIGSTOP) + mov x8, SYS_KILL + mov x1, SIGSTOP + svc 0 + +selinux_proc_file: + .asciz "/proc/thread-self/attr/current" + +context: +__setcon_shell_code_end: diff --git a/utils/shell-as/shell-code/selinux-x86.S b/utils/shell-as/shell-code/selinux-x86.S new file mode 100644 index 000000000..81c150f13 --- /dev/null +++ b/utils/shell-as/shell-code/selinux-x86.S @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 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. + */ + +// Shell code that sets the current SELinux context to a given string. +// +// The desired SELinux context is appended to the payload as a null-terminated +// string. +// +// After the SELinux context has been updated the current process will raise +// SIGSTOP. + +#include "./shell-code/constants.S" +#include "./shell-code/constants-x86.S" + +.globl __setcon_shell_code_start +.globl __setcon_shell_code_end + +__setcon_shell_code_start: + + // x86 does not have RIP relative addressing. To work around this, relative + // calls are used to obtain the runtime address of a label. Once the location + // of one label is known, other labels can be addressed relative to the known + // label. + call constant_relative_address +constant_relative_address: + pop %esi + + // Ensure that the context and SELinux /proc file are readable. This assumes + // that the max length of these two strings is shorter than 0x1000. + // + // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC) + mov $SYS_MPROTECT, %eax + mov $~0xFFF, %ebx + and %esi, %ebx + mov $0x2000, %ecx + mov $(PROT_READ | PROT_EXEC), %edx + int $0x80 + + // ebx = open("/proc/self/attr/current", O_WRONLY, O_WRONLY) + mov $SYS_OPEN, %eax + lea (selinux_proc_file - constant_relative_address)(%esi), %ebx + mov $O_WRONLY, %ecx + mov $O_WRONLY, %edx + int $0x80 + mov %eax, %ebx + + // write(ebx, context, strlen(context)) + xor %edx, %edx + leal (context - constant_relative_address)(%esi), %ecx +strlen_start: + movb (%ecx, %edx), %al + test %al, %al + jz strlen_done + inc %edx + jmp strlen_start +strlen_done: + mov $SYS_WRITE, %eax + int $0x80 + + // close(ebx) + mov $SYS_CLOSE, %eax + int $0x80 + + // ebx = getpid() + mov $SYS_GETPID, %eax + int $0x80 + mov %eax, %ebx + + // kill(ebx, SIGSTOP) + mov $SYS_KILL, %eax + mov $SIGSTOP, %ecx + int $0x80 + +selinux_proc_file: + .asciz "/proc/self/attr/current" + +context: +__setcon_shell_code_end: diff --git a/utils/shell-as/shell-code/selinux-x86_64.S b/utils/shell-as/shell-code/selinux-x86_64.S new file mode 100644 index 000000000..94fc876c6 --- /dev/null +++ b/utils/shell-as/shell-code/selinux-x86_64.S @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 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. + */ + +// Shell code that sets the current SELinux context to a given string. +// +// The desired SELinux context is appended to the payload as a null-terminated +// string. +// +// After the SELinux context has been updated the current process will raise +// SIGSTOP. + +#include "./shell-code/constants.S" +#include "./shell-code/constants-x86_64.S" + +.globl __setcon_shell_code_start +.globl __setcon_shell_code_end + +__setcon_shell_code_start: + + // Ensure that the context and SELinux /proc file are readable. This assumes + // that the max length of these two strings is shorter than 0x1000. + // + // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC) + mov $SYS_MPROTECT, %rax + lea context(%rip), %rdi + and $~0xFFF, %rdi + mov $0x2000, %rsi + mov $(PROT_READ | PROT_EXEC), %rdx + syscall + + // rdi = open("/proc/self/attr/current", O_WRONLY, O_WRONLY) + mov $SYS_OPEN, %eax + lea selinux_proc_file(%rip), %rdi + mov $O_WRONLY, %rsi + mov $O_WRONLY, %rdx + syscall + mov %rax, %rdi + + // write(rdi, context, strlen(context)) + xor %rdx, %rdx + lea context(%rip), %rsi +strlen_start: + movb (%rsi, %rdx), %al + test %al, %al + jz strlen_done + inc %rdx + jmp strlen_start +strlen_done: + mov $SYS_WRITE, %rax + syscall + + // close(rdi) + mov $SYS_CLOSE, %rax + syscall + + // rdi = getpid() + mov $SYS_GETPID, %rax + syscall + mov %rax, %rdi + + // kill(rdi, SIGSTOP) + mov $SYS_KILL, %rax + mov $SIGSTOP, %rsi + syscall + +selinux_proc_file: + .asciz "/proc/self/attr/current" + +context: +__setcon_shell_code_end: diff --git a/utils/shell-as/shell-code/trap-arm.S b/utils/shell-as/shell-code/trap-arm.S new file mode 100644 index 000000000..8bb347411 --- /dev/null +++ b/utils/shell-as/shell-code/trap-arm.S @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./shell-code/constants.S" + +.thumb + +.globl __trap_shell_code_start +.globl __trap_shell_code_end +.globl __trap_shell_code_signal + +__trap_shell_code_start: +bkpt +__trap_shell_code_end: + +__trap_shell_code_signal: +.int SIGTRAP diff --git a/utils/shell-as/shell-code/trap-arm64.S b/utils/shell-as/shell-code/trap-arm64.S new file mode 100644 index 000000000..90063ffee --- /dev/null +++ b/utils/shell-as/shell-code/trap-arm64.S @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./shell-code/constants.S" + +.globl __trap_shell_code_start +.globl __trap_shell_code_end +.globl __trap_shell_code_signal + +__trap_shell_code_start: +hlt 0 +__trap_shell_code_end: + +__trap_shell_code_signal: +.int SIGILL diff --git a/utils/shell-as/shell-code/trap-x86.S b/utils/shell-as/shell-code/trap-x86.S new file mode 100644 index 000000000..1669bb8ee --- /dev/null +++ b/utils/shell-as/shell-code/trap-x86.S @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./shell-code/constants.S" + +.globl __trap_shell_code_start +.globl __trap_shell_code_end +.globl __trap_shell_code_signal + +__trap_shell_code_start: +int $0x03 +__trap_shell_code_end: + +__trap_shell_code_signal: +.int SIGTRAP diff --git a/utils/shell-as/shell-code/trap-x86_64.S b/utils/shell-as/shell-code/trap-x86_64.S new file mode 100644 index 000000000..1669bb8ee --- /dev/null +++ b/utils/shell-as/shell-code/trap-x86_64.S @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./shell-code/constants.S" + +.globl __trap_shell_code_start +.globl __trap_shell_code_end +.globl __trap_shell_code_signal + +__trap_shell_code_start: +int $0x03 +__trap_shell_code_end: + +__trap_shell_code_signal: +.int SIGTRAP diff --git a/utils/shell-as/string-utils.cpp b/utils/shell-as/string-utils.cpp new file mode 100644 index 000000000..8977f73e0 --- /dev/null +++ b/utils/shell-as/string-utils.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./string-utils.h" + +#include <errno.h> +#include <stdlib.h> +#include <string.h> + +namespace shell_as { + +bool StringToUInt32(const char* s, uint32_t* i) { + uint64_t value = 0; + if (!StringToUInt64(s, &value)) { + return false; + } + if (value > UINT_MAX) { + return false; + } + *i = value; + return true; +} + +bool StringToUInt64(const char* s, uint64_t* i) { + char* endptr = nullptr; + // Reset errno to a non-error value since strtoul does not clear errno. + errno = 0; + *i = strtoul(s, &endptr, 10); + // strtoul will return 0 if the value cannot be parsed as an unsigned long. If + // this occurs, ensure that the ID actually was zero. This is done by ensuring + // that the end pointer was advanced and that it now points to the end of the + // string (a null byte). + return errno == 0 && (*i != 0 || (endptr != s && *endptr == '\0')); +} + +bool SplitIdsAndSkip(char* line, const char* separators, int num_to_skip, + std::vector<uid_t>* ids) { + if (line == nullptr) { + return false; + } + + ids->clear(); + for (char* id_string = strtok(line, separators); id_string != nullptr; + id_string = strtok(nullptr, separators)) { + if (num_to_skip > 0) { + num_to_skip--; + continue; + } + + gid_t id; + if (!StringToUInt32(id_string, &id)) { + return false; + } + ids->push_back(id); + } + return true; +} + +} // namespace shell_as diff --git a/utils/shell-as/string-utils.h b/utils/shell-as/string-utils.h new file mode 100644 index 000000000..f4910894a --- /dev/null +++ b/utils/shell-as/string-utils.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef SHELL_AS_STRING_UTILS_H_ +#define SHELL_AS_STRING_UTILS_H_ + +#include <unistd.h> + +#include <vector> + +namespace shell_as { + +// Parses a string into an unsigned 32bit int value. Returns true on success and +// false otherwise. +bool StringToUInt32(const char* s, uint32_t* i); + +// Parses a string into a unsigned 64bit int value. Returns true on success and +// false otherwise. +bool StringToUInt64(const char* s, uint64_t* i); + +// Splits a line of uid_t/guid_t values by a given separator and returns the +// integer values in a vector. +// +// The separators string may contain multiple characters and is treated as a set +// of possible separating characters. +// +// If num_to_skip is non-zero, then that many entries will be skipped after +// splitting the line and before parsing the values as integers. This is useful +// if the line has a prefix such as "Gid: 1 2 3 4". +// +// Returns true on success and false otherwise. +bool SplitIdsAndSkip(char* line, const char* separators, int num_to_skip, + std::vector<uid_t>* ids); + +} // namespace shell_as + +#endif // SHELL_AS_STRING_UTILS_H_ diff --git a/utils/shell-as/test-app.cpp b/utils/shell-as/test-app.cpp new file mode 100644 index 000000000..84fedcab6 --- /dev/null +++ b/utils/shell-as/test-app.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 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. + */ + +#include "./test-app.h" + +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <iostream> +#include <string> + +#include "./string-utils.h" + +namespace shell_as { + +// Returns a pointer to bytes of the test app APK along with the length in bytes +// of the APK. +// +// This function is defined by the shell-as-test-app-apk-cpp genrule. +void GetTestApk(uint8_t **apk, size_t *length); + +namespace { + +// The staging path for the test app APK. +const char kTestAppApkStagingPath[] = "/data/local/tmp/shell-as-test-app.apk"; + +// Writes the test app to a staging location and then installs the APK via the +// 'pm' utility. The app is granted runtime permissions on installation. Returns +// true if the app is installed successfully. +bool InstallTestApp() { + uint8_t *apk = nullptr; + size_t apk_size = 0; + GetTestApk(&apk, &apk_size); + + int staging_file = open(kTestAppApkStagingPath, O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR); + if (staging_file == -1) { + std::cerr << "Unable to open staging APK path." << std::endl; + return false; + } + + size_t bytes_written = write(staging_file, apk, apk_size); + close(staging_file); + if (bytes_written != apk_size) { + std::cerr << "Unable to write entire test app APK." << std::endl; + return false; + } + + const char cmd_template[] = "pm install -g %s > /dev/null 2> /dev/null"; + char system_cmd[sizeof(cmd_template) + sizeof(kTestAppApkStagingPath) + 1] = + {}; + sprintf(system_cmd, cmd_template, kTestAppApkStagingPath); + return system(system_cmd) == 0; +} + +// Uninstalls the test app if it is installed. This method is a no-op if the app +// is not installed. +void UninstallTestApp() { + system( + "pm uninstall com.android.google.tools.security.shell_as" + " > /dev/null 2> /dev/null"); +} + +// Starts the main activity of the test app. This is necessary as some aspects +// of the security context can only be inferred from a running process. +bool StartTestApp() { + return system( + "am start-activity " + "com.android.google.tools.security.shell_as/" + ".MainActivity" + " > /dev/null 2> /dev/null") == 0; +} + +// Obtain the process ID of the test app and returns true if it is running. +// Returns false otherwise. +bool GetTestAppProcessId(pid_t *test_app_pid) { + FILE *pgrep = popen( + "pgrep -f " + "com.android.google.tools.security.shell_as", + "r"); + if (!pgrep) { + std::cerr << "Unable to execute pgrep." << std::endl; + return false; + } + + char pgrep_output[128]; + memset(pgrep_output, 0, sizeof(pgrep_output)); + int bytes_read = fread(pgrep_output, 1, sizeof(pgrep_output) - 1, pgrep); + pclose(pgrep); + if (bytes_read <= 0) { + // Unable to find the process. This may happen if the app is still starting + // up. + return false; + } + return StringToUInt32(pgrep_output, (uint32_t *)test_app_pid); +} +} // namespace + +bool SetupAndStartTestApp(pid_t *test_app_pid) { + UninstallTestApp(); + + if (!InstallTestApp()) { + std::cerr << "Unable to install test app." << std::endl; + return false; + } + + if (!StartTestApp()) { + std::cerr << "Unable to start and obtain test app PID." << std::endl; + return false; + } + + for (int i = 0; i < 5; i++) { + if (GetTestAppProcessId(test_app_pid)) { + return true; + } + sleep(1); + } + return false; +} +} // namespace shell_as diff --git a/utils/shell-as/test-app.h b/utils/shell-as/test-app.h new file mode 100644 index 000000000..866bbfb33 --- /dev/null +++ b/utils/shell-as/test-app.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 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. + */ + +#ifndef SHELL_AS_TEST_APP_H_ +#define SHELL_AS_TEST_APP_H_ + +#include <sys/types.h> + +namespace shell_as { + +// Installs and launches the embedded shell-as test app. The test app requests +// and is granted all non-system permissions defined by the OS. The test_app_pid +// parameter is set to the process ID of the running test app. Returns true if +// successful. +bool SetupAndStartTestApp(pid_t *test_app_pid); +} + +#endif // SHELL_AS_TEST_APP_H_ |