spirv-fuzz: fuzzer pass to adjust memory access operands (#2968)

A new pass that gives spirv-fuzz the ability to adjust the memory
operand masks associated with memory access instructions (such as
OpLoad and OpCopy Memory).

Fixes #2940.
diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt
index 587a05a..39bf1d3 100644
--- a/source/fuzz/CMakeLists.txt
+++ b/source/fuzz/CMakeLists.txt
@@ -41,6 +41,7 @@
         fuzzer_pass_add_useful_constructs.h
         fuzzer_pass_adjust_function_controls.h
         fuzzer_pass_adjust_loop_controls.h
+        fuzzer_pass_adjust_memory_operands_masks.h
         fuzzer_pass_adjust_selection_controls.h
         fuzzer_pass_apply_id_synonyms.h
         fuzzer_pass_construct_composites.h
@@ -74,6 +75,7 @@
         transformation_replace_id_with_synonym.h
         transformation_set_function_control.h
         transformation_set_loop_control.h
+        transformation_set_memory_operands_mask.h
         transformation_set_selection_control.h
         transformation_split_block.h
         uniform_buffer_element_descriptor.h
@@ -91,6 +93,7 @@
         fuzzer_pass_add_useful_constructs.cpp
         fuzzer_pass_adjust_function_controls.cpp
         fuzzer_pass_adjust_loop_controls.cpp
+        fuzzer_pass_adjust_memory_operands_masks.cpp
         fuzzer_pass_adjust_selection_controls.cpp
         fuzzer_pass_apply_id_synonyms.cpp
         fuzzer_pass_construct_composites.cpp
@@ -123,6 +126,7 @@
         transformation_replace_id_with_synonym.cpp
         transformation_set_function_control.cpp
         transformation_set_loop_control.cpp
+        transformation_set_memory_operands_mask.cpp
         transformation_set_selection_control.cpp
         transformation_split_block.cpp
         uniform_buffer_element_descriptor.cpp
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index d07c173..01b4258 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -18,6 +18,7 @@
 #include <memory>
 #include <sstream>
 
+#include "fuzzer_pass_adjust_memory_operands_masks.h"
 #include "source/fuzz/fact_manager.h"
 #include "source/fuzz/fuzzer_context.h"
 #include "source/fuzz/fuzzer_pass_add_dead_breaks.h"
@@ -173,14 +174,17 @@
   // Now apply some passes that it does not make sense to apply repeatedly,
   // as they do not unlock other passes.
   std::vector<std::unique_ptr<FuzzerPass>> final_passes;
-  MaybeAddPass<FuzzerPassAdjustFunctionControls>(&passes, ir_context.get(),
-                                                 &fact_manager, &fuzzer_context,
-                                                 transformation_sequence_out);
-  MaybeAddPass<FuzzerPassAdjustLoopControls>(&passes, ir_context.get(),
+  MaybeAddPass<FuzzerPassAdjustFunctionControls>(
+      &final_passes, ir_context.get(), &fact_manager, &fuzzer_context,
+      transformation_sequence_out);
+  MaybeAddPass<FuzzerPassAdjustLoopControls>(&final_passes, ir_context.get(),
                                              &fact_manager, &fuzzer_context,
                                              transformation_sequence_out);
+  MaybeAddPass<FuzzerPassAdjustMemoryOperandsMasks>(
+      &final_passes, ir_context.get(), &fact_manager, &fuzzer_context,
+      transformation_sequence_out);
   MaybeAddPass<FuzzerPassAdjustSelectionControls>(
-      &passes, ir_context.get(), &fact_manager, &fuzzer_context,
+      &final_passes, ir_context.get(), &fact_manager, &fuzzer_context,
       transformation_sequence_out);
   for (auto& pass : final_passes) {
     pass->Apply();
diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp
index 68903d0..356cb35 100644
--- a/source/fuzz/fuzzer_context.cpp
+++ b/source/fuzz/fuzzer_context.cpp
@@ -30,6 +30,8 @@
 const std::pair<uint32_t, uint32_t> kChanceOfAdjustingFunctionControl = {20,
                                                                          70};
 const std::pair<uint32_t, uint32_t> kChanceOfAdjustingLoopControl = {20, 90};
+const std::pair<uint32_t, uint32_t> kChanceOfAdjustingMemoryOperandsMask = {20,
+                                                                            90};
 const std::pair<uint32_t, uint32_t> kChanceOfAdjustingSelectionControl = {20,
                                                                           90};
 const std::pair<uint32_t, uint32_t> kChanceOfCopyingObject = {20, 50};
@@ -72,6 +74,8 @@
       ChooseBetweenMinAndMax(kChanceOfAdjustingFunctionControl);
   chance_of_adjusting_loop_control_ =
       ChooseBetweenMinAndMax(kChanceOfAdjustingLoopControl);
+  chance_of_adjusting_memory_operands_mask_ =
+      ChooseBetweenMinAndMax(kChanceOfAdjustingMemoryOperandsMask);
   chance_of_adjusting_selection_control_ =
       ChooseBetweenMinAndMax(kChanceOfAdjustingSelectionControl);
   chance_of_constructing_composite_ =
diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h
index fcf2c9f..c8242e6 100644
--- a/source/fuzz/fuzzer_context.h
+++ b/source/fuzz/fuzzer_context.h
@@ -71,6 +71,9 @@
   uint32_t GetChanceOfAdjustingLoopControl() {
     return chance_of_adjusting_loop_control_;
   }
+  uint32_t GetChanceOfAdjustingMemoryOperandsMask() {
+    return chance_of_adjusting_memory_operands_mask_;
+  }
   uint32_t GetChanceOfAdjustingSelectionControl() {
     return chance_of_adjusting_selection_control_;
   }
@@ -112,6 +115,7 @@
   uint32_t chance_of_adding_no_contraction_decoration_;
   uint32_t chance_of_adjusting_function_control_;
   uint32_t chance_of_adjusting_loop_control_;
+  uint32_t chance_of_adjusting_memory_operands_mask_;
   uint32_t chance_of_adjusting_selection_control_;
   uint32_t chance_of_constructing_composite_;
   uint32_t chance_of_copying_object_;
diff --git a/source/fuzz/fuzzer_pass_adjust_memory_operands_masks.cpp b/source/fuzz/fuzzer_pass_adjust_memory_operands_masks.cpp
new file mode 100644
index 0000000..a9d4b32
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_adjust_memory_operands_masks.cpp
@@ -0,0 +1,113 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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 "source/fuzz/fuzzer_pass_adjust_memory_operands_masks.h"
+
+#include "source/fuzz/instruction_descriptor.h"
+#include "source/fuzz/transformation_set_memory_operands_mask.h"
+
+namespace spvtools {
+namespace fuzz {
+
+FuzzerPassAdjustMemoryOperandsMasks::FuzzerPassAdjustMemoryOperandsMasks(
+    opt::IRContext* ir_context, FactManager* fact_manager,
+    FuzzerContext* fuzzer_context,
+    protobufs::TransformationSequence* transformations)
+    : FuzzerPass(ir_context, fact_manager, fuzzer_context, transformations) {}
+
+FuzzerPassAdjustMemoryOperandsMasks::~FuzzerPassAdjustMemoryOperandsMasks() =
+    default;
+
+void FuzzerPassAdjustMemoryOperandsMasks::Apply() {
+  // Consider every block in every function.
+  for (auto& function : *GetIRContext()->module()) {
+    for (auto& block : function) {
+      // Consider every instruction in this block, using an explicit iterator so
+      // that when we find an instruction of interest we can search backwards to
+      // create an id descriptor for it.
+      for (auto inst_it = block.cbegin(); inst_it != block.cend(); ++inst_it) {
+        if (!TransformationSetMemoryOperandsMask::IsMemoryAccess(*inst_it)) {
+          // We are only interested in memory access instructions.
+          continue;
+        }
+
+        std::vector<uint32_t> indices_of_available_masks_to_adjust;
+        // All memory instructions have at least one memory operands mask.
+        indices_of_available_masks_to_adjust.push_back(0);
+        // From SPIR-V 1.4 onwards, OpCopyMemory and OpCopyMemorySized have a
+        // second mask.
+        switch (inst_it->opcode()) {
+          case SpvOpCopyMemory:
+          case SpvOpCopyMemorySized:
+            if (TransformationSetMemoryOperandsMask::
+                    MultipleMemoryOperandMasksAreSupported(GetIRContext())) {
+              indices_of_available_masks_to_adjust.push_back(1);
+            }
+            break;
+          default:
+            break;
+        }
+
+        // Consider the available masks
+        for (auto mask_index : indices_of_available_masks_to_adjust) {
+          // Randomly decide whether to adjust this mask.
+          if (!GetFuzzerContext()->ChoosePercentage(
+                  GetFuzzerContext()
+                      ->GetChanceOfAdjustingMemoryOperandsMask())) {
+            continue;
+          }
+          // Get the existing mask, using None if there was no mask present at
+          // all.
+          auto existing_mask_in_operand_index =
+              TransformationSetMemoryOperandsMask::GetInOperandIndexForMask(
+                  *inst_it, mask_index);
+          auto existing_mask =
+              existing_mask_in_operand_index < inst_it->NumInOperands()
+                  ? inst_it->GetSingleWordOperand(
+                        existing_mask_in_operand_index)
+                  : static_cast<uint32_t>(SpvMemoryAccessMaskNone);
+
+          // There are two things we can do to a mask:
+          // - add Volatile if not already present
+          // - toggle Nontemporal
+          // The following ensures that we do at least one of these
+          bool add_volatile = !(existing_mask & SpvMemoryAccessVolatileMask) &&
+                              GetFuzzerContext()->ChooseEven();
+          bool toggle_nontemporal =
+              !add_volatile || GetFuzzerContext()->ChooseEven();
+
+          // These bitwise operations use '|' to add Volatile if desired, and
+          // '^' to toggle Nontemporal if desired.
+          uint32_t new_mask =
+              (existing_mask | (add_volatile ? SpvMemoryAccessVolatileMask
+                                             : SpvMemoryAccessMaskNone)) ^
+              (toggle_nontemporal ? SpvMemoryAccessNontemporalMask
+                                  : SpvMemoryAccessMaskNone);
+
+          TransformationSetMemoryOperandsMask transformation(
+              MakeInstructionDescriptor(block, inst_it), new_mask, mask_index);
+          assert(
+              transformation.IsApplicable(GetIRContext(), *GetFactManager()) &&
+              "Transformation should be applicable by construction.");
+          transformation.Apply(GetIRContext(), GetFactManager());
+          *GetTransformations()->add_transformation() =
+              transformation.ToMessage();
+        }
+      }
+    }
+  }
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass_adjust_memory_operands_masks.h b/source/fuzz/fuzzer_pass_adjust_memory_operands_masks.h
new file mode 100644
index 0000000..c3d7118
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_adjust_memory_operands_masks.h
@@ -0,0 +1,40 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_FUZZ_FUZZER_PASS_ADJUST_MEMORY_OPERANDS_MASKS_H_
+#define SOURCE_FUZZ_FUZZER_PASS_ADJUST_MEMORY_OPERANDS_MASKS_H_
+
+#include "source/fuzz/fuzzer_pass.h"
+
+namespace spvtools {
+namespace fuzz {
+
+// A fuzzer pass to adjust the memory operand masks in memory access
+// instructions.
+class FuzzerPassAdjustMemoryOperandsMasks : public FuzzerPass {
+ public:
+  FuzzerPassAdjustMemoryOperandsMasks(
+      opt::IRContext* ir_context, FactManager* fact_manager,
+      FuzzerContext* fuzzer_context,
+      protobufs::TransformationSequence* transformations);
+
+  ~FuzzerPassAdjustMemoryOperandsMasks();
+
+  void Apply() override;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_FUZZER_PASS_ADJUST_MEMORY_OPERANDS_MASKS_H_
diff --git a/source/fuzz/instruction_descriptor.cpp b/source/fuzz/instruction_descriptor.cpp
index bd48d48..2b4217a 100644
--- a/source/fuzz/instruction_descriptor.cpp
+++ b/source/fuzz/instruction_descriptor.cpp
@@ -66,5 +66,40 @@
   return result;
 }
 
+protobufs::InstructionDescriptor MakeInstructionDescriptor(
+    const opt::BasicBlock& block,
+    const opt::BasicBlock::const_iterator& inst_it) {
+  const SpvOp opcode =
+      inst_it->opcode();    // The opcode of the instruction being described.
+  uint32_t skip_count = 0;  // The number of these opcodes we have skipped when
+                            // searching backwards.
+
+  // Consider instructions in the block in reverse order, starting from
+  // |inst_it|.
+  for (opt::BasicBlock::const_iterator backwards_iterator = inst_it;;
+       --backwards_iterator) {
+    if (backwards_iterator->HasResultId()) {
+      // As soon as we find an instruction with a result id, we can return a
+      // descriptor for |inst_it|.
+      return MakeInstructionDescriptor(backwards_iterator->result_id(), opcode,
+                                       skip_count);
+    }
+    if (backwards_iterator != inst_it &&
+        backwards_iterator->opcode() == opcode) {
+      // We are skipping over an instruction with the same opcode as |inst_it|;
+      // we increase our skip count to reflect this.
+      skip_count++;
+    }
+    if (backwards_iterator == block.begin()) {
+      // We exit the loop when we reach the start of the block, but only after
+      // we have processed the first instruction in the block.
+      break;
+    }
+  }
+  // We did not find an instruction inside the block with a result id, so we use
+  // the block's label's id.
+  return MakeInstructionDescriptor(block.id(), opcode, skip_count);
+}
+
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/source/fuzz/instruction_descriptor.h b/source/fuzz/instruction_descriptor.h
index 1164318..c0febca 100644
--- a/source/fuzz/instruction_descriptor.h
+++ b/source/fuzz/instruction_descriptor.h
@@ -16,6 +16,7 @@
 #define SOURCE_FUZZ_INSTRUCTION_DESCRIPTOR_H_
 
 #include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/opt/basic_block.h"
 #include "source/opt/ir_context.h"
 
 namespace spvtools {
@@ -34,6 +35,14 @@
     uint32_t base_instruction_result_id, SpvOp target_instruction_opcode,
     uint32_t num_opcodes_to_ignore);
 
+// Returns an instruction descriptor that describing the instruction at
+// |inst_it|, which must be inside |block|.  The descriptor will be with
+// respect to the first instruction at or before |inst_it| that has a result
+// id.
+protobufs::InstructionDescriptor MakeInstructionDescriptor(
+    const opt::BasicBlock& block,
+    const opt::BasicBlock::const_iterator& inst_it);
+
 }  // namespace fuzz
 }  // namespace spvtools
 
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index 53a28f5..6d0299d 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -190,6 +190,7 @@
     TransformationSetLoopControl set_loop_control = 17;
     TransformationSetFunctionControl set_function_control = 18;
     TransformationAddNoContractionDecoration add_no_contraction_decoration = 19;
+    TransformationSetMemoryOperandsMask set_memory_operands_mask = 20;
     // Add additional option using the next available number.
   }
 }
@@ -464,6 +465,25 @@
 
 }
 
+message TransformationSetMemoryOperandsMask {
+
+  // A transformation that sets the memory operands mask of a memory access
+  // instruction.
+
+  // A descriptor for a memory access instruction, e.g. an OpLoad
+  InstructionDescriptor memory_access_instruction = 1;
+
+  // A mask of memory operands to be applied to the instruction.  It must be the
+  // same as the original mask, except that Volatile can be added, and
+  // Nontemporal can be added or removed.
+  uint32 memory_operands_mask = 2;
+
+  // Some memory access instructions allow more than one mask to be specified;
+  // this field indicates which mask should be set
+  uint32 memory_operands_mask_index = 3;
+
+}
+
 message TransformationSetSelectionControl {
 
   // A transformation that sets the selection control operand of an
diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp
index 9d0841f..ddc0b07 100644
--- a/source/fuzz/transformation.cpp
+++ b/source/fuzz/transformation.cpp
@@ -33,6 +33,7 @@
 #include "source/fuzz/transformation_replace_id_with_synonym.h"
 #include "source/fuzz/transformation_set_function_control.h"
 #include "source/fuzz/transformation_set_loop_control.h"
+#include "source/fuzz/transformation_set_memory_operands_mask.h"
 #include "source/fuzz/transformation_set_selection_control.h"
 #include "source/fuzz/transformation_split_block.h"
 #include "source/util/make_unique.h"
@@ -94,6 +95,9 @@
     case protobufs::Transformation::TransformationCase::kSetLoopControl:
       return MakeUnique<TransformationSetLoopControl>(
           message.set_loop_control());
+    case protobufs::Transformation::TransformationCase::kSetMemoryOperandsMask:
+      return MakeUnique<TransformationSetMemoryOperandsMask>(
+          message.set_memory_operands_mask());
     case protobufs::Transformation::TransformationCase::kSetSelectionControl:
       return MakeUnique<TransformationSetSelectionControl>(
           message.set_selection_control());
diff --git a/source/fuzz/transformation_set_memory_operands_mask.cpp b/source/fuzz/transformation_set_memory_operands_mask.cpp
new file mode 100644
index 0000000..a14e1a6
--- /dev/null
+++ b/source/fuzz/transformation_set_memory_operands_mask.cpp
@@ -0,0 +1,201 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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 "source/fuzz/transformation_set_memory_operands_mask.h"
+
+#include "source/fuzz/instruction_descriptor.h"
+
+namespace spvtools {
+namespace fuzz {
+
+namespace {
+
+const uint32_t kOpLoadMemoryOperandsMaskIndex = 1;
+const uint32_t kOpStoreMemoryOperandsMaskIndex = 2;
+const uint32_t kOpCopyMemoryFirstMemoryOperandsMaskIndex = 2;
+const uint32_t kOpCopyMemorySizedFirstMemoryOperandsMaskIndex = 3;
+
+}  // namespace
+
+TransformationSetMemoryOperandsMask::TransformationSetMemoryOperandsMask(
+    const spvtools::fuzz::protobufs::TransformationSetMemoryOperandsMask&
+        message)
+    : message_(message) {}
+
+TransformationSetMemoryOperandsMask::TransformationSetMemoryOperandsMask(
+    const protobufs::InstructionDescriptor& memory_access_instruction,
+    uint32_t memory_operands_mask, uint32_t memory_operands_mask_index) {
+  *message_.mutable_memory_access_instruction() = memory_access_instruction;
+  message_.set_memory_operands_mask(memory_operands_mask);
+  message_.set_memory_operands_mask_index(memory_operands_mask_index);
+}
+
+bool TransformationSetMemoryOperandsMask::IsApplicable(
+    opt::IRContext* context,
+    const spvtools::fuzz::FactManager& /*unused*/) const {
+  if (message_.memory_operands_mask_index() != 0) {
+    // The following conditions should never be violated, even if
+    // transformations end up being replayed in a different way to the manner in
+    // which they were applied during fuzzing, hence why these are assertions
+    // rather than applicability checks.
+    assert(message_.memory_operands_mask_index() == 1);
+    assert(message_.memory_access_instruction().target_instruction_opcode() ==
+               SpvOpCopyMemory ||
+           message_.memory_access_instruction().target_instruction_opcode() ==
+               SpvOpCopyMemorySized);
+    assert(MultipleMemoryOperandMasksAreSupported(context));
+  }
+
+  auto instruction =
+      FindInstruction(message_.memory_access_instruction(), context);
+  if (!instruction) {
+    return false;
+  }
+  if (!IsMemoryAccess(*instruction)) {
+    return false;
+  }
+
+  auto original_mask_in_operand_index = GetInOperandIndexForMask(
+      *instruction, message_.memory_operands_mask_index());
+  assert(original_mask_in_operand_index != 0 &&
+         "The given mask index is not valid.");
+  uint32_t original_mask =
+      original_mask_in_operand_index < instruction->NumInOperands()
+          ? instruction->GetSingleWordInOperand(original_mask_in_operand_index)
+          : static_cast<uint32_t>(SpvMemoryAccessMaskNone);
+  uint32_t new_mask = message_.memory_operands_mask();
+
+  // Volatile must not be removed
+  if ((original_mask & SpvMemoryAccessVolatileMask) &&
+      !(new_mask & SpvMemoryAccessVolatileMask)) {
+    return false;
+  }
+
+  // Nontemporal can be added or removed, and no other flag is allowed to
+  // change.  We do this by checking that the masks are equal once we set
+  // their Volatile and Nontemporal flags to the same value (this works
+  // because valid manipulation of Volatile is checked above, and the manner
+  // in which Nontemporal is manipulated does not matter).
+  return (original_mask | SpvMemoryAccessVolatileMask |
+          SpvMemoryAccessNontemporalMask) ==
+         (new_mask | SpvMemoryAccessVolatileMask |
+          SpvMemoryAccessNontemporalMask);
+}
+
+void TransformationSetMemoryOperandsMask::Apply(
+    opt::IRContext* context, spvtools::fuzz::FactManager* /*unused*/) const {
+  auto instruction =
+      FindInstruction(message_.memory_access_instruction(), context);
+  auto original_mask_in_operand_index = GetInOperandIndexForMask(
+      *instruction, message_.memory_operands_mask_index());
+  // Either add a new operand, if no mask operand was already present, or
+  // replace an existing mask operand.
+  if (original_mask_in_operand_index >= instruction->NumInOperands()) {
+    instruction->AddOperand(
+        {SPV_OPERAND_TYPE_MEMORY_ACCESS, {message_.memory_operands_mask()}});
+
+  } else {
+    instruction->SetInOperand(original_mask_in_operand_index,
+                              {message_.memory_operands_mask()});
+  }
+}
+
+protobufs::Transformation TransformationSetMemoryOperandsMask::ToMessage()
+    const {
+  protobufs::Transformation result;
+  *result.mutable_set_memory_operands_mask() = message_;
+  return result;
+}
+
+bool TransformationSetMemoryOperandsMask::IsMemoryAccess(
+    const opt::Instruction& instruction) {
+  switch (instruction.opcode()) {
+    case SpvOpLoad:
+    case SpvOpStore:
+    case SpvOpCopyMemory:
+    case SpvOpCopyMemorySized:
+      return true;
+    default:
+      return false;
+  }
+}
+
+uint32_t TransformationSetMemoryOperandsMask::GetInOperandIndexForMask(
+    const opt::Instruction& instruction, uint32_t mask_index) {
+  // Get the input operand index associated with the first memory operands mask
+  // for the instruction.
+  uint32_t first_mask_in_operand_index = 0;
+  switch (instruction.opcode()) {
+    case SpvOpLoad:
+      first_mask_in_operand_index = kOpLoadMemoryOperandsMaskIndex;
+      break;
+    case SpvOpStore:
+      first_mask_in_operand_index = kOpStoreMemoryOperandsMaskIndex;
+      break;
+    case SpvOpCopyMemory:
+      first_mask_in_operand_index = kOpCopyMemoryFirstMemoryOperandsMaskIndex;
+      break;
+    case SpvOpCopyMemorySized:
+      first_mask_in_operand_index =
+          kOpCopyMemorySizedFirstMemoryOperandsMaskIndex;
+      break;
+    default:
+      assert(false && "Unknown memory instruction.");
+      break;
+  }
+  // If we are looking for the input operand index of the first mask, return it.
+  if (mask_index == 0) {
+    return first_mask_in_operand_index;
+  }
+  assert(mask_index == 1 && "Memory operands mask index must be 0 or 1.");
+
+  // We are looking for the input operand index of the second mask.  This is a
+  // little complicated because, depending on the contents of the first mask,
+  // there may be some input operands separating the two masks.
+  uint32_t first_mask =
+      instruction.GetSingleWordInOperand(first_mask_in_operand_index);
+
+  // Consider each bit that might have an associated extra input operand, and
+  // count how many there are expected to be.
+  uint32_t first_mask_extra_operand_count = 0;
+  for (auto mask_bit :
+       {SpvMemoryAccessAlignedMask, SpvMemoryAccessMakePointerAvailableMask,
+        SpvMemoryAccessMakePointerAvailableKHRMask,
+        SpvMemoryAccessMakePointerVisibleMask,
+        SpvMemoryAccessMakePointerVisibleKHRMask}) {
+    if (first_mask & mask_bit) {
+      first_mask_extra_operand_count++;
+    }
+  }
+  return first_mask_in_operand_index + first_mask_extra_operand_count + 1;
+}
+
+bool TransformationSetMemoryOperandsMask::
+    MultipleMemoryOperandMasksAreSupported(opt::IRContext* context) {
+  // TODO(afd): We capture the universal environments for which this loop
+  //  control is definitely not supported.  The check should be refined on
+  //  demand for other target environments.
+  switch (context->grammar().target_env()) {
+    case SPV_ENV_UNIVERSAL_1_0:
+    case SPV_ENV_UNIVERSAL_1_1:
+    case SPV_ENV_UNIVERSAL_1_2:
+    case SPV_ENV_UNIVERSAL_1_3:
+      return false;
+    default:
+      return true;
+  }
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/transformation_set_memory_operands_mask.h b/source/fuzz/transformation_set_memory_operands_mask.h
new file mode 100644
index 0000000..20ae145
--- /dev/null
+++ b/source/fuzz/transformation_set_memory_operands_mask.h
@@ -0,0 +1,76 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_FUZZ_TRANSFORMATION_SET_MEMORY_OPERANDS_MASK_H_
+#define SOURCE_FUZZ_TRANSFORMATION_SET_MEMORY_OPERANDS_MASK_H_
+
+#include "source/fuzz/fact_manager.h"
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/transformation.h"
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace fuzz {
+
+class TransformationSetMemoryOperandsMask : public Transformation {
+ public:
+  explicit TransformationSetMemoryOperandsMask(
+      const protobufs::TransformationSetMemoryOperandsMask& message);
+
+  TransformationSetMemoryOperandsMask(
+      const protobufs::InstructionDescriptor& memory_access_instruction,
+      uint32_t memory_operands_mask, uint32_t memory_operands_mask_index);
+
+  // - |message_.memory_access_instruction| must describe a memory access
+  //   instruction.
+  // - |message_.memory_operands_mask_index| must be suitable for this memory
+  //   access instruction, e.g. it must be 0 in the case of OpLoad, and may be
+  //   1 in the case of OpCopyMemory if the SPIR-V version is 1.4 or higher.
+  // - |message_.memory_operands_mask| must be identical to the original memory
+  //   operands mask, except that Volatile may be added, and Nontemporal may be
+  //   toggled.
+  bool IsApplicable(opt::IRContext* context,
+                    const FactManager& fact_manager) const override;
+
+  // Replaces the operands mask identified by
+  // |message_.memory_operands_mask_index| in the instruction described by
+  // |message_.memory_access_instruction| with |message_.memory_operands_mask|,
+  // creating an input operand for the mask if no such operand was present.
+  void Apply(opt::IRContext* context, FactManager* fact_manager) const override;
+
+  protobufs::Transformation ToMessage() const override;
+
+  // Helper function that determines whether |instruction| is a memory
+  // instruction (e.g. OpLoad).
+  static bool IsMemoryAccess(const opt::Instruction& instruction);
+
+  // Does the version of SPIR-V being used support multiple memory operand
+  // masks on relevant memory access instructions?
+  static bool MultipleMemoryOperandMasksAreSupported(opt::IRContext* context);
+
+  // Helper function to get the input operand index associated with mask number
+  // |mask_index|. This is a bit tricky if there are multiple masks, because the
+  // index associated with the second mask depends on whether the first mask
+  // includes any flags such as Aligned that have corresponding operands.
+  static uint32_t GetInOperandIndexForMask(const opt::Instruction& instruction,
+                                           uint32_t mask_index);
+
+ private:
+  protobufs::TransformationSetMemoryOperandsMask message_;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_TRANSFORMATION_SET_MEMORY_OPERANDS_MASK_H_
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
index cd22426..5b913b9 100644
--- a/test/fuzz/CMakeLists.txt
+++ b/test/fuzz/CMakeLists.txt
@@ -20,6 +20,7 @@
           fact_manager_test.cpp
           fuzz_test_util.cpp
           fuzzer_pass_add_useful_constructs_test.cpp
+          instruction_descriptor_test.cpp
           transformation_add_constant_boolean_test.cpp
           transformation_add_constant_scalar_test.cpp
           transformation_add_dead_break_test.cpp
@@ -37,6 +38,7 @@
           transformation_replace_id_with_synonym_test.cpp
           transformation_set_function_control_test.cpp
           transformation_set_loop_control_test.cpp
+          transformation_set_memory_operands_mask_test.cpp
           transformation_set_selection_control_test.cpp
           transformation_split_block_test.cpp
           uniform_buffer_element_descriptor_test.cpp)
diff --git a/test/fuzz/instruction_descriptor_test.cpp b/test/fuzz/instruction_descriptor_test.cpp
new file mode 100644
index 0000000..5165cfb
--- /dev/null
+++ b/test/fuzz/instruction_descriptor_test.cpp
@@ -0,0 +1,69 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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 "source/fuzz/instruction_descriptor.h"
+#include "test/fuzz/fuzz_test_util.h"
+
+namespace spvtools {
+namespace fuzz {
+namespace {
+
+TEST(InstructionDescriptorTest, BasicTest) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 0
+          %7 = OpTypePointer Function %6
+          %9 = OpConstant %6 0
+         %10 = OpTypeInt 32 1
+         %11 = OpTypePointer Function %10
+         %13 = OpConstant %10 2
+         %32 = OpConstant %10 0
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+        %164 = OpVariable %11 Function
+        %165 = OpVariable %11 Function
+               OpBranch %16
+         %16 = OpLabel
+               OpStore %164 %32
+               OpStore %165 %13
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_4;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  for (auto& function : *context->module()) {
+    for (auto& block : function) {
+      for (auto inst_it = block.cbegin(); inst_it != block.cend(); ++inst_it) {
+        ASSERT_EQ(&*inst_it,
+                  FindInstruction(MakeInstructionDescriptor(block, inst_it),
+                                  context.get()));
+      }
+    }
+  }
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/test/fuzz/transformation_set_memory_operands_mask_test.cpp b/test/fuzz/transformation_set_memory_operands_mask_test.cpp
new file mode 100644
index 0000000..ad4dc25
--- /dev/null
+++ b/test/fuzz/transformation_set_memory_operands_mask_test.cpp
@@ -0,0 +1,432 @@
+// Copyright (c) 2019 Google LLC
+//
+// 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 "source/fuzz/transformation_set_memory_operands_mask.h"
+#include "source/fuzz/instruction_descriptor.h"
+#include "test/fuzz/fuzz_test_util.h"
+
+namespace spvtools {
+namespace fuzz {
+namespace {
+
+TEST(TransformationSetMemoryOperandsMaskTest, PreSpirv14) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %7 "Point3D"
+               OpMemberName %7 0 "x"
+               OpMemberName %7 1 "y"
+               OpMemberName %7 2 "z"
+               OpName %12 "global_points"
+               OpName %15 "block"
+               OpMemberName %15 0 "in_points"
+               OpMemberName %15 1 "in_point"
+               OpName %17 ""
+               OpName %133 "local_points"
+               OpMemberDecorate %7 0 Offset 0
+               OpMemberDecorate %7 1 Offset 4
+               OpMemberDecorate %7 2 Offset 8
+               OpDecorate %10 ArrayStride 16
+               OpMemberDecorate %15 0 Offset 0
+               OpMemberDecorate %15 1 Offset 192
+               OpDecorate %15 Block
+               OpDecorate %17 DescriptorSet 0
+               OpDecorate %17 Binding 0
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeStruct %6 %6 %6
+          %8 = OpTypeInt 32 0
+          %9 = OpConstant %8 12
+         %10 = OpTypeArray %7 %9
+         %11 = OpTypePointer Private %10
+         %12 = OpVariable %11 Private
+         %15 = OpTypeStruct %10 %7
+         %16 = OpTypePointer Uniform %15
+         %17 = OpVariable %16 Uniform
+         %18 = OpTypeInt 32 1
+         %19 = OpConstant %18 0
+         %20 = OpTypePointer Uniform %10
+         %24 = OpTypePointer Private %7
+         %27 = OpTypePointer Private %6
+         %30 = OpConstant %18 1
+        %132 = OpTypePointer Function %10
+        %135 = OpTypePointer Uniform %7
+        %145 = OpTypePointer Function %7
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+        %133 = OpVariable %132 Function
+         %21 = OpAccessChain %20 %17 %19
+               OpCopyMemory %12 %21 Aligned 16
+               OpCopyMemory %133 %12 Volatile
+        %136 = OpAccessChain %135 %17 %30
+        %138 = OpAccessChain %24 %12 %19
+               OpCopyMemory %138 %136 None
+        %146 = OpAccessChain %145 %133 %30
+        %147 = OpLoad %7 %146 Volatile|Nontemporal|Aligned 16
+        %148 = OpAccessChain %24 %12 %19
+               OpStore %148 %147 Nontemporal
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  // Not OK: the instruction is not a memory access.
+  ASSERT_FALSE(TransformationSetMemoryOperandsMask(
+                   MakeInstructionDescriptor(21, SpvOpAccessChain, 0),
+                   SpvMemoryAccessMaskNone, 0)
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Not OK to remove Aligned
+  ASSERT_FALSE(TransformationSetMemoryOperandsMask(
+                   MakeInstructionDescriptor(147, SpvOpLoad, 0),
+                   SpvMemoryAccessVolatileMask | SpvMemoryAccessNontemporalMask,
+                   0)
+                   .IsApplicable(context.get(), fact_manager));
+
+  TransformationSetMemoryOperandsMask transformation1(
+      MakeInstructionDescriptor(147, SpvOpLoad, 0),
+      SpvMemoryAccessAlignedMask | SpvMemoryAccessVolatileMask, 0);
+  ASSERT_TRUE(transformation1.IsApplicable(context.get(), fact_manager));
+  transformation1.Apply(context.get(), &fact_manager);
+
+  // Not OK to remove Aligned
+  ASSERT_FALSE(TransformationSetMemoryOperandsMask(
+                   MakeInstructionDescriptor(21, SpvOpCopyMemory, 0),
+                   SpvMemoryAccessMaskNone, 0)
+                   .IsApplicable(context.get(), fact_manager));
+
+  // OK: leaves the mask as is
+  ASSERT_TRUE(TransformationSetMemoryOperandsMask(
+                  MakeInstructionDescriptor(21, SpvOpCopyMemory, 0),
+                  SpvMemoryAccessAlignedMask, 0)
+                  .IsApplicable(context.get(), fact_manager));
+
+  // OK: adds Nontemporal and Volatile
+  TransformationSetMemoryOperandsMask transformation2(
+      MakeInstructionDescriptor(21, SpvOpCopyMemory, 0),
+      SpvMemoryAccessAlignedMask | SpvMemoryAccessNontemporalMask |
+          SpvMemoryAccessVolatileMask,
+      0);
+  ASSERT_TRUE(transformation2.IsApplicable(context.get(), fact_manager));
+  transformation2.Apply(context.get(), &fact_manager);
+
+  // Not OK to remove Volatile
+  ASSERT_FALSE(TransformationSetMemoryOperandsMask(
+                   MakeInstructionDescriptor(21, SpvOpCopyMemory, 1),
+                   SpvMemoryAccessNontemporalMask, 0)
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Not OK to add Aligned
+  ASSERT_FALSE(TransformationSetMemoryOperandsMask(
+                   MakeInstructionDescriptor(21, SpvOpCopyMemory, 1),
+                   SpvMemoryAccessAlignedMask | SpvMemoryAccessVolatileMask, 0)
+                   .IsApplicable(context.get(), fact_manager));
+
+  // OK: adds Nontemporal
+  TransformationSetMemoryOperandsMask transformation3(
+      MakeInstructionDescriptor(21, SpvOpCopyMemory, 1),
+      SpvMemoryAccessNontemporalMask | SpvMemoryAccessVolatileMask, 0);
+  ASSERT_TRUE(transformation3.IsApplicable(context.get(), fact_manager));
+  transformation3.Apply(context.get(), &fact_manager);
+
+  // OK: adds Nontemporal and Volatile
+  TransformationSetMemoryOperandsMask transformation4(
+      MakeInstructionDescriptor(138, SpvOpCopyMemory, 0),
+      SpvMemoryAccessNontemporalMask | SpvMemoryAccessVolatileMask, 0);
+  ASSERT_TRUE(transformation4.IsApplicable(context.get(), fact_manager));
+  transformation4.Apply(context.get(), &fact_manager);
+
+  // OK: removes Nontemporal, adds Volatile
+  TransformationSetMemoryOperandsMask transformation5(
+      MakeInstructionDescriptor(148, SpvOpStore, 0),
+      SpvMemoryAccessVolatileMask, 0);
+  ASSERT_TRUE(transformation5.IsApplicable(context.get(), fact_manager));
+  transformation5.Apply(context.get(), &fact_manager);
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %7 "Point3D"
+               OpMemberName %7 0 "x"
+               OpMemberName %7 1 "y"
+               OpMemberName %7 2 "z"
+               OpName %12 "global_points"
+               OpName %15 "block"
+               OpMemberName %15 0 "in_points"
+               OpMemberName %15 1 "in_point"
+               OpName %17 ""
+               OpName %133 "local_points"
+               OpMemberDecorate %7 0 Offset 0
+               OpMemberDecorate %7 1 Offset 4
+               OpMemberDecorate %7 2 Offset 8
+               OpDecorate %10 ArrayStride 16
+               OpMemberDecorate %15 0 Offset 0
+               OpMemberDecorate %15 1 Offset 192
+               OpDecorate %15 Block
+               OpDecorate %17 DescriptorSet 0
+               OpDecorate %17 Binding 0
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeStruct %6 %6 %6
+          %8 = OpTypeInt 32 0
+          %9 = OpConstant %8 12
+         %10 = OpTypeArray %7 %9
+         %11 = OpTypePointer Private %10
+         %12 = OpVariable %11 Private
+         %15 = OpTypeStruct %10 %7
+         %16 = OpTypePointer Uniform %15
+         %17 = OpVariable %16 Uniform
+         %18 = OpTypeInt 32 1
+         %19 = OpConstant %18 0
+         %20 = OpTypePointer Uniform %10
+         %24 = OpTypePointer Private %7
+         %27 = OpTypePointer Private %6
+         %30 = OpConstant %18 1
+        %132 = OpTypePointer Function %10
+        %135 = OpTypePointer Uniform %7
+        %145 = OpTypePointer Function %7
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+        %133 = OpVariable %132 Function
+         %21 = OpAccessChain %20 %17 %19
+               OpCopyMemory %12 %21 Aligned|Nontemporal|Volatile 16
+               OpCopyMemory %133 %12 Nontemporal|Volatile
+        %136 = OpAccessChain %135 %17 %30
+        %138 = OpAccessChain %24 %12 %19
+               OpCopyMemory %138 %136 Nontemporal|Volatile
+        %146 = OpAccessChain %145 %133 %30
+        %147 = OpLoad %7 %146 Aligned|Volatile 16
+        %148 = OpAccessChain %24 %12 %19
+               OpStore %148 %147 Volatile
+               OpReturn
+               OpFunctionEnd
+  )";
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationSetMemoryOperandsMaskTest, Spirv14) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main" %12 %17
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %7 "Point3D"
+               OpMemberName %7 0 "x"
+               OpMemberName %7 1 "y"
+               OpMemberName %7 2 "z"
+               OpName %12 "global_points"
+               OpName %15 "block"
+               OpMemberName %15 0 "in_points"
+               OpMemberName %15 1 "in_point"
+               OpName %17 ""
+               OpName %133 "local_points"
+               OpMemberDecorate %7 0 Offset 0
+               OpMemberDecorate %7 1 Offset 4
+               OpMemberDecorate %7 2 Offset 8
+               OpDecorate %10 ArrayStride 16
+               OpMemberDecorate %15 0 Offset 0
+               OpMemberDecorate %15 1 Offset 192
+               OpDecorate %15 Block
+               OpDecorate %17 DescriptorSet 0
+               OpDecorate %17 Binding 0
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeStruct %6 %6 %6
+          %8 = OpTypeInt 32 0
+          %9 = OpConstant %8 12
+         %10 = OpTypeArray %7 %9
+         %11 = OpTypePointer Private %10
+         %12 = OpVariable %11 Private
+         %15 = OpTypeStruct %10 %7
+         %16 = OpTypePointer Uniform %15
+         %17 = OpVariable %16 Uniform
+         %18 = OpTypeInt 32 1
+         %19 = OpConstant %18 0
+         %20 = OpTypePointer Uniform %10
+         %24 = OpTypePointer Private %7
+         %27 = OpTypePointer Private %6
+         %30 = OpConstant %18 1
+        %132 = OpTypePointer Function %10
+        %135 = OpTypePointer Uniform %7
+        %145 = OpTypePointer Function %7
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+        %133 = OpVariable %132 Function
+         %21 = OpAccessChain %20 %17 %19
+               OpCopyMemory %12 %21 Aligned 16 Nontemporal|Aligned 16
+               OpCopyMemory %133 %12 Volatile
+        %136 = OpAccessChain %135 %17 %30
+        %138 = OpAccessChain %24 %12 %19
+               OpCopyMemory %138 %136 None Aligned 16
+               OpCopyMemory %138 %136 Aligned 16
+        %146 = OpAccessChain %145 %133 %30
+        %147 = OpLoad %7 %146 Volatile|Nontemporal|Aligned 16
+        %148 = OpAccessChain %24 %12 %19
+               OpStore %148 %147 Nontemporal
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_4;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  TransformationSetMemoryOperandsMask transformation1(
+      MakeInstructionDescriptor(21, SpvOpCopyMemory, 0),
+      SpvMemoryAccessAlignedMask | SpvMemoryAccessVolatileMask, 1);
+  // Bad: cannot remove aligned
+  ASSERT_FALSE(TransformationSetMemoryOperandsMask(
+                   MakeInstructionDescriptor(21, SpvOpCopyMemory, 0),
+                   SpvMemoryAccessVolatileMask, 1)
+                   .IsApplicable(context.get(), fact_manager));
+  ASSERT_TRUE(transformation1.IsApplicable(context.get(), fact_manager));
+  transformation1.Apply(context.get(), &fact_manager);
+
+  TransformationSetMemoryOperandsMask transformation2(
+      MakeInstructionDescriptor(21, SpvOpCopyMemory, 1),
+      SpvMemoryAccessNontemporalMask | SpvMemoryAccessVolatileMask, 1);
+  // Bad: cannot remove volatile
+  ASSERT_FALSE(TransformationSetMemoryOperandsMask(
+                   MakeInstructionDescriptor(21, SpvOpCopyMemory, 1),
+                   SpvMemoryAccessNontemporalMask, 0)
+                   .IsApplicable(context.get(), fact_manager));
+  ASSERT_TRUE(transformation2.IsApplicable(context.get(), fact_manager));
+  transformation2.Apply(context.get(), &fact_manager);
+
+  TransformationSetMemoryOperandsMask transformation3(
+      MakeInstructionDescriptor(138, SpvOpCopyMemory, 0),
+      SpvMemoryAccessAlignedMask | SpvMemoryAccessNontemporalMask, 1);
+  // Bad: the first mask is None, so Aligned cannot be added to it.
+  ASSERT_FALSE(TransformationSetMemoryOperandsMask(
+                   MakeInstructionDescriptor(138, SpvOpCopyMemory, 0),
+                   SpvMemoryAccessAlignedMask | SpvMemoryAccessNontemporalMask,
+                   0)
+                   .IsApplicable(context.get(), fact_manager));
+  ASSERT_TRUE(transformation3.IsApplicable(context.get(), fact_manager));
+  transformation3.Apply(context.get(), &fact_manager);
+
+  TransformationSetMemoryOperandsMask transformation4(
+      MakeInstructionDescriptor(138, SpvOpCopyMemory, 1),
+      SpvMemoryAccessVolatileMask, 1);
+  ASSERT_TRUE(transformation4.IsApplicable(context.get(), fact_manager));
+  transformation4.Apply(context.get(), &fact_manager);
+
+  TransformationSetMemoryOperandsMask transformation5(
+      MakeInstructionDescriptor(147, SpvOpLoad, 0),
+      SpvMemoryAccessVolatileMask | SpvMemoryAccessAlignedMask, 0);
+  ASSERT_TRUE(transformation5.IsApplicable(context.get(), fact_manager));
+  transformation5.Apply(context.get(), &fact_manager);
+
+  TransformationSetMemoryOperandsMask transformation6(
+      MakeInstructionDescriptor(148, SpvOpStore, 0), SpvMemoryAccessMaskNone,
+      0);
+  ASSERT_TRUE(transformation6.IsApplicable(context.get(), fact_manager));
+  transformation6.Apply(context.get(), &fact_manager);
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main" %12 %17
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %7 "Point3D"
+               OpMemberName %7 0 "x"
+               OpMemberName %7 1 "y"
+               OpMemberName %7 2 "z"
+               OpName %12 "global_points"
+               OpName %15 "block"
+               OpMemberName %15 0 "in_points"
+               OpMemberName %15 1 "in_point"
+               OpName %17 ""
+               OpName %133 "local_points"
+               OpMemberDecorate %7 0 Offset 0
+               OpMemberDecorate %7 1 Offset 4
+               OpMemberDecorate %7 2 Offset 8
+               OpDecorate %10 ArrayStride 16
+               OpMemberDecorate %15 0 Offset 0
+               OpMemberDecorate %15 1 Offset 192
+               OpDecorate %15 Block
+               OpDecorate %17 DescriptorSet 0
+               OpDecorate %17 Binding 0
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeStruct %6 %6 %6
+          %8 = OpTypeInt 32 0
+          %9 = OpConstant %8 12
+         %10 = OpTypeArray %7 %9
+         %11 = OpTypePointer Private %10
+         %12 = OpVariable %11 Private
+         %15 = OpTypeStruct %10 %7
+         %16 = OpTypePointer Uniform %15
+         %17 = OpVariable %16 Uniform
+         %18 = OpTypeInt 32 1
+         %19 = OpConstant %18 0
+         %20 = OpTypePointer Uniform %10
+         %24 = OpTypePointer Private %7
+         %27 = OpTypePointer Private %6
+         %30 = OpConstant %18 1
+        %132 = OpTypePointer Function %10
+        %135 = OpTypePointer Uniform %7
+        %145 = OpTypePointer Function %7
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+        %133 = OpVariable %132 Function
+         %21 = OpAccessChain %20 %17 %19
+               OpCopyMemory %12 %21 Aligned 16 Aligned|Volatile 16
+               OpCopyMemory %133 %12 Volatile Nontemporal|Volatile
+        %136 = OpAccessChain %135 %17 %30
+        %138 = OpAccessChain %24 %12 %19
+               OpCopyMemory %138 %136 None Aligned|Nontemporal 16
+               OpCopyMemory %138 %136 Aligned 16 Volatile
+        %146 = OpAccessChain %145 %133 %30
+        %147 = OpLoad %7 %146 Volatile|Aligned 16
+        %148 = OpAccessChain %24 %12 %19
+               OpStore %148 %147 None
+               OpReturn
+               OpFunctionEnd
+  )";
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools