Fix freed memory access due to DestroyObjects.

- Use a while loop to pop the list instead of iterating it and deleting at the same time.
- Adds regression tests to verify that the fix works.

Bug: chromium:1318792
Change-Id: I84fb494c64b07d3e1bd2b5b3af7cb9f82eee28b0
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/88043
Reviewed-by: Austin Eng <enga@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Loko Kung <lokokung@google.com>
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 5f92015..b8048c3 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -287,10 +287,10 @@
 
     void DeviceBase::DestroyObjects() {
         // List of object types in reverse "dependency" order so we can iterate and delete the
-        // objects safely starting at leaf objects. We define dependent here such that if B has
-        // a ref to A, then B depends on A. We therefore try to destroy B before destroying A. Note
-        // that this only considers the immediate frontend dependencies, while backend objects could
-        // add complications and extra dependencies.
+        // objects safely. We define dependent here such that if B has a ref to A, then B depends on
+        // A. We therefore try to destroy B before destroying A. Note that this only considers the
+        // immediate frontend dependencies, while backend objects could add complications and extra
+        // dependencies.
         //
         // Note that AttachmentState is not an ApiObject so it cannot be eagerly destroyed. However,
         // since AttachmentStates are cached by the device, objects that hold references to
@@ -330,8 +330,9 @@
             const std::lock_guard<std::mutex> lock(objList.mutex);
             objList.objects.MoveInto(&objects);
         }
-        for (LinkNode<ApiObjectBase>* node : objects) {
-            node->value()->Destroy();
+        while (!objects.empty()) {
+            // The destroy call should also remove the object from the list.
+            objects.head()->value()->Destroy();
         }
     }
 
diff --git a/src/dawn/tests/unittests/native/DestroyObjectTests.cpp b/src/dawn/tests/unittests/native/DestroyObjectTests.cpp
index e70682d0..47481c5 100644
--- a/src/dawn/tests/unittests/native/DestroyObjectTests.cpp
+++ b/src/dawn/tests/unittests/native/DestroyObjectTests.cpp
@@ -17,6 +17,7 @@
 #include "dawn/native/Toggles.h"
 #include "dawn/tests/DawnNativeTest.h"
 #include "dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "dawn/utils/WGPUHelpers.h"
 #include "mocks/BindGroupLayoutMock.h"
 #include "mocks/BindGroupMock.h"
 #include "mocks/BufferMock.h"
@@ -763,6 +764,64 @@
         EXPECT_FALSE(textureView->IsAlive());
     }
 
+    static constexpr std::string_view kComputeShader = R"(
+        @stage(compute) @workgroup_size(1) fn main() {}
+    )";
+
+    static constexpr std::string_view kVertexShader = R"(
+        @stage(vertex) fn main() -> @builtin(position) vec4<f32> {
+            return vec4<f32>(0.0, 0.0, 0.0, 0.0);
+        }
+    )";
+
+    static constexpr std::string_view kFragmentShader = R"(
+        @stage(fragment) fn main() {}
+    )";
+
+    class DestroyObjectRegressionTests : public DawnNativeTest {};
+
+    // LastRefInCommand* tests are regression test(s) for https://crbug.com/chromium/1318792. The
+    // regression tests here are not exhuastive. In order to have an exhuastive test case for this
+    // class of failures, we should test every possible command with the commands holding the last
+    // references (or as last as possible) of their needed objects. For now, including simple cases
+    // including a stripped-down case from the original bug.
+
+    // Tests that when a RenderPipeline's last reference is held in a command in an unfinished
+    // CommandEncoder, that destroying the device still works as expected (and does not cause
+    // double-free).
+    TEST_F(DestroyObjectRegressionTests, LastRefInCommandRenderPipeline) {
+        utils::BasicRenderPass pass = utils::CreateBasicRenderPass(device, 1, 1);
+
+        utils::ComboRenderPassDescriptor passDesc{};
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::RenderPassEncoder renderEncoder = encoder.BeginRenderPass(&pass.renderPassInfo);
+
+        utils::ComboRenderPipelineDescriptor pipelineDesc;
+        pipelineDesc.cTargets[0].writeMask = wgpu::ColorWriteMask::None;
+        pipelineDesc.vertex.module = utils::CreateShaderModule(device, kVertexShader.data());
+        pipelineDesc.vertex.entryPoint = "main";
+        pipelineDesc.cFragment.module = utils::CreateShaderModule(device, kFragmentShader.data());
+        pipelineDesc.cFragment.entryPoint = "main";
+        renderEncoder.SetPipeline(device.CreateRenderPipeline(&pipelineDesc));
+
+        device.Destroy();
+    }
+
+    // Tests that when a ComputePipelines's last reference is held in a command in an unfinished
+    // CommandEncoder, that destroying the device still works as expected (and does not cause
+    // double-free).
+    TEST_F(DestroyObjectRegressionTests, LastRefInCommandComputePipeline) {
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        wgpu::ComputePassEncoder computeEncoder = encoder.BeginComputePass();
+
+        wgpu::ComputePipelineDescriptor pipelineDesc;
+        pipelineDesc.compute.module = utils::CreateShaderModule(device, kComputeShader.data());
+        pipelineDesc.compute.entryPoint = "main";
+        computeEncoder.SetPipeline(device.CreateComputePipeline(&pipelineDesc));
+
+        device.Destroy();
+    }
+
     // TODO(https://crbug.com/dawn/1381) Remove when namespaces are not indented.
     // NOLINTNEXTLINE(readability/namespace)
 }}  // namespace dawn::native::