Support updateable groups.

Adds updateable group support to OTA.

* DeltaPerformer combines partition sizes with
  dynamic_partition_metadata to
  BootControlInterface::PartitionMetadata.

* BootControlAndroid::InitPartitionMetadata:
    * Copy all groups / partitions from source metadata slot
    * Remove all groups / partitions mentioned in the manifest (of the
    target slot)
    * Re-add all groups / partitions mentioned in the manifest.

* BootControlAndroid::InitPartitionMetadata can check
  the incoming PartitionMetadata to see if a partition is dynamic
  or not. The guessing logic is completely removed.

* Because a partition is removed then re-added, there is no need
  for preserving the entry with size 0 to indicate that a partition
  is removed. When update_engine sees a partition in a group "foo" on
  the device, but manifest contains group "foo" without the partition,
  it removes the partition.

* Hence, Removing a partition does NOT require keeping the entry (i.e.
  RemovePartition is used instead of ShrinkPartition(0) ). This makes
  retrofitting dynamic partitions on older devices easier.

The following is now allowed:
- Adding / removing / resizing partitions
- Adding / resizing groups

It is not allowed to remove a group, but a group can always be resized
to zero to deprecate it.

Test: update_engine_unittests
Bug: 117182932

Change-Id: I39d77f1d1d1fc52fc245f3de699635e6a429015e
diff --git a/boot_control_android.cc b/boot_control_android.cc
index 61711d2..8eafdce 100644
--- a/boot_control_android.cc
+++ b/boot_control_android.cc
@@ -18,9 +18,11 @@
 
 #include <memory>
 #include <utility>
+#include <vector>
 
 #include <base/bind.h>
 #include <base/logging.h>
+#include <base/strings/string_util.h>
 #include <bootloader_message/bootloader_message.h>
 #include <brillo/message_loops/message_loop.h>
 #include <fs_mgr.h>
@@ -31,9 +33,7 @@
 using std::string;
 
 using android::dm::DmDeviceState;
-using android::fs_mgr::MetadataBuilder;
 using android::fs_mgr::Partition;
-using android::fs_mgr::UpdatePartitionTable;
 using android::hardware::hidl_string;
 using android::hardware::Return;
 using android::hardware::boot::V1_0::BoolResult;
