Add fuzz test for libsnapshot
Bug: 154633114
Test: source fuzz.sh && run_snapshot_fuzz_all -runs=100000
Change-Id: I5cd9e3f088ca283d3a49959c38aa74a483931f2c
diff --git a/fs_mgr/libsnapshot/Android.bp b/fs_mgr/libsnapshot/Android.bp
index 384595d..705d296 100644
--- a/fs_mgr/libsnapshot/Android.bp
+++ b/fs_mgr/libsnapshot/Android.bp
@@ -100,6 +100,7 @@
cc_library_static {
name: "libsnapshot_init",
+ native_coverage : true,
defaults: ["libsnapshot_defaults"],
srcs: [":libsnapshot_sources"],
recovery_available: true,
@@ -244,3 +245,43 @@
],
gtest: false,
}
+
+cc_fuzz {
+ name: "libsnapshot_fuzzer",
+
+ // TODO(b/154633114): make host supported.
+ // host_supported: true,
+
+ native_coverage : true,
+ srcs: [
+ "snapshot_fuzz.cpp",
+ "snapshot_fuzz_utils.cpp",
+ "fuzz_utils.cpp",
+ ],
+ static_libs: [
+ "libbase",
+ "libcrypto_static",
+ "libcutils",
+ "libfs_mgr",
+ "libgtest", // from libsnapshot_test_helpers
+ "libgmock", // from libsnapshot_test_helpers
+ "liblog",
+ "liblp",
+ "libsnapshot_init", // don't use binder or hwbinder
+ "libsnapshot_test_helpers",
+ "libprotobuf-cpp-lite",
+ "update_metadata-protos",
+ ],
+ header_libs: [
+ "libstorage_literals_headers",
+ ],
+
+ fuzz_config: {
+ cc: ["android-virtual-ab+bugs@google.com"],
+ componentid: 30545,
+ hotlists: ["1646452"],
+ fuzz_on_haiku_host: false,
+ // TODO(b/154633114): set to true to run this automatically.
+ fuzz_on_haiku_device: false,
+ },
+}
diff --git a/fs_mgr/libsnapshot/fuzz.sh b/fs_mgr/libsnapshot/fuzz.sh
new file mode 100755
index 0000000..64d8224
--- /dev/null
+++ b/fs_mgr/libsnapshot/fuzz.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+PROJECT_PATH=system/core/fs_mgr/libsnapshot
+FUZZ_TARGET=libsnapshot_fuzzer
+TARGET_ARCH=$(get_build_var TARGET_ARCH)
+FUZZ_BINARY=/data/fuzz/${TARGET_ARCH}/${FUZZ_TARGET}/${FUZZ_TARGET}
+DEVICE_CORPSE_DIR=/data/local/tmp/${FUZZ_TARGET}
+DEVICE_GCOV_DIR=/data/local/tmp/${FUZZ_TARGET}/gcov
+HOST_SCRATCH_DIR=/tmp/${FUZZ_TARGET}
+GCOV_TOOL=${HOST_SCRATCH_DIR}/llvm-gcov
+
+build_normal() (
+ pushd $(gettop)
+ NATIVE_COVERAGE="" NATIVE_LINE_COVERAGE="" COVERAGE_PATHS="" m ${FUZZ_TARGET}
+ ret=$?
+ popd
+ return ${ret}
+)
+
+build_cov() {
+ pushd $(gettop)
+ ret=$?
+ NATIVE_COVERAGE="true" NATIVE_LINE_COVERAGE="true" COVERAGE_PATHS="${PROJECT_PATH}" m ${FUZZ_TARGET}
+ popd
+ return ${ret}
+}
+
+prepare_device() {
+ adb root && adb remount &&
+ adb shell mkdir -p ${DEVICE_CORPSE_DIR} &&
+ adb shell rm -rf ${DEVICE_GCOV_DIR} &&
+ adb shell mkdir -p ${DEVICE_GCOV_DIR}
+}
+
+push_binary() {
+ adb push ${ANDROID_PRODUCT_OUT}/${FUZZ_BINARY} ${FUZZ_BINARY}
+}
+
+prepare_host() {
+ which lcov || {
+ echo "please run:";
+ echo " sudo apt-get install lcov ";
+ return 1;
+ }
+ rm -rf ${HOST_SCRATCH_DIR} &&
+ mkdir -p ${HOST_SCRATCH_DIR}
+}
+
+# run_snapshot_fuzz -runs=10000
+generate_corpse() {
+ [[ "$@" ]] || { echo "run with -runs=X"; return 1; }
+
+ prepare_device &&
+ build_normal &&
+ push_binary &&
+ adb shell ${FUZZ_BINARY} "$@" ${DEVICE_CORPSE_DIR}
+}
+
+run_snapshot_fuzz() {
+ prepare_device &&
+ build_cov &&
+ push_binary &&
+ adb shell GCOV_PREFIX=${DEVICE_GCOV_DIR} GCOV_PREFIX_STRIP=3 \
+ ${FUZZ_BINARY} \
+ -runs=0 \
+ ${DEVICE_CORPSE_DIR}
+}
+
+show_fuzz_result() {
+ prepare_host &&
+ unzip -o -j -d ${HOST_SCRATCH_DIR} ${ANDROID_PRODUCT_OUT}/coverage/data/fuzz/${TARGET_ARCH}/${FUZZ_TARGET}/${FUZZ_TARGET}.zip &&
+ adb shell find ${DEVICE_GCOV_DIR} -type f | xargs -I {} adb pull {} ${HOST_SCRATCH_DIR} &&
+ ls ${HOST_SCRATCH_DIR} &&
+ cat > ${GCOV_TOOL} <<< '
+#!/bin/bash
+exec llvm-cov gcov "$@"
+' &&
+ chmod +x ${GCOV_TOOL} &&
+ lcov --directory ${HOST_SCRATCH_DIR} --base-directory $(gettop) --gcov-tool ${GCOV_TOOL} --capture -o ${HOST_SCRATCH_DIR}/report.cov &&
+ genhtml ${HOST_SCRATCH_DIR}/report.cov -o ${HOST_SCRATCH_DIR}/html &&
+ echo file://$(realpath ${HOST_SCRATCH_DIR}/html/index.html)
+}
+
+# run_snapshot_fuzz -runs=10000
+run_snapshot_fuzz_all() {
+ generate_corpse "$@" &&
+ run_snapshot_fuzz &&
+ show_fuzz_result
+}
diff --git a/fs_mgr/libsnapshot/fuzz_utils.cpp b/fs_mgr/libsnapshot/fuzz_utils.cpp
new file mode 100644
index 0000000..509eb1b
--- /dev/null
+++ b/fs_mgr/libsnapshot/fuzz_utils.cpp
@@ -0,0 +1,25 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fuzz_utils.h"
+
+#include <android-base/logging.h>
+
+namespace android::fuzz {
+
+void CheckInternal(bool value, std::string_view msg) {
+ CHECK(value) << msg;
+}
+
+} // namespace android::fuzz
diff --git a/fs_mgr/libsnapshot/fuzz_utils.h b/fs_mgr/libsnapshot/fuzz_utils.h
new file mode 100644
index 0000000..4e14b9c
--- /dev/null
+++ b/fs_mgr/libsnapshot/fuzz_utils.h
@@ -0,0 +1,267 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+// Generic classes for fuzzing a collection of APIs.
+
+namespace android::fuzz {
+
+// My custom boolean type -- to avoid conflict with (u)int8_t and char.
+struct Bool {
+ bool value;
+ operator bool() const { return value; }
+};
+
+// Helper for FuzzData.
+// A wrapper over an optional const object T. The buffer is maintained elsewhere.
+template <typename T>
+class Optional {
+ public:
+ Optional(const T* ptr) : ptr_(ptr) {}
+ const T& operator*() const { return *ptr_; }
+ const T& value() const { return *ptr_; }
+ bool has_value() const { return ptr_; }
+
+ private:
+ const T* ptr_;
+};
+
+// Helper for FuzzData.
+// A wrapper over an optional boolean. The boolean is owned by this object.
+template <>
+class Optional<Bool> {
+ public:
+ Optional(std::optional<Bool>&& val) : val_(std::move(val)) {}
+ const Bool& operator*() const { return *val_; }
+ const Bool& value() const { return val_.value(); }
+ bool has_value() const { return val_.has_value(); }
+
+ private:
+ std::optional<Bool> val_;
+};
+
+// Helper for FuzzData.
+// A view on a raw data buffer. Client is responsible for maintaining the lifetime of the data
+// buffer.
+class DataView {
+ public:
+ DataView(const uint8_t* data, uint64_t size) : data_(data), size_(size) {}
+ DataView(const void* data, uint64_t size) : DataView(static_cast<const uint8_t*>(data), size) {}
+ inline uint64_t size() const { return size_; }
+ inline const uint8_t* data() const { return data_; }
+ inline bool CanConsume(uint64_t size) { return size_ >= size; }
+ // Consume the first |size| bytes from |this| and return a DataView object that represents
+ // the consumed data. Data pointer in |this| is incremented by |size| bytes.
+ // If not enough bytes, return nullopt.
+ std::optional<DataView> Consume(uint64_t size) {
+ if (!CanConsume(size)) return std::nullopt;
+ DataView ret(data_, size);
+ size_ -= size;
+ data_ += size;
+ return ret;
+ }
+
+ private:
+ const uint8_t* data_;
+ uint64_t size_;
+};
+
+// A view on the fuzz data. Provides APIs to consume typed objects.
+class FuzzData : public DataView {
+ public:
+ // Inherit constructors.
+ using DataView::DataView;
+ // Consume a data object T and return the pointer (into the buffer). No copy is done.
+ // If not enough bytes, return nullptr.
+ template <typename T>
+ inline Optional<T> Consume() {
+ auto data_view = DataView::Consume(sizeof(T));
+ if (!data_view.has_value()) return nullptr;
+ return reinterpret_cast<const T*>(data_view->data());
+ }
+ // To provide enough entropy for booleans, they are consumed bit by bit.
+ // Hence, the returned value is not indexed into the buffer. See Optional<Bool>.
+ template <>
+ Optional<Bool> Consume<Bool>() {
+ if (!boolean_buffer_.has_value() || boolean_bit_offset_ >= sizeof(*boolean_buffer_)) {
+ boolean_buffer_ = Consume<uint8_t>();
+ boolean_bit_offset_ = 0;
+ }
+ if (!boolean_buffer_.has_value()) {
+ return Optional<Bool>(std::nullopt);
+ }
+ const auto& byte = *boolean_buffer_;
+ bool ret = (byte >> boolean_bit_offset_) & 0x1;
+ boolean_bit_offset_++;
+ return Optional<Bool>(Bool{ret});
+ }
+
+ private:
+ // Separate buffer for booleans.
+ Optional<uint8_t> boolean_buffer_ = nullptr;
+ uint8_t boolean_bit_offset_ = 0;
+};
+
+enum class CallResult {
+ SUCCESS,
+ NOT_ENOUGH_DATA,
+};
+
+inline bool AllArgsHasValue() {
+ return true;
+}
+template <typename T>
+inline bool AllArgsHasValue(const Optional<T>& t) {
+ return t.has_value();
+}
+template <typename First, typename... Remaining>
+inline bool AllArgsHasValue(const Optional<First>& first, const Optional<Remaining>&... remaining) {
+ return first.has_value() && AllArgsHasValue(remaining...);
+}
+
+// Base class of FuzzFunction.
+class FuzzFunctionBase {
+ public:
+ virtual ~FuzzFunctionBase() = default;
+ virtual CallResult Call(FuzzData* fuzz_data) const = 0;
+};
+
+template <typename T>
+class FuzzFunction; // undefined
+
+// A wrapper over a fuzzed function.
+template <typename R, typename... Args>
+class FuzzFunction<R(Args...)> : public FuzzFunctionBase {
+ public:
+ using Function = std::function<R(Args...)>;
+ FuzzFunction(Function&& function) : function_(std::move(function)) {}
+ // Eat necessary data in |fuzz_data| and invoke the function.
+ CallResult Call(FuzzData* fuzz_data) const override {
+ return CallWithOptionalArgs(fuzz_data->Consume<std::remove_reference_t<Args>>()...);
+ }
+
+ private:
+ Function function_;
+
+ CallResult CallWithOptionalArgs(const Optional<std::remove_reference_t<Args>>&... args) const {
+ if (!AllArgsHasValue(args...)) {
+ return CallResult::NOT_ENOUGH_DATA;
+ }
+ (void)function_(args.value()...); // ignore returned value
+ return CallResult::SUCCESS;
+ }
+};
+
+// CHECK(value) << msg
+void CheckInternal(bool value, std::string_view msg);
+
+// A collection of FuzzFunction's.
+// FunctionsSizeType must be an integral type where
+// functions_.size() <= std::numeric_limits<FunctionSizeType>::max().
+template <typename FunctionsSizeType>
+class FuzzFunctions {
+ public:
+ // Subclass should override this to register functions via AddFunction.
+ FuzzFunctions() = default;
+ virtual ~FuzzFunctions() = default;
+ // Eat some amount of data in |fuzz_data| and call one of the |functions_|.
+ CallResult CallOne(FuzzData* fuzz_data) const {
+ auto opt_number = fuzz_data->Consume<FunctionsSizeType>();
+ if (!opt_number.has_value()) {
+ return CallResult::NOT_ENOUGH_DATA;
+ }
+ auto function_index = opt_number.value() % functions_.size();
+ return functions_[function_index]->Call(fuzz_data);
+ }
+
+ private:
+ template <typename R, typename... Args>
+ struct FunctionTraits {
+ using Function = std::function<R(Args...)>;
+ };
+
+ public:
+ // There are no deduction guide from lambda to std::function, so the
+ // signature of the lambda must be specified in the template argument.
+ // FuzzFunctions provide the following 3 ways to specify the signature of
+ // the lambda:
+
+ // AddFunction<R(Args...)>, e.g. AddFunction<ReturnType(ArgType, ArgType)>
+ template <typename T>
+ void AddFunction(std::function<T>&& func) {
+ functions_.push_back(std::make_unique<FuzzFunction<T>>(std::move(func)));
+ }
+
+ // AddFunction<R, Args...>, e.g. AddFunction<ReturnType, ArgType, ArgType>
+ template <typename R, typename... Args>
+ void AddFunction(typename FunctionTraits<R, Args...>::Function&& func) {
+ functions_.push_back(std::make_unique<FuzzFunction<R(Args...)>>(std::move(func)));
+ }
+
+ // AddFunction<ArgType...>. Equivalent to AddFunction<void, Args...>
+ template <typename... Args>
+ void AddFunction(typename FunctionTraits<void, Args...>::Function&& func) {
+ functions_.push_back(std::make_unique<FuzzFunction<void(Args...)>>(std::move(func)));
+ }
+
+ // Use |fuzz_data| as a guide to call |functions_| until |fuzz_data| is
+ // depleted. Return
+ void DepleteData(FuzzData* fuzz_data) const {
+ CallResult result;
+ while ((result = CallOne(fuzz_data)) == CallResult::SUCCESS)
+ ;
+ CheckInternal(result == CallResult::NOT_ENOUGH_DATA,
+ "result is " + std::to_string(static_cast<int>(result)));
+ }
+
+ protected:
+ // Helper for subclass to check that size of |functions_| is actually within
+ // SizeType. Should be called after all functions are registered.
+ void CheckFunctionsSize() const {
+ CheckInternal(functions_.size() <= std::numeric_limits<FunctionsSizeType>::max(),
+ "Need to extend number of bits for function count; there are " +
+ std::to_string(functions_.size()) + " functions now.");
+ }
+
+ private:
+ std::vector<std::unique_ptr<FuzzFunctionBase>> functions_;
+};
+
+// An object whose APIs are being fuzzed.
+template <typename T, typename FunctionsSizeType>
+class FuzzObject : public FuzzFunctions<FunctionsSizeType> {
+ public:
+ // Not thread-safe; client is responsible for ensuring only one thread calls DepleteData.
+ void DepleteData(T* obj, FuzzData* fuzz_data) {
+ obj_ = obj;
+ FuzzFunctions<FunctionsSizeType>::DepleteData(fuzz_data);
+ obj_ = nullptr;
+ }
+
+ protected:
+ // Helper for subclass to get the module under test in the added functions.
+ T* get() const {
+ CheckInternal(obj_ != nullptr, "No module under test is found.");
+ return obj_;
+ }
+
+ private:
+ T* obj_ = nullptr;
+};
+
+} // namespace android::fuzz
diff --git a/fs_mgr/libsnapshot/include/libsnapshot/snapshot.h b/fs_mgr/libsnapshot/include/libsnapshot/snapshot.h
index fff667e..8081866 100644
--- a/fs_mgr/libsnapshot/include/libsnapshot/snapshot.h
+++ b/fs_mgr/libsnapshot/include/libsnapshot/snapshot.h
@@ -318,6 +318,7 @@
friend class SnapshotUpdateTest;
friend class FlashAfterUpdateTest;
friend class LockTestConsumer;
+ friend class SnapshotFuzzEnv;
friend struct AutoDeleteCowImage;
friend struct AutoDeleteSnapshot;
friend struct PartitionCowCreator;
diff --git a/fs_mgr/libsnapshot/snapshot_fuzz.cpp b/fs_mgr/libsnapshot/snapshot_fuzz.cpp
new file mode 100644
index 0000000..12a0531
--- /dev/null
+++ b/fs_mgr/libsnapshot/snapshot_fuzz.cpp
@@ -0,0 +1,141 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <stddef.h>
+#include <stdint.h>
+#include <sysexits.h>
+
+#include <functional>
+#include <sstream>
+#include <tuple>
+
+#include <android-base/logging.h>
+#include <storage_literals/storage_literals.h>
+
+#include "fuzz_utils.h"
+#include "snapshot_fuzz_utils.h"
+
+using android::base::LogId;
+using android::base::LogSeverity;
+using android::base::SetLogger;
+using android::base::StderrLogger;
+using android::base::StdioLogger;
+using android::fuzz::Bool;
+using android::fuzz::FuzzData;
+using android::fuzz::FuzzObject;
+using android::snapshot::SnapshotFuzzEnv;
+using android::snapshot::SnapshotManagerFuzzData;
+
+// Avoid linking to libgsi since it needs disk I/O.
+namespace android::gsi {
+bool IsGsiRunning() {
+ LOG(FATAL) << "Called IsGsiRunning";
+ __builtin_unreachable();
+}
+std::string GetDsuSlot(const std::string& install_dir) {
+ LOG(FATAL) << "Called GetDsuSlot(" << install_dir << ")";
+ __builtin_unreachable();
+}
+} // namespace android::gsi
+
+namespace android::snapshot {
+
+class FuzzSnapshotManager : public FuzzObject<ISnapshotManager, uint8_t> {
+ public:
+ FuzzSnapshotManager();
+};
+
+FuzzSnapshotManager::FuzzSnapshotManager() {
+ AddFunction([this]() { (void)get()->BeginUpdate(); });
+ AddFunction([this]() { (void)get()->CancelUpdate(); });
+ AddFunction<Bool>([this](Bool wipe) { (void)get()->FinishedSnapshotWrites(wipe); });
+ AddFunction([this]() { (void)get()->InitiateMerge(); });
+ AddFunction<Bool, Bool>([this](auto has_before_cancel, auto fail_before_cancel) {
+ std::function<bool()> before_cancel;
+ if (has_before_cancel) {
+ before_cancel = [=]() { return fail_before_cancel; };
+ }
+ (void)get()->ProcessUpdateState({}, before_cancel);
+ });
+ AddFunction<Bool>([this](auto has_progress_arg) {
+ double progress;
+ (void)get()->GetUpdateState(has_progress_arg ? &progress : nullptr);
+ });
+ // TODO add CreateUpdateSnapshots according to proto
+ // TODO add MapUpdateSnapshot
+ // TODO add UnmapUpdateSnapshot using names from the dictionary
+ AddFunction([this]() { (void)get()->NeedSnapshotsInFirstStageMount(); });
+ // TODO add CreateLogicalAndSnapshotPartitions
+ AddFunction<Bool>([this](const Bool& has_callback) {
+ std::function<void()> callback;
+ if (has_callback) {
+ callback = []() {};
+ }
+ (void)get()->HandleImminentDataWipe(callback);
+ });
+ AddFunction([this]() { (void)get()->RecoveryCreateSnapshotDevices(); });
+ // TODO add RecoveryCreateSnapshotDevices with metadata_device arg
+ AddFunction([this]() {
+ std::stringstream ss;
+ (void)get()->Dump(ss);
+ });
+ AddFunction([this]() { (void)get()->EnsureMetadataMounted(); });
+ AddFunction([this]() { (void)get()->GetSnapshotMergeStatsInstance(); });
+
+ CheckFunctionsSize();
+}
+
+// During global init, log all messages to stdio. This is only done once.
+int AllowLoggingDuringGlobalInit() {
+ SetLogger(&StdioLogger);
+ return 0;
+}
+
+// Only log fatal messages during tests.
+void FatalOnlyLogger(LogId logid, LogSeverity severity, const char* tag, const char* file,
+ unsigned int line, const char* message) {
+ if (severity == LogSeverity::FATAL) {
+ StderrLogger(logid, severity, tag, file, line, message);
+ }
+}
+// Stop logging (except fatal messages) after global initialization. This is only done once.
+int StopLoggingAfterGlobalInit() {
+ SetLogger(&FatalOnlyLogger);
+ return 0;
+}
+
+} // namespace android::snapshot
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ using namespace android::snapshot;
+
+ [[maybe_unused]] static auto allow_logging = AllowLoggingDuringGlobalInit();
+ static SnapshotFuzzEnv env;
+ static FuzzSnapshotManager fuzz_snapshot_manager;
+ [[maybe_unused]] static auto stop_logging = StopLoggingAfterGlobalInit();
+
+ CHECK(env.InitOk());
+ FuzzData fuzz_data(data, size);
+
+ auto snapshot_manager_data = fuzz_data.Consume<SnapshotManagerFuzzData>();
+ if (!snapshot_manager_data.has_value()) {
+ return 0;
+ }
+ auto snapshot_manager = env.CreateSnapshotManager(snapshot_manager_data.value());
+ CHECK(snapshot_manager);
+
+ fuzz_snapshot_manager.DepleteData(snapshot_manager.get(), &fuzz_data);
+
+ return 0;
+}
diff --git a/fs_mgr/libsnapshot/snapshot_fuzz_utils.cpp b/fs_mgr/libsnapshot/snapshot_fuzz_utils.cpp
new file mode 100644
index 0000000..d5e3e96
--- /dev/null
+++ b/fs_mgr/libsnapshot/snapshot_fuzz_utils.cpp
@@ -0,0 +1,227 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <ftw.h>
+#include <inttypes.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sysexits.h>
+
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <libsnapshot/auto_device.h>
+#include <libsnapshot/snapshot.h>
+#include <storage_literals/storage_literals.h>
+
+#include "snapshot_fuzz_utils.h"
+
+using namespace android::storage_literals;
+using namespace std::string_literals;
+
+using android::base::StringPrintf;
+using android::base::unique_fd;
+using android::base::WriteStringToFile;
+using android::fiemap::IImageManager;
+using android::fiemap::ImageManager;
+
+static const char MNT_DIR[] = "/mnt";
+static const char FAKE_ROOT_NAME[] = "snapshot_fuzz";
+static const auto SUPER_IMAGE_SIZE = 16_MiB;
+static const auto FAKE_ROOT_SIZE = 64_MiB;
+
+namespace android::snapshot {
+
+bool Mkdir(const std::string& path) {
+ if (mkdir(path.c_str(), 0750) == -1 && errno != EEXIST) {
+ PLOG(ERROR) << "Cannot create " << path;
+ return false;
+ }
+ return true;
+}
+
+bool RmdirRecursive(const std::string& path) {
+ auto callback = [](const char* child, const struct stat*, int file_type, struct FTW*) -> int {
+ switch (file_type) {
+ case FTW_D:
+ case FTW_DP:
+ case FTW_DNR:
+ if (rmdir(child) == -1) {
+ PLOG(ERROR) << "rmdir " << child;
+ return -1;
+ }
+ return 0;
+ case FTW_NS:
+ default:
+ if (rmdir(child) != -1) break;
+ [[fallthrough]];
+ case FTW_F:
+ case FTW_SL:
+ case FTW_SLN:
+ if (unlink(child) == -1) {
+ PLOG(ERROR) << "unlink " << child;
+ return -1;
+ }
+ return 0;
+ }
+ return 0;
+ };
+
+ return nftw(path.c_str(), callback, 128, FTW_DEPTH | FTW_MOUNT | FTW_PHYS) == 0;
+}
+
+class AutoDeleteDir : public AutoDevice {
+ public:
+ static std::unique_ptr<AutoDeleteDir> New(const std::string& path) {
+ if (!Mkdir(path)) {
+ return std::unique_ptr<AutoDeleteDir>(new AutoDeleteDir(""));
+ }
+ return std::unique_ptr<AutoDeleteDir>(new AutoDeleteDir(path));
+ }
+ ~AutoDeleteDir() {
+ if (!HasDevice()) return;
+ if (rmdir(name_.c_str()) == -1) {
+ PLOG(ERROR) << "Cannot remove " << name_;
+ }
+ }
+
+ private:
+ AutoDeleteDir(const std::string& path) : AutoDevice(path) {}
+};
+
+class AutoUnmount : public AutoDevice {
+ public:
+ static std::unique_ptr<AutoUnmount> New(const std::string& path, uint64_t size) {
+ if (mount("tmpfs", path.c_str(), "tmpfs", 0,
+ (void*)StringPrintf("size=%" PRIu64, size).data()) == -1) {
+ PLOG(ERROR) << "Cannot mount " << path;
+ return std::unique_ptr<AutoUnmount>(new AutoUnmount(""));
+ }
+ return std::unique_ptr<AutoUnmount>(new AutoUnmount(path));
+ }
+ ~AutoUnmount() {
+ if (!HasDevice()) return;
+ if (umount(name_.c_str()) == -1) {
+ PLOG(ERROR) << "Cannot umount " << name_;
+ }
+ }
+
+ private:
+ AutoUnmount(const std::string& path) : AutoDevice(path) {}
+};
+
+// A directory on tmpfs. Upon destruct, it is unmounted and deleted.
+class AutoMemBasedDir : public AutoDevice {
+ public:
+ static std::unique_ptr<AutoMemBasedDir> New(const std::string& name, uint64_t size) {
+ auto ret = std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(name));
+ ret->auto_delete_mount_dir_ = AutoDeleteDir::New(ret->mount_path());
+ if (!ret->auto_delete_mount_dir_->HasDevice()) {
+ return std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(""));
+ }
+ ret->auto_umount_mount_point_ = AutoUnmount::New(ret->mount_path(), size);
+ if (!ret->auto_umount_mount_point_->HasDevice()) {
+ return std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(""));
+ }
+ // path() does not need to be deleted upon destruction, hence it is not wrapped with
+ // AutoDeleteDir.
+ if (!Mkdir(ret->path())) {
+ return std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(""));
+ }
+ return ret;
+ }
+ // Return the scratch directory.
+ std::string path() const {
+ CHECK(HasDevice());
+ return mount_path() + "/root";
+ }
+ // Delete all contents in path() and start over. path() itself is re-created.
+ bool SoftReset() { return RmdirRecursive(path()) && Mkdir(path()); }
+
+ private:
+ AutoMemBasedDir(const std::string& name) : AutoDevice(name) {}
+ std::string mount_path() const {
+ CHECK(HasDevice());
+ return MNT_DIR + "/"s + name_;
+ }
+ std::unique_ptr<AutoDeleteDir> auto_delete_mount_dir_;
+ std::unique_ptr<AutoUnmount> auto_umount_mount_point_;
+};
+
+SnapshotFuzzEnv::SnapshotFuzzEnv() {
+ fake_root_ = AutoMemBasedDir::New(FAKE_ROOT_NAME, FAKE_ROOT_SIZE);
+}
+
+SnapshotFuzzEnv::~SnapshotFuzzEnv() = default;
+
+bool SnapshotFuzzEnv::InitOk() const {
+ if (fake_root_ == nullptr || !fake_root_->HasDevice()) return false;
+ return true;
+}
+
+bool SnapshotFuzzEnv::SoftReset() {
+ return fake_root_->SoftReset();
+}
+
+std::unique_ptr<IImageManager> SnapshotFuzzEnv::CreateFakeImageManager(
+ const std::string& fake_root) {
+ auto images_dir = fake_root + "/images";
+ auto metadata_dir = images_dir + "/metadata";
+ auto data_dir = images_dir + "/data";
+
+ if (!Mkdir(images_dir) || !Mkdir(metadata_dir) || !Mkdir(data_dir)) {
+ return nullptr;
+ }
+ return ImageManager::Open(metadata_dir, data_dir);
+}
+
+std::unique_ptr<TestPartitionOpener> SnapshotFuzzEnv::CreatePartitionOpener(
+ const std::string& fake_root) {
+ auto fake_super = fake_root + "/super.img";
+ std::string zeros(SUPER_IMAGE_SIZE, '\0');
+
+ if (!WriteStringToFile(zeros, fake_super)) {
+ PLOG(ERROR) << "Cannot write zeros to " << fake_super;
+ return nullptr;
+ }
+
+ return std::make_unique<TestPartitionOpener>(fake_super);
+}
+
+std::string SnapshotFuzzEnv::root() const {
+ CHECK(InitOk());
+ return fake_root_->path();
+}
+
+std::unique_ptr<ISnapshotManager> SnapshotFuzzEnv::CreateSnapshotManager(
+ const SnapshotManagerFuzzData& data) {
+ // TODO(b/154633114): create valid super partition according to fuzz data
+ auto partition_opener = CreatePartitionOpener(root());
+ if (partition_opener == nullptr) return nullptr;
+ auto metadata_dir = root() + "/snapshot_metadata";
+ if (!Mkdir(metadata_dir)) return nullptr;
+
+ auto device_info = new SnapshotFuzzDeviceInfo(data.device_info_data,
+ std::move(partition_opener), metadata_dir);
+ auto snapshot = SnapshotManager::New(device_info /* takes ownership */);
+ snapshot->images_ = CreateFakeImageManager(root());
+ snapshot->has_local_image_manager_ = data.is_local_image_manager;
+
+ return snapshot;
+}
+
+} // namespace android::snapshot
diff --git a/fs_mgr/libsnapshot/snapshot_fuzz_utils.h b/fs_mgr/libsnapshot/snapshot_fuzz_utils.h
new file mode 100644
index 0000000..32910a9
--- /dev/null
+++ b/fs_mgr/libsnapshot/snapshot_fuzz_utils.h
@@ -0,0 +1,115 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <libfiemap/image_manager.h>
+#include <libsnapshot/auto_device.h>
+#include <libsnapshot/test_helpers.h>
+
+// libsnapshot-specific code for fuzzing. Defines fake classes that are depended
+// by SnapshotManager.
+
+namespace android::snapshot {
+
+// Controls the behavior of IDeviceInfo.
+typedef struct SnapshotFuzzDeviceInfoData {
+ bool slot_suffix_is_a : 1;
+ bool is_overlayfs_setup : 1;
+ bool allow_set_boot_control_merge_status : 1;
+ bool allow_set_slot_as_unbootable : 1;
+ bool is_recovery : 1;
+} __attribute__((packed)) SnapshotFuzzDeviceInfoData;
+
+// Controls the behavior of the test SnapshotManager.
+typedef struct SnapshotManagerFuzzData {
+ SnapshotFuzzDeviceInfoData device_info_data;
+ bool is_local_image_manager : 1;
+} __attribute__((packed)) SnapshotManagerFuzzData;
+
+class AutoMemBasedDir;
+
+// Prepare test environment. This has a heavy overhead and should be done once.
+class SnapshotFuzzEnv {
+ public:
+ // Check if test should run at all.
+ static bool ShouldSkipTest();
+
+ // Initialize the environment.
+ SnapshotFuzzEnv();
+ ~SnapshotFuzzEnv();
+
+ // Check if environment is initialized properly.
+ bool InitOk() const;
+
+ // A scratch directory for the test to play around with. The scratch directory
+ // is backed by tmpfs. SoftReset() clears the directory.
+ std::string root() const;
+
+ // Soft reset part of the environment before running the next test.
+ bool SoftReset();
+
+ // Create a snapshot manager for this test run.
+ // Client is responsible for maintaining the lifetime of |data| over the life time of
+ // ISnapshotManager.
+ std::unique_ptr<ISnapshotManager> CreateSnapshotManager(const SnapshotManagerFuzzData& data);
+
+ private:
+ std::unique_ptr<AutoMemBasedDir> fake_root_;
+
+ static std::unique_ptr<android::fiemap::IImageManager> CreateFakeImageManager(
+ const std::string& fake_root);
+ static std::unique_ptr<TestPartitionOpener> CreatePartitionOpener(const std::string& fake_root);
+};
+
+class SnapshotFuzzDeviceInfo : public ISnapshotManager::IDeviceInfo {
+ public:
+ // Client is responsible for maintaining the lifetime of |data|.
+ SnapshotFuzzDeviceInfo(const SnapshotFuzzDeviceInfoData& data,
+ std::unique_ptr<TestPartitionOpener>&& partition_opener,
+ const std::string& metadata_dir)
+ : data_(data),
+ partition_opener_(std::move(partition_opener)),
+ metadata_dir_(metadata_dir) {}
+
+ // Following APIs are mocked.
+ std::string GetGsidDir() const override { return "fuzz_ota"; }
+ std::string GetMetadataDir() const override { return metadata_dir_; }
+ std::string GetSuperDevice(uint32_t) const override {
+ // TestPartitionOpener can recognize this.
+ return "super";
+ }
+ const android::fs_mgr::IPartitionOpener& GetPartitionOpener() const override {
+ return *partition_opener_;
+ }
+
+ // Following APIs are fuzzed.
+ std::string GetSlotSuffix() const override { return data_.slot_suffix_is_a ? "_a" : "_b"; }
+ std::string GetOtherSlotSuffix() const override { return data_.slot_suffix_is_a ? "_b" : "_a"; }
+ bool IsOverlayfsSetup() const override { return data_.is_overlayfs_setup; }
+ bool SetBootControlMergeStatus(android::hardware::boot::V1_1::MergeStatus) override {
+ return data_.allow_set_boot_control_merge_status;
+ }
+ bool SetSlotAsUnbootable(unsigned int) override { return data_.allow_set_slot_as_unbootable; }
+ bool IsRecovery() const override { return data_.is_recovery; }
+
+ private:
+ SnapshotFuzzDeviceInfoData data_;
+ std::unique_ptr<TestPartitionOpener> partition_opener_;
+ std::string metadata_dir_;
+};
+
+} // namespace android::snapshot