Merge "Build an apex shim with different certificate"
diff --git a/apexd/apex_shim.cpp b/apexd/apex_shim.cpp
index a896984..7394e6d 100644
--- a/apexd/apex_shim.cpp
+++ b/apexd/apex_shim.cpp
@@ -26,6 +26,7 @@
#include <sstream>
#include <unordered_set>
+#include "apex_constants.h"
#include "apex_file.h"
#include "string_log.h"
@@ -78,7 +79,7 @@
return ss.str();
}
-Result<std::vector<std::string>> ReadSha512(const std::string& path) {
+Result<std::vector<std::string>> GetAllowedHashes(const std::string& path) {
using android::base::ReadFileToString;
using android::base::StringPrintf;
const std::string& file_path =
@@ -88,7 +89,14 @@
if (!ReadFileToString(file_path, &hash, false /* follows symlinks */)) {
return ErrnoError() << "Failed to read " << file_path;
}
- return android::base::Split(hash, "\n");
+ std::vector<std::string> allowed_hashes = android::base::Split(hash, "\n");
+ auto system_shim_hash = CalculateSha512(
+ StringPrintf("%s/%s", kApexPackageSystemDir, shim::kSystemShimApexName));
+ if (!system_shim_hash) {
+ return system_shim_hash.error();
+ }
+ allowed_hashes.push_back(std::move(*system_shim_hash));
+ return allowed_hashes;
}
Result<void> IsRegularFile(const fs::directory_entry& entry) {
@@ -205,7 +213,7 @@
const std::string& new_apex_path) {
LOG(DEBUG) << "Validating update of shim apex to " << new_apex_path
<< " using system shim apex " << system_apex_path;
- auto allowed = ReadSha512(system_apex_path);
+ auto allowed = GetAllowedHashes(system_apex_path);
if (!allowed) {
return allowed.error();
}
diff --git a/apexd/apexd.cpp b/apexd/apexd.cpp
index a81a194..ceca6ec 100644
--- a/apexd/apexd.cpp
+++ b/apexd/apexd.cpp
@@ -51,6 +51,7 @@
#include <dirent.h>
#include <fcntl.h>
#include <linux/loop.h>
+#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
@@ -65,6 +66,7 @@
#include <memory>
#include <optional>
#include <string>
+#include <thread>
#include <unordered_map>
#include <unordered_set>
@@ -1623,6 +1625,66 @@
return 0;
}
+Result<void> remountApexFile(const std::string& path) {
+ auto ret = deactivatePackage(path);
+ if (!ret) return ret.error();
+
+ ret = activatePackage(path);
+ if (!ret) return ret.error();
+
+ return {};
+}
+
+Result<void> monitorBuiltinDirs() {
+ int fd = inotify_init1(IN_CLOEXEC);
+ if (fd == -1) {
+ return ErrnoErrorf("inotify_init failed");
+ }
+ std::map<int, std::string> desc_to_dir;
+ for (const auto& dir : kApexPackageBuiltinDirs) {
+ int desc = inotify_add_watch(fd, dir.c_str(), IN_CREATE | IN_MODIFY);
+ if (desc == -1 && errno != ENOENT) {
+ // don't complain about missing directories like /product/apex
+ return ErrnoErrorf("failed to add watch on {}", dir);
+ }
+ desc_to_dir.emplace(desc, dir);
+ }
+ static std::thread th([fd, desc_to_dir]() -> void {
+ constexpr int num_events = 100;
+ constexpr size_t average_path_length = 50;
+ char buffer[num_events *
+ (sizeof(struct inotify_event) + average_path_length)];
+ while (true) {
+ ssize_t length = read(fd, buffer, sizeof(buffer));
+ if (length < 0) {
+ PLOG(ERROR) << "failed to read inotify event: " << strerror(errno);
+ continue;
+ }
+ int i = 0;
+ while (i < length) {
+ struct inotify_event* e = (struct inotify_event*)&buffer[i];
+ if (e->len > 0 && (e->mask & (IN_CREATE | IN_MODIFY)) != 0) {
+ if (desc_to_dir.find(e->wd) == desc_to_dir.end()) {
+ LOG(ERROR) << "unexpected watch descriptor " << e->wd
+ << " for name: " << e->name;
+ } else {
+ std::string path = desc_to_dir.at(e->wd) + "/" + e->name;
+ auto ret = remountApexFile(path);
+ if (!ret) {
+ LOG(ERROR) << ret.error().message();
+ } else {
+ LOG(INFO) << path << " remounted because it was changed";
+ }
+ }
+ }
+ i += sizeof(struct inotify_event) + e->len;
+ }
+ }
+ });
+
+ return {};
+}
+
void onStart(CheckpointInterface* checkpoint_service) {
LOG(INFO) << "Marking APEXd as starting";
if (!android::base::SetProperty(kApexStatusSysprop, kApexStatusStarting)) {
@@ -1709,6 +1771,13 @@
<< status.error();
}
}
+
+ if (android::base::GetBoolProperty("ro.debuggable", false)) {
+ status = monitorBuiltinDirs();
+ if (!status) {
+ LOG(ERROR) << "cannot monitor built-in dirs: " << status.error();
+ }
+ }
}
void onAllPackagesReady() {
diff --git a/apexd/apexd_utils.h b/apexd/apexd_utils.h
index feb6a08..4d35249 100644
--- a/apexd/apexd_utils.h
+++ b/apexd/apexd_utils.h
@@ -36,6 +36,7 @@
#include "string_log.h"
using android::base::ErrnoError;
+using android::base::ErrnoErrorf;
using android::base::Error;
using android::base::Result;
diff --git a/apexd/apexservice.cpp b/apexd/apexservice.cpp
index 6a8a4df..5830c4b 100644
--- a/apexd/apexservice.cpp
+++ b/apexd/apexservice.cpp
@@ -138,7 +138,8 @@
int session_id, const std::vector<int>& child_session_ids,
ApexInfoList* apex_info_list) {
LOG(DEBUG) << "submitStagedSession() received by ApexService, session id "
- << session_id;
+ << session_id << " child sessions: ["
+ << android::base::Join(child_session_ids, ',') << "]";
Result<std::vector<ApexFile>> packages =
::android::apex::submitStagedSession(session_id, child_session_ids);
diff --git a/apexd/apexservice_test.cpp b/apexd/apexservice_test.cpp
index caa822e..e574899 100644
--- a/apexd/apexservice_test.cpp
+++ b/apexd/apexservice_test.cpp
@@ -1979,6 +1979,29 @@
ASSERT_FALSE(IsOk(service_->submitStagedSession(42, {}, &list)));
}
+TEST_F(ApexShimUpdateTest, UpdateToV1Success) {
+ PrepareTestApexForInstall installer(
+ GetTestFile("com.android.apex.cts.shim.apex"));
+
+ if (!installer.Prepare()) {
+ FAIL() << GetDebugStr(&installer);
+ }
+
+ ASSERT_TRUE(IsOk(service_->stagePackages({installer.test_file})));
+}
+
+TEST_F(ApexShimUpdateTest, SubmitStagedSessionV1ShimApexSuccess) {
+ PrepareTestApexForInstall installer(
+ GetTestFile("com.android.apex.cts.shim.apex"),
+ "/data/app-staging/session_97", "staging_data_file");
+ if (!installer.Prepare()) {
+ FAIL() << GetDebugStr(&installer);
+ }
+
+ ApexInfoList list;
+ ASSERT_TRUE(IsOk(service_->submitStagedSession(97, {}, &list)));
+}
+
TEST_F(ApexServiceTest, SubmitStagedSessionCorruptApexFails) {
PrepareTestApexForInstall installer(
GetTestFile("apex.apexd_test_corrupt_apex.apex"),
diff --git a/docs/README.md b/docs/README.md
index 495ebb4..5ba0ab9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -487,6 +487,17 @@
<device.mk>:
PRODUCT_PROPERTY_OVERRIDES += ro.apex.updatable=true
+
+BoardConfig.mk:
+TARGET_FLATTEN_APEX := false
+```
+
+or just
+
+```
+<device.mk>:
+
+$(call inherit-product, $(SRC_TARGET_DIR)/product/updatable_apex.mk)
```
## Flattened APEX
@@ -504,22 +515,21 @@
Activating a flattened APEX doesn't involve the loop device. The entire
directory `/system/apex/my.apex` is directly bind-mounted to `/apex/name@ver`.
-To build flattened APEXs, build the platform with the following flag.
-
-```
-BoardConfig.mk:
-
-TARGET_FLATTEN_APEX := true
-
-<device.mk>:
-
-PRODUCT_PROPERTY_OVERRIDES += ro.apex.updatable=false
-```
-
Flattened APEXs can't be updated by downloading updated versions
of the APEXs from network because the downloaded APEXs can't be flattened.
Flattened APEXs can be updated only via a regular OTA.
+Note that flattened APEX is the default configuration for now. This means all
+APEXes are by default flattened unless you explicitly configure your device
+to support updatable APEX (explained above).
+
+Also note that, mixing flattened and non-flattened APEXes in a device is NOT
+supported. It should be either all non-flattened or all flattened. This is
+especially important when shipping pre-signed APEX prebuilts for the projects
+like Mainline. APEXes that are not pre-signed (i.e. built from the source)
+should also be non-flattened and signed with proper keys in that case. The
+device should inherit from `updatable_apex.mk` as explained above.
+
## Alternatives considered when developing APEX
Here are some options that we considered when designing the APEX file
diff --git a/proto/Android.bp b/proto/Android.bp
index 962542b..1ec12b4 100644
--- a/proto/Android.bp
+++ b/proto/Android.bp
@@ -42,6 +42,16 @@
},
}
+java_library_static {
+ name: "apex_manifest_proto_java",
+ host_supported: true,
+ device_supported: false,
+ proto: {
+ type: "full",
+ },
+ srcs: ["apex_manifest.proto"],
+}
+
cc_library_static {
name: "lib_apex_session_state_proto",
host_supported: true,
diff --git a/proto/apex_manifest.proto b/proto/apex_manifest.proto
index f9531b0..3795acb 100644
--- a/proto/apex_manifest.proto
+++ b/proto/apex_manifest.proto
@@ -18,6 +18,9 @@
package apex.proto;
+option java_package = "com.android.apex";
+option java_outer_classname = "Protos";
+
message ApexManifest {
// Package Name
diff --git a/tests/Android.bp b/tests/Android.bp
index cabf3ad..c44acec 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -141,3 +141,18 @@
":com.android.apex.cts.shim.v2_prebuilt",
],
}
+
+java_test_host {
+ name: "apex_remount_tests",
+ srcs: ["src/**/ApexRemountTest.java"],
+ libs: ["tradefed"],
+ static_libs: [
+ "module_test_util",
+ "truth-prebuilt",
+ "apex_manifest_proto_java",
+ ],
+ test_config: "apex-remount-tests.xml",
+ test_suites: ["general-tests"],
+
+ data: [":com.android.apex.cts.shim.v2_prebuilt"],
+}
\ No newline at end of file
diff --git a/tests/apex-remount-tests.xml b/tests/apex-remount-tests.xml
new file mode 100644
index 0000000..50bf95f
--- /dev/null
+++ b/tests/apex-remount-tests.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs the apex remount test cases">
+ <option name="test-suite-tag" value="apex_remount_tests" />
+ <option name="test-suite-tag" value="apct" />
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="jar" value="apex_remount_tests.jar" />
+ </test>
+</configuration>
diff --git a/tests/src/com/android/tests/apex/ApexRemountTest.java b/tests/src/com/android/tests/apex/ApexRemountTest.java
new file mode 100644
index 0000000..bb8aa59
--- /dev/null
+++ b/tests/src/com/android/tests/apex/ApexRemountTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.apex;
+
+import com.android.apex.Protos.ApexManifest;
+import com.android.tests.util.ModuleTestUtils;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for automatic remount of APEXes when they are updated via `adb sync`
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ApexRemountTest extends BaseHostJUnit4Test {
+ private File mSavedShimFile;
+
+ private static final String SHIM_APEX_PACKAGE_NAME = "com.android.apex.cts.shim";
+ private static final String SHIM_APEX_PATH = "/system/apex/com.android.apex.cts.shim.apex";
+
+ @Before
+ public void setUp() throws Exception {
+ mSavedShimFile = getDevice().pullFile(SHIM_APEX_PATH);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mSavedShimFile != null) {
+ getDevice().remountSystemWritable();
+ getDevice().pushFile(mSavedShimFile, SHIM_APEX_PATH);
+ getDevice().reboot();
+ assertThat(getShimApexManifest().getVersion()).isEqualTo(1L);
+ }
+ }
+
+ @Test
+ public void testApexIsRemountedUponUpdate() throws Exception {
+ assumeTrue("APEXes on the device are flattened", hasNonFlattenedApex());
+
+ ModuleTestUtils utils = new ModuleTestUtils(this);
+ File updatedFile = utils.getTestFile("com.android.apex.cts.shim.v2.apex");
+
+ getDevice().remountSystemWritable();
+ getDevice().pushFile(updatedFile, SHIM_APEX_PATH);
+
+ // Wait some time until the update is detected by apexd and remount is done
+ TimeUnit.SECONDS.sleep(5);
+
+ assertThat(getShimApexManifest().getVersion()).isEqualTo(2L);
+ }
+
+ private ApexManifest getShimApexManifest() throws DeviceNotAvailableException,
+ InvalidProtocolBufferException {
+ String json = getDevice().executeShellCommand(
+ "cat /apex/com.android.apex.cts.shim/apex_manifest.json");
+ ApexManifest.Builder builder = ApexManifest.newBuilder();
+ JsonFormat.parser().merge(json, builder);
+ return builder.build();
+ }
+
+ private boolean hasNonFlattenedApex() throws Exception {
+ return "true".equals(getDevice().getProperty("ro.apex.updatable"));
+ }
+}