@@ -225,117 +225,103 @@
 
 namespace {
 
-// Resize |partition_name|_|slot| to the given |size|.
-bool ResizePartition(MetadataBuilder* builder,
-                     const string& target_partition_name,
-                     uint64_t size) {
-  Partition* partition = builder->FindPartition(target_partition_name);
-  if (partition == nullptr) {
-    LOG(ERROR) << "Cannot find " << target_partition_name << " in metadata.";
+bool InitPartitionMetadataInternal(
+    DynamicPartitionControlInterface* dynamic_control,
+    const string& super_device,
+    Slot source_slot,
+    Slot target_slot,
+    const string& target_suffix,
+    const PartitionMetadata& partition_metadata) {
+  auto builder =
+      dynamic_control->LoadMetadataBuilder(super_device, source_slot);
+  if (builder == nullptr) {
+    // TODO(elsk): allow reconstructing metadata from partition_metadata
+    // in recovery sideload.
+    LOG(ERROR) << "No metadata at "
+               << BootControlInterface::SlotName(source_slot);
     return false;
   }
 
-  uint64_t old_size = partition->size();
-  const string action = "resize " + target_partition_name + " in super (" +
-                        std::to_string(old_size) + " -> " +
-                        std::to_string(size) + " bytes)";
-  if (!builder->ResizePartition(partition, size)) {
-    LOG(ERROR) << "Cannot " << action << "; see previous log messages.";
+  std::vector<string> groups = builder->ListGroups();
+  for (const auto& group_name : groups) {
+    if (base::EndsWith(
+            group_name, target_suffix, base::CompareCase::SENSITIVE)) {
+      LOG(INFO) << "Removing group " << group_name;
+      builder->RemoveGroupAndPartitions(group_name);
+    }
+  }
+
+  uint64_t total_size = 0;
+  for (const auto& group : partition_metadata.groups) {
+    total_size += group.size;
+  }
+
+  if (total_size > (builder->AllocatableSpace() / 2)) {
+    LOG(ERROR)
+        << "The maximum size of all groups with suffix " << target_suffix
+        << " (" << total_size
+        << ") has exceeded half of allocatable space for dynamic partitions "
+        << (builder->AllocatableSpace() / 2) << ".";
     return false;
   }
 
-  if (partition->size() != size) {
-    LOG(ERROR) << "Cannot " << action
-               << "; value is misaligned and partition should have been "
-               << partition->size();
-    return false;
+  for (const auto& group : partition_metadata.groups) {
+    auto group_name_suffix = group.name + target_suffix;
+    if (!builder->AddGroup(group_name_suffix, group.size)) {
+      LOG(ERROR) << "Cannot add group " << group_name_suffix << " with size "
+                 << group.size;
+      return false;
+    }
+    LOG(INFO) << "Added group " << group_name_suffix << " with size "
+              << group.size;
+
+    for (const auto& partition : group.partitions) {
+      auto parition_name_suffix = partition.name + target_suffix;
+      Partition* p = builder->AddPartition(
+          parition_name_suffix, group_name_suffix, LP_PARTITION_ATTR_READONLY);
+      if (!p) {
+        LOG(ERROR) << "Cannot add partition " << parition_name_suffix
+                   << " to group " << group_name_suffix;
+        return false;
+      }
+      if (!builder->ResizePartition(p, partition.size)) {
+        LOG(ERROR) << "Cannot resize partition " << parition_name_suffix
+                   << " to size " << partition.size << ". Not enough space?";
+        return false;
+      }
+      LOG(INFO) << "Added partition " << parition_name_suffix << " to group "
+                << group_name_suffix << " with size " << partition.size;
+    }
   }
 
-  LOG(INFO) << "Successfully " << action;
-
-  return true;
+  return dynamic_control->StoreMetadata(
+      super_device, builder.get(), target_slot);
 }
 
-bool ResizePartitions(DynamicPartitionControlInterface* dynamic_control,
-                      const string& super_device,
-                      Slot target_slot,
-                      const string& target_suffix,
-                      const PartitionMetadata& logical_sizes,
-                      MetadataBuilder* builder) {
-  // Delete all extents to ensure that each partition has enough space to
-  // grow.
-  for (const auto& pair : logical_sizes) {
-    const string target_partition_name = pair.first + target_suffix;
-    if (builder->FindPartition(target_partition_name) == nullptr) {
-      // Use constant GUID because it is unused.
-      LOG(INFO) << "Adding partition " << target_partition_name << " to slot "
-                << BootControlInterface::SlotName(target_slot) << " in "
-                << super_device;
-      if (builder->AddPartition(target_partition_name,
-                                LP_PARTITION_ATTR_READONLY) == nullptr) {
-        LOG(ERROR) << "Cannot add partition " << target_partition_name;
+// Unmap all partitions, and remap partitions.
+bool Remap(DynamicPartitionControlInterface* dynamic_control,
+           const string& super_device,
+           Slot target_slot,
+           const string& target_suffix,
+           const PartitionMetadata& partition_metadata) {
+  for (const auto& group : partition_metadata.groups) {
+    for (const auto& partition : group.partitions) {
+      if (!dynamic_control->UnmapPartitionOnDeviceMapper(
+              partition.name + target_suffix, true /* wait */)) {
+        return false;
+      }
+      if (partition.size == 0) {
+        continue;
+      }
+      string map_path;
+      if (!dynamic_control->MapPartitionOnDeviceMapper(
+              super_device,
+              partition.name + target_suffix,
+              target_slot,
+              &map_path)) {
         return false;
       }
     }
-    if (!ResizePartition(builder, pair.first + target_suffix, 0 /* size */)) {
-      return false;
-    }
-  }
-
-  for (const auto& pair : logical_sizes) {
-    if (!ResizePartition(builder, pair.first + target_suffix, pair.second)) {
-      LOG(ERROR) << "Not enough space?";
-      return false;
-    }
-  }
-
-  if (!dynamic_control->StoreMetadata(super_device, builder, target_slot)) {
-    return false;
-  }
-  return true;
-}
-
-// Assume upgrading from slot A to B. A partition foo is considered dynamic
-// iff one of the following:
-// 1. foo_a exists as a dynamic partition (so it should continue to be a
-//    dynamic partition)
-// 2. foo_b does not exist as a static partition (in which case we may be
-//    adding a new partition).
-bool IsDynamicPartition(DynamicPartitionControlInterface* dynamic_control,
-                        const base::FilePath& device_dir,
-                        MetadataBuilder* source_metadata,
-                        const string& partition_name,
-                        const string& source_suffix,
-                        const string& target_suffix) {
-  bool dynamic_source_exist =
-      source_metadata->FindPartition(partition_name + source_suffix) != nullptr;
-  bool static_target_exist = dynamic_control->DeviceExists(
-      device_dir.Append(partition_name + target_suffix).value());
-
-  return dynamic_source_exist || !static_target_exist;
-}
-
-bool FilterPartitionSizes(DynamicPartitionControlInterface* dynamic_control,
-                          const base::FilePath& device_dir,
-                          const PartitionMetadata& partition_metadata,
-                          MetadataBuilder* source_metadata,
-                          const string& source_suffix,
-                          const string& target_suffix,
-                          PartitionMetadata* logical_sizes) {
-  for (const auto& pair : partition_metadata) {
-    if (!IsDynamicPartition(dynamic_control,
-                            device_dir,
-                            source_metadata,
-                            pair.first,
-                            source_suffix,
-                            target_suffix)) {
-      // In the future we can check static partition sizes, but skip for now.
-      LOG(INFO) << pair.first << " is static; assume its size is "
-                << pair.second << " bytes.";
-      continue;
-    }
-
-    logical_sizes->insert(pair);
   }
   return true;
 }
@@ -362,60 +348,28 @@
     return false;
   }
 
-  string current_suffix;
-  if (!GetSuffix(current_slot, &current_suffix)) {
-    return false;
-  }
-
   string target_suffix;
   if (!GetSuffix(target_slot, &target_suffix)) {
     return false;
   }
 
-  auto builder =
-      dynamic_control_->LoadMetadataBuilder(super_device, current_slot);
-  if (builder == nullptr) {
+  if (!InitPartitionMetadataInternal(dynamic_control_.get(),
+                                     super_device,
+                                     current_slot,
+                                     target_slot,
+                                     target_suffix,
+                                     partition_metadata)) {
     return false;
   }
 
-  // Read metadata from current slot to determine which partitions are logical
-  // and may be resized. Do not read from target slot because metadata at
-  // target slot may be corrupted.
-  PartitionMetadata logical_sizes;
-  if (!FilterPartitionSizes(dynamic_control_.get(),
-                            device_dir,
-                            partition_metadata,
-                            builder.get() /* source metadata */,
-                            current_suffix,
-                            target_suffix,
-                            &logical_sizes)) {
+  if (!Remap(dynamic_control_.get(),
+             super_device,
+             target_slot,
+             target_suffix,
+             partition_metadata)) {
     return false;
   }
 
-  if (!ResizePartitions(dynamic_control_.get(),
-                        super_device,
-                        target_slot,
-                        target_suffix,
-                        logical_sizes,
-                        builder.get())) {
-    return false;
-  }
-
-  // Unmap all partitions, and remap partitions if size is non-zero.
-  for (const auto& pair : logical_sizes) {
-    if (!dynamic_control_->UnmapPartitionOnDeviceMapper(
-            pair.first + target_suffix, true /* wait */)) {
-      return false;
-    }
-    if (pair.second == 0) {
-      continue;
-    }
-    string map_path;
-    if (!dynamic_control_->MapPartitionOnDeviceMapper(
-            super_device, pair.first + target_suffix, target_slot, &map_path)) {
-      return false;
-    }
-  }
   return true;
 }
 
diff --git a/boot_control_android_unittest.cc b/boot_control_android_unittest.cc
index 34eabb0..8185508 100644
--- a/boot_control_android_unittest.cc
+++ b/boot_control_android_unittest.cc
@@ -17,8 +17,10 @@
 #include "update_engine/boot_control_android.h"
 
 #include <set>
+#include <vector>
 
-#include <android-base/strings.h>
+#include <base/logging.h>
+#include <base/strings/string_util.h>
 #include <fs_mgr.h>
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
@@ -26,7 +28,6 @@
 #include "update_engine/mock_boot_control_hal.h"
 #include "update_engine/mock_dynamic_partition_control.h"
 
-using android::base::Join;
 using android::fs_mgr::MetadataBuilder;
 using android::hardware::Void;
 using testing::_;
@@ -40,6 +41,7 @@
 using testing::MatcherInterface;
 using testing::MatchResultListener;
 using testing::NiceMock;
+using testing::Not;
 using testing::Return;
 
 namespace chromeos_update_engine {
@@ -49,19 +51,37 @@
 constexpr const char* kFakeDevicePath = "/fake/dev/path/";
 constexpr const char* kFakeMappedPath = "/fake/mapped/path/";
 constexpr const uint32_t kFakeMetadataSize = 65536;
+constexpr const char* kDefaultGroup = "foo";
+
+// "vendor"
+struct PartitionName : std::string {
+  using std::string::string;
+};
+
+// "vendor_a"
+struct PartitionNameSuffix : std::string {
+  using std::string::string;
+};
 
 // A map describing the size of each partition.
-using PartitionSizes = std::map<std::string, uint64_t>;
+using PartitionSizes = std::map<PartitionName, uint64_t>;
+using PartitionSuffixSizes = std::map<PartitionNameSuffix, uint64_t>;
+
+using PartitionMetadata = BootControlInterface::PartitionMetadata;
 
 // C++ standards do not allow uint64_t (aka unsigned long) to be the parameter
 // of user-defined literal operators.
-unsigned long long operator"" _MiB(unsigned long long x) {  // NOLINT
+constexpr unsigned long long operator"" _MiB(unsigned long long x) {  // NOLINT
   return x << 20;
 }
-unsigned long long operator"" _GiB(unsigned long long x) {  // NOLINT
+constexpr unsigned long long operator"" _GiB(unsigned long long x) {  // NOLINT
   return x << 30;
 }
 
+constexpr uint64_t kDefaultGroupSize = 5_GiB;
+// Super device size. 1 MiB for metadata.
+constexpr uint64_t kDefaultSuperSize = kDefaultGroupSize * 2 + 1_MiB;
+
 template <typename U, typename V>
 std::ostream& operator<<(std::ostream& os, const std::map<U, V>& param) {
   os << "{";
@@ -75,6 +95,32 @@
   return os << "}";
 }
 
+template <typename T>
+std::ostream& operator<<(std::ostream& os, const std::vector<T>& param) {
+  os << "[";
+  bool first = true;
+  for (const auto& e : param) {
+    if (!first)
+      os << ", ";
+    os << e;
+    first = false;
+  }
+  return os << "]";
+}
+
+std::ostream& operator<<(std::ostream& os,
+                         const PartitionMetadata::Partition& p) {
+  return os << "{" << p.name << ", " << p.size << "}";
+}
+
+std::ostream& operator<<(std::ostream& os, const PartitionMetadata::Group& g) {
+  return os << "{" << g.name << ", " << g.size << ", " << g.partitions << "}";
+}
+
+std::ostream& operator<<(std::ostream& os, const PartitionMetadata& m) {
+  return os << m.groups;
+}
+
 inline std::string GetDevice(const std::string& name) {
   return kFakeDevicePath + name;
 }
@@ -91,62 +137,122 @@
             << "}";
 }
 
-std::unique_ptr<MetadataBuilder> NewFakeMetadata(const PartitionSizes& sizes) {
-  auto builder = MetadataBuilder::New(10_GiB, kFakeMetadataSize, kMaxNumSlots);
+// To support legacy tests, auto-convert {name_a: size} map to
+// PartitionMetadata.
+PartitionMetadata toMetadata(const PartitionSuffixSizes& partition_sizes) {
+  PartitionMetadata metadata;
+  for (const char* suffix : kSlotSuffixes) {
+    metadata.groups.push_back(
+        {std::string(kDefaultGroup) + suffix, kDefaultGroupSize, {}});
+  }
+  for (const auto& pair : partition_sizes) {
+    for (size_t suffix_idx = 0; suffix_idx < kMaxNumSlots; ++suffix_idx) {
+      if (base::EndsWith(pair.first,
+                         kSlotSuffixes[suffix_idx],
+                         base::CompareCase::SENSITIVE)) {
+        metadata.groups[suffix_idx].partitions.push_back(
+            {pair.first, pair.second});
+      }
+    }
+  }
+  return metadata;
+}
+
+// To support legacy tests, auto-convert {name: size} map to PartitionMetadata.
+PartitionMetadata toMetadata(const PartitionSizes& partition_sizes) {
+  PartitionMetadata metadata;
+  metadata.groups.push_back(
+      {std::string{kDefaultGroup}, kDefaultGroupSize, {}});
+  for (const auto& pair : partition_sizes) {
+    metadata.groups[0].partitions.push_back({pair.first, pair.second});
+  }
+  return metadata;
+}
+
+std::unique_ptr<MetadataBuilder> NewFakeMetadata(
+    const PartitionMetadata& metadata) {
+  auto builder =
+      MetadataBuilder::New(kDefaultSuperSize, kFakeMetadataSize, kMaxNumSlots);
+  EXPECT_GE(builder->AllocatableSpace(), kDefaultGroupSize * 2);
   EXPECT_NE(nullptr, builder);
   if (builder == nullptr)
     return nullptr;
-  for (const auto& pair : sizes) {
-    auto p = builder->AddPartition(pair.first, 0 /* attr */);
-    EXPECT_TRUE(p && builder->ResizePartition(p, pair.second));
+  for (const auto& group : metadata.groups) {
+    EXPECT_TRUE(builder->AddGroup(group.name, group.size));
+    for (const auto& partition : group.partitions) {
+      auto p = builder->AddPartition(partition.name, group.name, 0 /* attr */);
+      EXPECT_TRUE(p && builder->ResizePartition(p, partition.size));
+    }
   }
   return builder;
 }
 
 class MetadataMatcher : public MatcherInterface<MetadataBuilder*> {
  public:
-  explicit MetadataMatcher(const PartitionSizes& partition_sizes)
-      : partition_sizes_(partition_sizes) {}
+  explicit MetadataMatcher(const PartitionSuffixSizes& partition_sizes)
+      : partition_metadata_(toMetadata(partition_sizes)) {}
+  explicit MetadataMatcher(const PartitionMetadata& partition_metadata)
+      : partition_metadata_(partition_metadata) {}
+
   bool MatchAndExplain(MetadataBuilder* metadata,
                        MatchResultListener* listener) const override {
     bool success = true;
-    for (const auto& pair : partition_sizes_) {
-      auto p = metadata->FindPartition(pair.first);
-      if (p == nullptr) {
-        if (success)
-          *listener << "; ";
-        *listener << "No partition " << pair.first;
-        success = false;
-        continue;
-      }
-      if (p->size() != pair.second) {
-        if (success)
-          *listener << "; ";
-        *listener << "Partition " << pair.first << " has size " << p->size()
-                  << ", expected " << pair.second;
-        success = false;
+    for (const auto& group : partition_metadata_.groups) {
+      for (const auto& partition : group.partitions) {
+        auto p = metadata->FindPartition(partition.name);
+        if (p == nullptr) {
+          if (!success)
+            *listener << "; ";
+          *listener << "No partition " << partition.name;
+          success = false;
+          continue;
+        }
+        if (p->size() != partition.size) {
+          if (!success)
+            *listener << "; ";
+          *listener << "Partition " << partition.name << " has size "
+                    << p->size() << ", expected " << partition.size;
+          success = false;
+        }
+        if (p->group_name() != group.name) {
+          if (!success)
+            *listener << "; ";
+          *listener << "Partition " << partition.name << " has group "
+                    << p->group_name() << ", expected " << group.name;
+          success = false;
+        }
       }
     }
     return success;
   }
 
   void DescribeTo(std::ostream* os) const override {
-    *os << "expect: " << partition_sizes_;
+    *os << "expect: " << partition_metadata_;
   }
 
   void DescribeNegationTo(std::ostream* os) const override {
-    *os << "expect not: " << partition_sizes_;
+    *os << "expect not: " << partition_metadata_;
   }
 
  private:
-  PartitionSizes partition_sizes_;
+  PartitionMetadata partition_metadata_;
 };
 
 inline Matcher<MetadataBuilder*> MetadataMatches(
-    const PartitionSizes& partition_sizes) {
+    const PartitionSuffixSizes& partition_sizes) {
   return MakeMatcher(new MetadataMatcher(partition_sizes));
 }
 
+inline Matcher<MetadataBuilder*> MetadataMatches(
+    const PartitionMetadata& partition_metadata) {
+  return MakeMatcher(new MetadataMatcher(partition_metadata));
+}
+
+MATCHER_P(HasGroup, group, " has group " + group) {
+  auto groups = arg->ListGroups();
+  return std::find(groups.begin(), groups.end(), group) != groups.end();
+}
+
 class BootControlAndroidTest : public ::testing::Test {
  protected:
   void SetUp() override {
@@ -187,10 +293,15 @@
 
   // Set the fake metadata to return when LoadMetadataBuilder is called on
   // |slot|.
-  void SetMetadata(uint32_t slot, const PartitionSizes& sizes) {
+  void SetMetadata(uint32_t slot, const PartitionSuffixSizes& sizes) {
+    SetMetadata(slot, toMetadata(sizes));
+  }
+
+  void SetMetadata(uint32_t slot, const PartitionMetadata& metadata) {
     EXPECT_CALL(dynamicControl(), LoadMetadataBuilder(GetSuperDevice(), slot))
-        .WillOnce(
-            Invoke([sizes](auto, auto) { return NewFakeMetadata(sizes); }));
+        .Times(AnyNumber())
+        .WillRepeatedly(Invoke(
+            [metadata](auto, auto) { return NewFakeMetadata(metadata); }));
   }
 
   // Expect that MapPartitionOnDeviceMapper is called on target() metadata slot
@@ -245,7 +356,7 @@
     }
   }
 
-  void ExpectStoreMetadata(const PartitionSizes& partition_sizes) {
+  void ExpectStoreMetadata(const PartitionSuffixSizes& partition_sizes) {
     ExpectStoreMetadataMatch(MetadataMatches(partition_sizes));
   }
 
@@ -261,13 +372,13 @@
   uint32_t target() { return slots_.target; }
 
   // Return partition names with suffix of source().
-  std::string S(const std::string& name) {
-    return name + std::string(kSlotSuffixes[source()]);
+  PartitionNameSuffix S(const std::string& name) {
+    return PartitionNameSuffix(name + std::string(kSlotSuffixes[source()]));
   }
 
   // Return partition names with suffix of target().
-  std::string T(const std::string& name) {
-    return name + std::string(kSlotSuffixes[target()]);
+  PartitionNameSuffix T(const std::string& name) {
+    return PartitionNameSuffix(name + std::string(kSlotSuffixes[target()]));
   }
 
   // Set source and target slots to use before testing.
@@ -280,6 +391,16 @@
     // Should not store metadata to source slot.
     EXPECT_CALL(dynamicControl(), StoreMetadata(GetSuperDevice(), _, source()))
         .Times(0);
+    // Should not load metadata from target slot.
+    EXPECT_CALL(dynamicControl(),
+                LoadMetadataBuilder(GetSuperDevice(), target()))
+        .Times(0);
+  }
+
+  bool InitPartitionMetadata(uint32_t slot, PartitionSizes partition_sizes) {
+    auto m = toMetadata(partition_sizes);
+    LOG(INFO) << m;
+    return bootctl_.InitPartitionMetadata(slot, m);
   }
 
   BootControlAndroid bootctl_;  // BootControlAndroid under test.
@@ -301,191 +422,174 @@
 // Test resize case. Grow if target metadata contains a partition with a size
 // less than expected.
 TEST_P(BootControlAndroidTestP, NeedGrowIfSizeNotMatchWhenResizing) {
-  PartitionSizes initial{{S("system"), 2_GiB},
-                         {S("vendor"), 1_GiB},
-                         {T("system"), 2_GiB},
-                         {T("vendor"), 1_GiB}};
-  SetMetadata(source(), initial);
-  SetMetadata(target(), initial);
+  SetMetadata(source(),
+              {{S("system"), 2_GiB},
+               {S("vendor"), 1_GiB},
+               {T("system"), 2_GiB},
+               {T("vendor"), 1_GiB}});
   ExpectStoreMetadata({{S("system"), 2_GiB},
                        {S("vendor"), 1_GiB},
                        {T("system"), 3_GiB},
                        {T("vendor"), 1_GiB}});
   ExpectRemap({T("system"), T("vendor")});
 
-  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
-      target(), {{"system", 3_GiB}, {"vendor", 1_GiB}}));
+  EXPECT_TRUE(
+      InitPartitionMetadata(target(), {{"system", 3_GiB}, {"vendor", 1_GiB}}));
   ExpectDevicesAreMapped({T("system"), T("vendor")});
 }
 
 // Test resize case. Shrink if target metadata contains a partition with a size
 // greater than expected.
 TEST_P(BootControlAndroidTestP, NeedShrinkIfSizeNotMatchWhenResizing) {
-  PartitionSizes initial{{S("system"), 2_GiB},
-                         {S("vendor"), 1_GiB},
-                         {T("system"), 2_GiB},
-                         {T("vendor"), 1_GiB}};
-  SetMetadata(source(), initial);
-  SetMetadata(target(), initial);
+  SetMetadata(source(),
+              {{S("system"), 2_GiB},
+               {S("vendor"), 1_GiB},
+               {T("system"), 2_GiB},
+               {T("vendor"), 1_GiB}});
   ExpectStoreMetadata({{S("system"), 2_GiB},
                        {S("vendor"), 1_GiB},
                        {T("system"), 2_GiB},
                        {T("vendor"), 150_MiB}});
   ExpectRemap({T("system"), T("vendor")});
 
-  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
-      target(), {{"system", 2_GiB}, {"vendor", 150_MiB}}));
+  EXPECT_TRUE(InitPartitionMetadata(target(),
+                                    {{"system", 2_GiB}, {"vendor", 150_MiB}}));
   ExpectDevicesAreMapped({T("system"), T("vendor")});
 }
 
 // Test adding partitions on the first run.
 TEST_P(BootControlAndroidTestP, AddPartitionToEmptyMetadata) {
-  SetMetadata(source(), {});
-  SetMetadata(target(), {});
+  SetMetadata(source(), PartitionSuffixSizes{});
   ExpectStoreMetadata({{T("system"), 2_GiB}, {T("vendor"), 1_GiB}});
   ExpectRemap({T("system"), T("vendor")});
 
-  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
-      target(), {{"system", 2_GiB}, {"vendor", 1_GiB}}));
+  EXPECT_TRUE(
+      InitPartitionMetadata(target(), {{"system", 2_GiB}, {"vendor", 1_GiB}}));
   ExpectDevicesAreMapped({T("system"), T("vendor")});
 }
 
 // Test subsequent add case.
 TEST_P(BootControlAndroidTestP, AddAdditionalPartition) {
   SetMetadata(source(), {{S("system"), 2_GiB}, {T("system"), 2_GiB}});
-  SetMetadata(target(), {{S("system"), 2_GiB}, {T("system"), 2_GiB}});
   ExpectStoreMetadata(
       {{S("system"), 2_GiB}, {T("system"), 2_GiB}, {T("vendor"), 1_GiB}});
   ExpectRemap({T("system"), T("vendor")});
 
-  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
-      target(), {{"system", 2_GiB}, {"vendor", 1_GiB}}));
+  EXPECT_TRUE(
+      InitPartitionMetadata(target(), {{"system", 2_GiB}, {"vendor", 1_GiB}}));
   ExpectDevicesAreMapped({T("system"), T("vendor")});
 }
 
 // Test delete one partition.
 TEST_P(BootControlAndroidTestP, DeletePartition) {
-  PartitionSizes initial{{S("system"), 2_GiB},
-                         {S("vendor"), 1_GiB},
-                         {T("system"), 2_GiB},
-                         {T("vendor"), 1_GiB}};
-  SetMetadata(source(), initial);
-  SetMetadata(target(), initial);
-  ExpectStoreMetadata({{S("system"), 2_GiB},
-                       {S("vendor"), 1_GiB},
-                       {T("system"), 2_GiB},
-                       {T("vendor"), 0}});
-  ExpectUnmap({T("system"), T("vendor")});
-  ExpectMap({T("system")});
+  SetMetadata(source(),
+              {{S("system"), 2_GiB},
+               {S("vendor"), 1_GiB},
+               {T("system"), 2_GiB},
+               {T("vendor"), 1_GiB}});
+  // No T("vendor")
+  ExpectStoreMetadata(
+      {{S("system"), 2_GiB}, {S("vendor"), 1_GiB}, {T("system"), 2_GiB}});
+  ExpectRemap({T("system")});
 
-  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
-      target(), {{"system", 2_GiB}, {"vendor", 0}}));
+  EXPECT_TRUE(InitPartitionMetadata(target(), {{"system", 2_GiB}}));
   ExpectDevicesAreMapped({T("system")});
 }
 
 // Test delete all partitions.
 TEST_P(BootControlAndroidTestP, DeleteAll) {
-  PartitionSizes initial{{S("system"), 2_GiB},
-                         {S("vendor"), 1_GiB},
-                         {T("system"), 2_GiB},
-                         {T("vendor"), 1_GiB}};
-  SetMetadata(source(), initial);
-  SetMetadata(target(), initial);
-  ExpectStoreMetadata({{S("system"), 2_GiB},
-                       {S("vendor"), 1_GiB},
-                       {T("system"), 0},
-                       {T("vendor"), 0}});
-  ExpectUnmap({T("system"), T("vendor")});
-  ExpectMap({});
-
-  EXPECT_TRUE(
-      bootctl_.InitPartitionMetadata(target(), {{"system", 0}, {"vendor", 0}}));
-  ExpectDevicesAreMapped({});
-}
-
-// Test corrupt source metadata case. This shouldn't happen in practice,
-// because the device is already booted normally.
-TEST_P(BootControlAndroidTestP, CorruptedSourceMetadata) {
-  EXPECT_CALL(dynamicControl(), LoadMetadataBuilder(GetSuperDevice(), source()))
-      .WillOnce(Invoke([](auto, auto) { return nullptr; }));
-  EXPECT_FALSE(bootctl_.InitPartitionMetadata(target(), {}))
-      << "Should not be able to continue with corrupt source metadata";
-}
-
-// Test corrupt target metadata case. This may happen in practice.
-// BootControlAndroid should copy from source metadata and make necessary
-// modifications on it.
-TEST_P(BootControlAndroidTestP, CorruptedTargetMetadata) {
   SetMetadata(source(),
               {{S("system"), 2_GiB},
                {S("vendor"), 1_GiB},
-               {T("system"), 0},
-               {T("vendor"), 0}});
-  EXPECT_CALL(dynamicControl(), LoadMetadataBuilder(GetSuperDevice(), target()))
+               {T("system"), 2_GiB},
+               {T("vendor"), 1_GiB}});
+  ExpectStoreMetadata({{S("system"), 2_GiB}, {S("vendor"), 1_GiB}});
+
+  EXPECT_TRUE(InitPartitionMetadata(target(), {}));
+  ExpectDevicesAreMapped({});
+}
+
+// Test corrupt source metadata case.
+TEST_P(BootControlAndroidTestP, CorruptedSourceMetadata) {
+  EXPECT_CALL(dynamicControl(), LoadMetadataBuilder(GetSuperDevice(), source()))
       .WillOnce(Invoke([](auto, auto) { return nullptr; }));
-  ExpectStoreMetadata({{S("system"), 2_GiB},
-                       {S("vendor"), 1_GiB},
-                       {T("system"), 3_GiB},
-                       {T("vendor"), 150_MiB}});
-  ExpectRemap({T("system"), T("vendor")});
-  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
-      target(), {{"system", 3_GiB}, {"vendor", 150_MiB}}));
-  ExpectDevicesAreMapped({T("system"), T("vendor")});
+  EXPECT_FALSE(InitPartitionMetadata(target(), {{"system", 1_GiB}}))
+      << "Should not be able to continue with corrupt source metadata";
 }
 
 // Test that InitPartitionMetadata fail if there is not enough space on the
 // device.
 TEST_P(BootControlAndroidTestP, NotEnoughSpace) {
-  PartitionSizes initial{{S("system"), 3_GiB},
-                         {S("vendor"), 2_GiB},
-                         {T("system"), 0},
-                         {T("vendor"), 0}};
-  SetMetadata(source(), initial);
-  SetMetadata(target(), initial);
-  EXPECT_FALSE(bootctl_.InitPartitionMetadata(
-      target(), {{"system", 3_GiB}, {"vendor", 3_GiB}}))
+  SetMetadata(source(),
+              {{S("system"), 3_GiB},
+               {S("vendor"), 2_GiB},
+               {T("system"), 0},
+               {T("vendor"), 0}});
+  EXPECT_FALSE(
+      InitPartitionMetadata(target(), {{"system", 3_GiB}, {"vendor", 3_GiB}}))
       << "Should not be able to fit 11GiB data into 10GiB space";
 }
 
-INSTANTIATE_TEST_CASE_P(ParamTest,
+TEST_P(BootControlAndroidTestP, NotEnoughSpaceForSlot) {
+  SetMetadata(source(),
+              {{S("system"), 1_GiB},
+               {S("vendor"), 1_GiB},
+               {T("system"), 0},
+               {T("vendor"), 0}});
+  EXPECT_FALSE(
+      InitPartitionMetadata(target(), {{"system", 3_GiB}, {"vendor", 3_GiB}}))
+      << "Should not be able to grow over size of super / 2";
+}
+
+INSTANTIATE_TEST_CASE_P(BootControlAndroidTest,
                         BootControlAndroidTestP,
                         testing::Values(TestParam{0, 1}, TestParam{1, 0}));
 
-const PartitionSizes update_sizes_0() {
-  return {{"grown_a", 2_GiB},
-          {"shrunk_a", 1_GiB},
-          {"same_a", 100_MiB},
-          {"deleted_a", 150_MiB},
-          {"grown_b", 200_MiB},
-          {"shrunk_b", 0},
-          {"same_b", 0}};
-}
-
-const PartitionSizes update_sizes_1() {
+const PartitionSuffixSizes update_sizes_0() {
+  // Initial state is 0 for "other" slot.
   return {
       {"grown_a", 2_GiB},
       {"shrunk_a", 1_GiB},
       {"same_a", 100_MiB},
       {"deleted_a", 150_MiB},
+      // no added_a
+      {"grown_b", 200_MiB},
+      // simulate system_other
+      {"shrunk_b", 0},
+      {"same_b", 0},
+      {"deleted_b", 0},
+      // no added_b
+  };
+}
+
+const PartitionSuffixSizes update_sizes_1() {
+  return {
+      {"grown_a", 2_GiB},
+      {"shrunk_a", 1_GiB},
+      {"same_a", 100_MiB},
+      {"deleted_a", 150_MiB},
+      // no added_a
       {"grown_b", 3_GiB},
       {"shrunk_b", 150_MiB},
       {"same_b", 100_MiB},
       {"added_b", 150_MiB},
-      {"deleted_b", 0},
+      // no deleted_b
   };
 }
 
-const PartitionSizes update_sizes_2() {
-  return {{"grown_a", 4_GiB},
-          {"shrunk_a", 100_MiB},
-          {"same_a", 100_MiB},
-          {"added_a", 0_MiB},
-          {"deleted_a", 64_MiB},
-          {"grown_b", 3_GiB},
-          {"shrunk_b", 150_MiB},
-          {"same_b", 100_MiB},
-          {"added_b", 150_MiB},
-          {"deleted_b", 0}};
+const PartitionSuffixSizes update_sizes_2() {
+  return {
+      {"grown_a", 4_GiB},
+      {"shrunk_a", 100_MiB},
+      {"same_a", 100_MiB},
+      {"deleted_a", 64_MiB},
+      // no added_a
+      {"grown_b", 3_GiB},
+      {"shrunk_b", 150_MiB},
+      {"same_b", 100_MiB},
+      {"added_b", 150_MiB},
+      // no deleted_b
+  };
 }
 
 // Test case for first update after the device is manufactured, in which
@@ -497,15 +601,13 @@
   SetMetadata(source(), update_sizes_0());
   SetMetadata(target(), update_sizes_0());
   ExpectStoreMetadata(update_sizes_1());
-  ExpectUnmap({"grown_b", "shrunk_b", "same_b", "added_b", "deleted_b"});
-  ExpectMap({"grown_b", "shrunk_b", "same_b", "added_b"});
+  ExpectRemap({"grown_b", "shrunk_b", "same_b", "added_b"});
 
-  EXPECT_TRUE(bootctl_.InitPartitionMetadata(target(),
-                                             {{"grown", 3_GiB},
-                                              {"shrunk", 150_MiB},
-                                              {"same", 100_MiB},
-                                              {"added", 150_MiB},
-                                              {"deleted", 0_MiB}}));
+  EXPECT_TRUE(InitPartitionMetadata(target(),
+                                    {{"grown", 3_GiB},
+                                     {"shrunk", 150_MiB},
+                                     {"same", 100_MiB},
+                                     {"added", 150_MiB}}));
   ExpectDevicesAreMapped({"grown_b", "shrunk_b", "same_b", "added_b"});
 }
 
@@ -518,22 +620,167 @@
   SetMetadata(target(), update_sizes_0());
 
   ExpectStoreMetadata(update_sizes_2());
-  ExpectUnmap({"grown_a", "shrunk_a", "same_a", "added_a", "deleted_a"});
-  ExpectMap({"grown_a", "shrunk_a", "same_a", "deleted_a"});
+  ExpectRemap({"grown_a", "shrunk_a", "same_a", "deleted_a"});
 
-  EXPECT_TRUE(bootctl_.InitPartitionMetadata(target(),
-                                             {{"grown", 4_GiB},
-                                              {"shrunk", 100_MiB},
-                                              {"same", 100_MiB},
-                                              {"added", 0_MiB},
-                                              {"deleted", 64_MiB}}));
+  EXPECT_TRUE(InitPartitionMetadata(target(),
+                                    {{"grown", 4_GiB},
+                                     {"shrunk", 100_MiB},
+                                     {"same", 100_MiB},
+                                     {"deleted", 64_MiB}}));
   ExpectDevicesAreMapped({"grown_a", "shrunk_a", "same_a", "deleted_a"});
 }
 
 TEST_F(BootControlAndroidTest, ApplyingToCurrentSlot) {
   SetSlots({1, 1});
-  EXPECT_FALSE(bootctl_.InitPartitionMetadata(target(), {}))
+  EXPECT_FALSE(InitPartitionMetadata(target(), {}))
       << "Should not be able to apply to current slot.";
 }
 
+class BootControlAndroidGroupTestP : public BootControlAndroidTestP {
+ public:
+  void SetUp() override {
+    BootControlAndroidTestP::SetUp();
+    SetMetadata(
+        source(),
+        {.groups = {SimpleGroup(S("android"), 3_GiB, S("system"), 2_GiB),
+                    SimpleGroup(S("oem"), 2_GiB, S("vendor"), 1_GiB),
+                    SimpleGroup(T("android"), 3_GiB, T("system"), 0),
+                    SimpleGroup(T("oem"), 2_GiB, T("vendor"), 0)}});
+  }
+
+  // Return a simple group with only one partition.
+  PartitionMetadata::Group SimpleGroup(const std::string& group,
+                                       uint64_t group_size,
+                                       const std::string& partition,
+                                       uint64_t partition_size) {
+    return {.name = group,
+            .size = group_size,
+            .partitions = {{.name = partition, .size = partition_size}}};
+  }
+
+  void ExpectStoreMetadata(const PartitionMetadata& partition_metadata) {
+    ExpectStoreMetadataMatch(MetadataMatches(partition_metadata));
+  }
+
+  // Expect that target slot is stored with target groups.
+  void ExpectStoreMetadataMatch(
+      const Matcher<MetadataBuilder*>& matcher) override {
+    BootControlAndroidTestP::ExpectStoreMetadataMatch(AllOf(
+        MetadataMatches(PartitionMetadata{
+            .groups = {SimpleGroup(S("android"), 3_GiB, S("system"), 2_GiB),
+                       SimpleGroup(S("oem"), 2_GiB, S("vendor"), 1_GiB)}}),
+        matcher));
+  }
+};
+
+// Allow to resize within group.
+TEST_P(BootControlAndroidGroupTestP, ResizeWithinGroup) {
+  ExpectStoreMetadata(PartitionMetadata{
+      .groups = {SimpleGroup(T("android"), 3_GiB, T("system"), 3_GiB),
+                 SimpleGroup(T("oem"), 2_GiB, T("vendor"), 2_GiB)}});
+  ExpectRemap({T("system"), T("vendor")});
+
+  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
+      target(),
+      PartitionMetadata{
+          .groups = {SimpleGroup("android", 3_GiB, "system", 3_GiB),
+                     SimpleGroup("oem", 2_GiB, "vendor", 2_GiB)}}));
+  ExpectDevicesAreMapped({T("system"), T("vendor")});
+}
+
+TEST_P(BootControlAndroidGroupTestP, NotEnoughSpaceForGroup) {
+  EXPECT_FALSE(bootctl_.InitPartitionMetadata(
+      target(),
+      PartitionMetadata{
+          .groups = {SimpleGroup("android", 3_GiB, "system", 1_GiB),
+                     SimpleGroup("oem", 2_GiB, "vendor", 3_GiB)}}))
+      << "Should not be able to grow over maximum size of group";
+}
+
+TEST_P(BootControlAndroidGroupTestP, GroupTooBig) {
+  EXPECT_FALSE(bootctl_.InitPartitionMetadata(
+      target(),
+      PartitionMetadata{.groups = {{.name = "android", .size = 3_GiB},
+                                   {.name = "oem", .size = 3_GiB}}}))
+      << "Should not be able to grow over size of super / 2";
+}
+
+TEST_P(BootControlAndroidGroupTestP, AddPartitionToGroup) {
+  ExpectStoreMetadata(PartitionMetadata{
+      .groups = {
+          {.name = T("android"),
+           .size = 3_GiB,
+           .partitions = {{.name = T("system"), .size = 2_GiB},
+                          {.name = T("product_services"), .size = 1_GiB}}}}});
+  ExpectRemap({T("system"), T("vendor"), T("product_services")});
+
+  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
+      target(),
+      PartitionMetadata{
+          .groups = {
+              {.name = "android",
+               .size = 3_GiB,
+               .partitions = {{.name = "system", .size = 2_GiB},
+                              {.name = "product_services", .size = 1_GiB}}},
+              SimpleGroup("oem", 2_GiB, "vendor", 2_GiB)}}));
+  ExpectDevicesAreMapped({T("system"), T("vendor"), T("product_services")});
+}
+
+TEST_P(BootControlAndroidGroupTestP, RemovePartitionFromGroup) {
+  ExpectStoreMetadata(PartitionMetadata{
+      .groups = {{.name = T("android"), .size = 3_GiB, .partitions = {}}}});
+  ExpectRemap({T("vendor")});
+
+  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
+      target(),
+      PartitionMetadata{
+          .groups = {{.name = "android", .size = 3_GiB, .partitions = {}},
+                     SimpleGroup("oem", 2_GiB, "vendor", 2_GiB)}}));
+  ExpectDevicesAreMapped({T("vendor")});
+}
+
+TEST_P(BootControlAndroidGroupTestP, AddGroup) {
+  ExpectStoreMetadata(PartitionMetadata{
+      .groups = {
+          SimpleGroup(T("new_group"), 2_GiB, T("new_partition"), 2_GiB)}});
+  ExpectRemap({T("system"), T("vendor"), T("new_partition")});
+
+  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
+      target(),
+      PartitionMetadata{
+          .groups = {
+              SimpleGroup("android", 2_GiB, "system", 2_GiB),
+              SimpleGroup("oem", 1_GiB, "vendor", 1_GiB),
+              SimpleGroup("new_group", 2_GiB, "new_partition", 2_GiB)}}));
+  ExpectDevicesAreMapped({T("system"), T("vendor"), T("new_partition")});
+}
+
+TEST_P(BootControlAndroidGroupTestP, RemoveGroup) {
+  ExpectStoreMetadataMatch(Not(HasGroup(T("oem"))));
+  ExpectRemap({T("system")});
+  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
+      target(),
+      PartitionMetadata{
+          .groups = {SimpleGroup("android", 2_GiB, "system", 2_GiB)}}));
+  ExpectDevicesAreMapped({T("system")});
+}
+
+TEST_P(BootControlAndroidGroupTestP, ResizeGroup) {
+  ExpectStoreMetadata(PartitionMetadata{
+      .groups = {SimpleGroup(T("android"), 2_GiB, T("system"), 2_GiB),
+                 SimpleGroup(T("oem"), 3_GiB, T("vendor"), 3_GiB)}});
+  ExpectRemap({T("system"), T("vendor")});
+
+  EXPECT_TRUE(bootctl_.InitPartitionMetadata(
+      target(),
+      PartitionMetadata{
+          .groups = {SimpleGroup("android", 2_GiB, "system", 2_GiB),
+                     SimpleGroup("oem", 3_GiB, "vendor", 3_GiB)}}));
+  ExpectDevicesAreMapped({T("system"), T("vendor")});
+}
+
+INSTANTIATE_TEST_CASE_P(BootControlAndroidTest,
+                        BootControlAndroidGroupTestP,
+                        testing::Values(TestParam{0, 1}, TestParam{1, 0}));
+
 }  // namespace chromeos_update_engine
