Add a limited privilege "trade-in mode" for adbd.
If service.adb.tradeinmode is 1, adbd will setcon() into a lower
privilege mode. If for some reason setcon() fails, the property is
changed to -1 to prevent infinite loops. On user builds, adbd is also
stopped.
To test:
adb root
adb shell setprop service.adb.tradeinmode 1
abd unroot
In this limited environment, the only command allowed is "adb shell
tradeinmode". On userdebug or eng builds, "adb root" can be used to
leave trade-in mode.
Bug: 307713521
Test: manual test
Flag: com.android.tradeinmode.flags.enable_trade_in_mode
Change-Id: Ie2829b880af07469c653cb6e9f83d709e6a982bd
diff --git a/Android.bp b/Android.bp
index 6a62c34..295d7f9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -628,6 +628,7 @@
"daemon/file_sync_service.cpp",
"daemon/services.cpp",
"daemon/shell_service.cpp",
+ "daemon/tradeinmode.cpp",
"shell_service_protocol.cpp",
],
@@ -676,6 +677,9 @@
"libmdnssd",
"libselinux",
],
+ static_libs: [
+ "android_trade_in_mode_flags_cc_lib",
+ ],
},
recovery: {
exclude_srcs: [
@@ -685,6 +689,9 @@
"libadb_pairing_auth",
"libadb_pairing_connection",
],
+ exclude_static_libs: [
+ "android_trade_in_mode_flags_cc_lib",
+ ],
},
},
@@ -805,6 +812,14 @@
"libadb_pairing_auth",
"libadb_pairing_connection",
],
+ exclude_static_libs: [
+ "android_trade_in_mode_flags_cc_lib",
+ ],
+ },
+ android: {
+ static_libs: [
+ "android_trade_in_mode_flags_cc_lib",
+ ],
},
},
}
@@ -893,6 +908,8 @@
"daemon/services.cpp",
"daemon/shell_service.cpp",
"daemon/shell_service_test.cpp",
+ "daemon/tradeinmode.cpp",
+ "daemon/tradeinmode_test.cpp",
"test_utils/test_utils.cpp",
"shell_service_protocol_test.cpp",
"mdns_test.cpp",
@@ -905,6 +922,9 @@
srcs: [
"daemon/property_monitor_test.cpp",
],
+ static_libs: [
+ "android_trade_in_mode_flags_cc_lib",
+ ],
},
},
diff --git a/apex/adbd.rc b/apex/adbd.rc
index 0fb6f69..09ceff8 100644
--- a/apex/adbd.rc
+++ b/apex/adbd.rc
@@ -1,4 +1,4 @@
-service adbd /apex/com.android.adbd/bin/adbd --root_seclabel=u:r:su:s0
+service adbd /apex/com.android.adbd/bin/adbd --root_seclabel=u:r:su:s0 --tim_seclabel=u:r:adbd_tradeinmode:s0
class core
socket adbd seqpacket 660 system system
disabled
diff --git a/daemon/main.cpp b/daemon/main.cpp
index 725070c..233f32e 100644
--- a/daemon/main.cpp
+++ b/daemon/main.cpp
@@ -51,6 +51,7 @@
#include "adb_utils.h"
#include "adb_wifi.h"
#include "socket_spec.h"
+#include "tradeinmode.h"
#include "transport.h"
#include "daemon/jdwp_service.h"
@@ -59,6 +60,7 @@
#if defined(__ANDROID__)
static const char* root_seclabel = nullptr;
+static const char* tim_seclabel = nullptr;
static bool should_drop_privileges() {
// The properties that affect `adb root` and `adb unroot` are ro.secure and
@@ -159,6 +161,13 @@
if (cap_set_proc(caps.get()) != 0) {
PLOG(FATAL) << "cap_set_proc() failed";
}
+
+ if (should_enter_tradeinmode()) {
+ enter_tradeinmode(tim_seclabel);
+ auth_required = false;
+ } else if (is_in_tradein_evaluation_mode()) {
+ auth_required = false;
+ }
} else {
// minijail_enter() will abort if any priv-dropping step fails.
minijail_enter(jail.get());
@@ -314,6 +323,7 @@
while (true) {
static struct option opts[] = {
{"root_seclabel", required_argument, nullptr, 's'},
+ {"tim_seclabel", required_argument, nullptr, 't'},
{"device_banner", required_argument, nullptr, 'b'},
{"version", no_argument, nullptr, 'v'},
{"logpostfsdata", no_argument, nullptr, 'l'},
@@ -331,6 +341,9 @@
case 's':
root_seclabel = optarg;
break;
+ case 't':
+ tim_seclabel = optarg;
+ break;
#endif
case 'b':
adb_device_banner = optarg;
diff --git a/daemon/services.cpp b/daemon/services.cpp
index 5090441..152603c 100644
--- a/daemon/services.cpp
+++ b/daemon/services.cpp
@@ -51,6 +51,7 @@
#include "services.h"
#include "socket_spec.h"
#include "sysdeps.h"
+#include "tradeinmode.h"
#include "transport.h"
#include "daemon/file_sync_service.h"
@@ -275,6 +276,10 @@
unique_fd daemon_service_to_fd(std::string_view name, atransport* transport) {
ADB_LOG(Service) << "transport " << transport->serial_name() << " opening service " << name;
+ if (is_in_tradeinmode() && !allow_tradeinmode_command(name)) {
+ return unique_fd{};
+ }
+
#if defined(__ANDROID__) && !defined(__ANDROID_RECOVERY__)
if (name.starts_with("abb:") || name.starts_with("abb_exec:")) {
return execute_abb_command(name);
diff --git a/daemon/tradeinmode.cpp b/daemon/tradeinmode.cpp
new file mode 100644
index 0000000..2b27e50
--- /dev/null
+++ b/daemon/tradeinmode.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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 <unistd.h>
+
+#include <regex>
+
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+
+#if defined(__ANDROID__)
+#include <log/log_properties.h>
+#include "selinux/android.h"
+#endif
+
+#if defined(__ANDROID__) && !defined(__ANDROID_RECOVERY__)
+#include <com_android_tradeinmode_flags.h>
+#endif
+
+static bool in_tradeinmode = false;
+static constexpr char kTradeInModeProp[] = "persist.adb.tradeinmode";
+
+static constexpr int TIM_DISABLED = -1;
+static constexpr int TIM_UNSET = 0;
+static constexpr int TIM_FOYER = 1;
+static constexpr int TIM_EVALUATION_MODE = 2;
+
+bool should_enter_tradeinmode() {
+#if defined(__ANDROID__) && !defined(__ANDROID_RECOVERY__)
+ if (!com_android_tradeinmode_flags_enable_trade_in_mode()) {
+ return false;
+ }
+ return android::base::GetIntProperty(kTradeInModeProp, TIM_UNSET) == TIM_FOYER;
+#else
+ return false;
+#endif
+}
+
+void enter_tradeinmode(const char* seclabel) {
+#if defined(__ANDROID__)
+ if (selinux_android_setcon(seclabel) < 0) {
+ PLOG(ERROR) << "Could not set SELinux context";
+
+ // Flag TIM as failed so we don't enter a restart loop.
+ android::base::SetProperty(kTradeInModeProp, std::to_string(TIM_DISABLED));
+
+ _exit(1);
+ }
+
+ // Keep a separate global flag for TIM in case the property changes (for
+ // example, if it's set while as root for testing).
+ in_tradeinmode = true;
+#endif
+}
+
+bool is_in_tradeinmode() {
+ return in_tradeinmode;
+}
+
+bool is_in_tradein_evaluation_mode() {
+ return android::base::GetIntProperty(kTradeInModeProp, TIM_UNSET) == TIM_EVALUATION_MODE;
+}
+
+bool allow_tradeinmode_command(std::string_view name) {
+#if defined(__ANDROID__)
+ // Allow "adb root" from trade-in-mode so that automated testing is possible.
+ if (__android_log_is_debuggable() && android::base::ConsumePrefix(&name, "root:")) {
+ return true;
+ }
+#endif
+
+ // Allow "shell tradeinmode" with only simple arguments.
+ std::regex tim_pattern("shell[^:]*:tradeinmode(\\s*|\\s[A-Za-z0-9_\\-\\s]*)");
+ return std::regex_match(std::string(name), tim_pattern);
+}
diff --git a/daemon/tradeinmode.h b/daemon/tradeinmode.h
new file mode 100644
index 0000000..2982728
--- /dev/null
+++ b/daemon/tradeinmode.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string_view>
+
+// Return true if adbd should transition to trade-in mode.
+bool should_enter_tradeinmode();
+
+// Transition adbd to the given trade-in mode secontext.
+void enter_tradeinmode(const char* seclabel);
+
+// Returns whether the given command string is allowed while in trade-in mode.
+bool allow_tradeinmode_command(std::string_view name);
+
+// Returns whether adbd is currently in trade-in mode (eg enter_tradeinmode was called).
+bool is_in_tradeinmode();
+
+// Returns whether the "tradeinmode enter" command was used. This command places the device in
+// "trade-in evaluation" mode, granting normal adb shell without authorization. In this mode, a
+// factory reset is guaranteed on reboot.
+bool is_in_tradein_evaluation_mode();
diff --git a/daemon/tradeinmode_test.cpp b/daemon/tradeinmode_test.cpp
new file mode 100644
index 0000000..8e0f75d
--- /dev/null
+++ b/daemon/tradeinmode_test.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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 "tradeinmode.h"
+
+#include <gtest/gtest.h>
+
+TEST(TradeInModeTest, ValidateCommand) {
+ EXPECT_FALSE(allow_tradeinmode_command("shell:blah"));
+ EXPECT_TRUE(allow_tradeinmode_command("shell,-x:tradeinmode"));
+ EXPECT_TRUE(allow_tradeinmode_command("shell:tradeinmode"));
+ EXPECT_FALSE(allow_tradeinmode_command("shell:tradeinmodebad"));
+ EXPECT_TRUE(allow_tradeinmode_command("shell:tradeinmode getstatus"));
+ EXPECT_TRUE(allow_tradeinmode_command("shell:tradeinmode getstatus -c 1234"));
+ EXPECT_TRUE(allow_tradeinmode_command("shell:tradeinmode enter"));
+ EXPECT_FALSE(allow_tradeinmode_command("shell:tradeinmode && ls"));
+}
diff --git a/docs/dev/README.md b/docs/dev/README.md
index 222b2d9..2c0fb59 100644
--- a/docs/dev/README.md
+++ b/docs/dev/README.md
@@ -3,4 +3,5 @@
- [Architecture](internals.md)
- [Debugging](debugging.md)
- [How root/unroot works](root.md)
-- [Understanding asocket](asocket.md)
\ No newline at end of file
+- [Understanding asocket](asocket.md)
+- [Trade-In Mode](adb_tradeinmode.md)
diff --git a/docs/dev/adb_tradeinmode.md b/docs/dev/adb_tradeinmode.md
new file mode 100644
index 0000000..7604b6e
--- /dev/null
+++ b/docs/dev/adb_tradeinmode.md
@@ -0,0 +1,70 @@
+# Architecture of *ADB Trade-In Mode*
+
+ADB can run in a specialized "trade-in mode" (TIM). This is a highly restricted ADB designed to
+faciliate automated diagnostics. It is only activated during the SetUp Wizard (SUW) on user builds.
+
+## Activation flow
+
+The DeviceDiagnostics apk has a `BOOT_COMPLETE` broadcast receiver, which it uses to call into the
+tradeinmode service (`ITradeInMode.start`). The service activates trade-in mode if the following
+conditions are true:
+
+1. ADB is disabled.
+2. `ro.debuggable` is 0 (to avoid breaking userdebug testing).
+3. The `USER_SETUP_COMPLETE` setting is 0.
+4. The `DEVICE_PROVISIONED` setting is 0.
+5. There is no active wifi connection.
+
+If all of these conditions hold, `persist.adb.tradeinmode` is set to `1` and the `ADB_ENABLED`
+setting is set to `1`.
+
+When adbd subsequentily starts, it sees `persist.adb.tradeinmode` is set and lowers its SELinux
+context to a highly restricted policy (`adb_tradeinmode`). This policy restricts adbd to
+effectively one command: `adb shell tradeinmode`. It also disables authorization.
+
+`ITradeInMode` monitors conditions 3, 4, and 5 above and turns off ADB as soon as any become true.
+
+If the device is rebooted, the persist property ensures that ADB will stay in trade-in mode.
+
+## userdebug testing
+
+On userdebug builds, TIM is not enabled by default since adb is already available. This means the
+authorization dialog is still present. However, TIM can still be manually tested with the following
+command sequence:
+1. `adb root`
+2. `adb shell setprop service.adb.tradeinmode 1`
+3. `adb unroot`
+
+Unlike user builds, if entering TIM fails, then userdebug adbd will simply restart without TIM
+enabled.
+
+## Trade-In Mode commands
+
+When ADB is in trade-in mode (the default in SUW when ro.debuggable is 0), the only allowed command
+is `adb shell tradeinmode` plus arguments. On userdebug or eng builds, `adb root` is also allowed.
+
+The tradeinmode shell command has two arguments:
+ - `getstatus [-challenge CHALLENGE]`: Returns diagnostic information about the device, optionally
+ with an attestation challenge.
+ - `evaluate`: Bypasses setup and enters Android in an evaluation mode. A factory reset is forced
+ on next boot.
+
+## Evaluation mode
+
+Evaluation mode is entered via `adb shell tradeinmode evaluate`. This changes
+`persist.adb.tradeinmode` to `2` and restarts adbd. adbd then starts normally, without trade-in
+mode restrictions. However, authorization is disabled. The device is factory reset on next boot.
+This mode allows further diagnostics via normal adb commands (such as adb install).
+
+## Factory reset
+
+The factory reset is guaranteed by `ITradeInModeService.enterEvaluationMode` which writes a marker
+to `/metadata/tradeinmode/wipe`. If first-stage init sees this file, it immediately reboots into
+recovery to issue an unprompted wipe.
+
+## persist.adb.tradeinmode values
+ - `-1`: Failed to start TIM.
+ - `0`: TIM is not enabled.
+ - `1`: TIM is enabled.
+ - `2`: "adb shell tradeinmode evaluate" was used, which enables adbd past SUW but
+ also guarantees a factory reset on reboot.
diff --git a/docs/dev/internals.md b/docs/dev/internals.md
index 9bb3cd1..68d3831 100644
--- a/docs/dev/internals.md
+++ b/docs/dev/internals.md
@@ -107,6 +107,10 @@
[here](adb_wifi.md)
+### ADB Trade-In Mode
+
+[here](adb_tradeinmode.md)
+
### Benchmark sample run for Pixel 8,USB
```
@@ -146,4 +150,4 @@
### More Legacy documentation
-[socket-activation.md](socket-activation.md): ADB socket control protocol.
\ No newline at end of file
+[socket-activation.md](socket-activation.md): ADB socket control protocol.