diff --git a/common/boot_control_interface.h b/common/boot_control_interface.h
index 1b76939..43517ce 100644
--- a/common/boot_control_interface.h
+++ b/common/boot_control_interface.h
@@ -20,6 +20,7 @@
 #include <climits>
 #include <map>
 #include <string>
+#include <vector>
 
 #include <base/callback.h>
 #include <base/macros.h>
@@ -33,7 +34,19 @@
 class BootControlInterface {
  public:
   using Slot = unsigned int;
-  using PartitionMetadata = std::map<std::string, uint64_t>;
+
+  struct PartitionMetadata {
+    struct Partition {
+      std::string name;
+      uint64_t size;
+    };
+    struct Group {
+      std::string name;
+      uint64_t size;
+      std::vector<Partition> partitions;
+    };
+    std::vector<Group> groups;
+  };
 
   static const Slot kInvalidSlot = UINT_MAX;
 
@@ -80,10 +93,11 @@
   virtual bool MarkBootSuccessfulAsync(base::Callback<void(bool)> callback) = 0;
 
   // Initialize metadata of underlying partitions for a given |slot|.
-  // Ensure that partitions at the specified |slot| has a given size, as
-  // specified by |partition_metadata|. |partition_metadata| has the format:
-  // {"vendor": 524288000, "system": 2097152000, ...}; values must be
-  // aligned to the logical block size of the super partition.
+  // Ensure that all updateable groups with the suffix GetSuffix(|slot|) exactly
+  // matches the layout specified in |partition_metadata|. Ensure that
+  // partitions at the specified |slot| has a given size and updateable group,
+  // as specified by |partition_metadata|. Sizes must be aligned to the logical
+  // block size of the super partition.
   virtual bool InitPartitionMetadata(
       Slot slot, const PartitionMetadata& partition_metadata) = 0;
 
diff --git a/payload_consumer/delta_performer.cc b/payload_consumer/delta_performer.cc
index 7dec48f..7a19374 100644
--- a/payload_consumer/delta_performer.cc
+++ b/payload_consumer/delta_performer.cc
@@ -944,8 +944,30 @@
   }
 
   BootControlInterface::PartitionMetadata partition_metadata;
-  for (const InstallPlan::Partition& partition : install_plan_->partitions) {
-    partition_metadata.emplace(partition.name, partition.target_size);
+  if (manifest_.has_dynamic_partition_metadata()) {
+    std::map<string, uint64_t> partition_sizes;
+    for (const InstallPlan::Partition& partition : install_plan_->partitions) {
+      partition_sizes.emplace(partition.name, partition.target_size);
+    }
+    for (const auto& group : manifest_.dynamic_partition_metadata().groups()) {
+      BootControlInterface::PartitionMetadata::Group e;
+      e.name = group.name();
+      e.size = group.size();
+      for (const auto& partition_name : group.partition_names()) {
+        auto it = partition_sizes.find(partition_name);
+        if (it == partition_sizes.end()) {
+          // TODO(tbao): Support auto-filling partition info for framework-only
+          // OTA.
+          LOG(ERROR) << "dynamic_partition_metadata contains partition "
+                     << partition_name
+                     << " but it is not part of the manifest. "
+                     << "This is not supported.";
+          return false;
+        }
+        e.partitions.push_back({partition_name, it->second});
+      }
+      partition_metadata.groups.push_back(std::move(e));
+    }
   }
 
   if (!boot_control_->InitPartitionMetadata(install_plan_->target_slot,
@@ -1676,6 +1698,16 @@
     return ErrorCode::kPayloadTimestampError;
   }
 
+  if (major_payload_version_ == kChromeOSMajorPayloadVersion) {
+    if (manifest_.has_dynamic_partition_metadata()) {
+      LOG(ERROR)
+          << "Should not contain dynamic_partition_metadata for major version "
+          << kChromeOSMajorPayloadVersion
+          << ". Please use major version 2 or above.";
+      return ErrorCode::kPayloadMismatchedType;
+    }
+  }
+
   // TODO(garnold) we should be adding more and more manifest checks, such as
   // partition boundaries etc (see chromium-os:37661).