tint: Implement pointer alias analysis

Track reads and writes to pointer parameters for each function in the
Resolver, as well as accesses to module-scope variables. At function
call sites, check the root identifiers of each pointer argument to
determine if problematic aliasing occurs.

The MSL backend passes pointers to sub-objects to functions when
handling workgroup storage variables, which triggers the alias
analysis. Add a validation override for this scenario.

Bug: tint:1675

Change-Id: I81a40d1309df65521cc5ad39764d6a09a260f51e
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/110167
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: James Price <jrprice@google.com>
diff --git a/docs/tint/origin-trial-changes.md b/docs/tint/origin-trial-changes.md
index 8aaaeb3..a55e39f 100644
--- a/docs/tint/origin-trial-changes.md
+++ b/docs/tint/origin-trial-changes.md
@@ -5,6 +5,8 @@
 ### Deprecated Features
 
 * The `sig` member of the return type of `frexp()` has been renamed to `fract`. [tint:1757](crbug.com/tint/1757)
+* Calling a function with multiple pointer arguments that alias each other is now a warning, and
+  will become an error in a future release. [tint:1675](crbug.com/tint/1675)
 
 ## Changes for M109
 
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 36fd66b..2ced868 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -1116,6 +1116,7 @@
     sources = [
       "resolver/address_space_layout_validation_test.cc",
       "resolver/address_space_validation_test.cc",
+      "resolver/alias_analysis_test.cc",
       "resolver/array_accessor_test.cc",
       "resolver/assignment_validation_test.cc",
       "resolver/atomics_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 978986e..f26c138 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -840,6 +840,7 @@
     program_builder_test.cc
     program_test.cc
     reflection_test.cc
+    resolver/alias_analysis_test.cc
     resolver/array_accessor_test.cc
     resolver/assignment_validation_test.cc
     resolver/atomics_test.cc
diff --git a/src/tint/ast/disable_validation_attribute.cc b/src/tint/ast/disable_validation_attribute.cc
index 2ae8c16..465a0ec 100644
--- a/src/tint/ast/disable_validation_attribute.cc
+++ b/src/tint/ast/disable_validation_attribute.cc
@@ -43,6 +43,8 @@
             return "disable_validation__ignore_stride";
         case DisabledValidation::kIgnoreInvalidPointerArgument:
             return "disable_validation__ignore_invalid_pointer_argument";
+        case DisabledValidation::kIgnorePointerAliasing:
+            return "disable_validation__ignore_pointer_aliasing";
     }
     return "<invalid>";
 }
diff --git a/src/tint/ast/disable_validation_attribute.h b/src/tint/ast/disable_validation_attribute.h
index d4c7455..f52b107 100644
--- a/src/tint/ast/disable_validation_attribute.h
+++ b/src/tint/ast/disable_validation_attribute.h
@@ -42,6 +42,9 @@
     /// When applied to a pointer function parameter, the validator will not require a function call
     /// argument passed for that parameter to have a certain form.
     kIgnoreInvalidPointerArgument,
+    /// When applied to a function declaration, the validator will not complain if multiple
+    /// pointer arguments alias when that function is called.
+    kIgnorePointerAliasing,
 };
 
 /// An internal attribute used to tell the validator to ignore specific
diff --git a/src/tint/resolver/alias_analysis_test.cc b/src/tint/resolver/alias_analysis_test.cc
new file mode 100644
index 0000000..38710b6
--- /dev/null
+++ b/src/tint/resolver/alias_analysis_test.cc
@@ -0,0 +1,897 @@
+// Copyright 2022 The Tint Authors.
+//
+// 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 "src/tint/resolver/resolver.h"
+#include "src/tint/resolver/resolver_test_helper.h"
+
+#include "gmock/gmock.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+struct ResolverAliasAnalysisTest : public resolver::TestHelper, public testing::Test {};
+
+// Base test harness for tests that pass two pointers to a function.
+//
+// fn target(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
+//   <test statements>
+// }
+// fn caller() {
+//   var v1 : i32;
+//   var v2 : i32;
+//   target(&v1, aliased ? &v1 : &v2);
+// }
+struct TwoPointerConfig {
+    ast::AddressSpace address_space;  // The address space for the pointers.
+    bool aliased;                     // Whether the pointers alias or not.
+};
+class TwoPointers : public ResolverTestWithParam<TwoPointerConfig> {
+  protected:
+    void SetUp() override {
+        utils::Vector<const ast::Statement*, 4> body;
+        if (GetParam().address_space == ast::AddressSpace::kFunction) {
+            body.Push(Decl(Var("v1", ty.i32())));
+            body.Push(Decl(Var("v2", ty.i32())));
+        } else {
+            GlobalVar("v1", ast::AddressSpace::kPrivate, ty.i32());
+            GlobalVar("v2", ast::AddressSpace::kPrivate, ty.i32());
+        }
+        body.Push(CallStmt(Call("target", AddressOf(Source{{12, 34}}, "v1"),
+                                AddressOf(Source{{56, 78}}, GetParam().aliased ? "v1" : "v2"))));
+        Func("caller", utils::Empty, ty.void_(), body);
+    }
+
+    void Run(utils::Vector<const ast::Statement*, 4>&& body, const char* err = nullptr) {
+        auto addrspace = GetParam().address_space;
+        Func("target",
+             utils::Vector{
+                 Param("p1", ty.pointer<i32>(addrspace)),
+                 Param("p2", ty.pointer<i32>(addrspace)),
+             },
+             ty.void_(), std::move(body));
+        if (GetParam().aliased && err) {
+            EXPECT_TRUE(r()->Resolve());
+            EXPECT_EQ(r()->error(), err);
+        } else {
+            EXPECT_TRUE(r()->Resolve()) << r()->error();
+        }
+    }
+};
+
+TEST_P(TwoPointers, ReadRead) {
+    // _ = *p1;
+    // _ = *p2;
+    Run({
+        Assign(Phony(), Deref("p1")),
+        Assign(Phony(), Deref("p2")),
+    });
+}
+
+TEST_P(TwoPointers, ReadWrite) {
+    // _ = *p1;
+    // *p2 = 42;
+    Run(
+        {
+            Assign(Phony(), Deref("p1")),
+            Assign(Deref("p2"), 42_a),
+        },
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(TwoPointers, WriteRead) {
+    // *p1 = 42;
+    // _ = *p2;
+    Run(
+        {
+            Assign(Deref("p1"), 42_a),
+            Assign(Phony(), Deref("p2")),
+        },
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(TwoPointers, WriteWrite) {
+    // *p1 = 42;
+    // *p2 = 42;
+    Run(
+        {
+            Assign(Deref("p1"), 42_a),
+            Assign(Deref("p2"), 42_a),
+        },
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(TwoPointers, ReadWriteThroughChain) {
+    // fn f2(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
+    //   _ = *p1;
+    //   *p2 = 42;
+    // }
+    // fn f1(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
+    //   f2(p1, p2);
+    // }
+    //
+    // f1(p1, p2);
+    Func("f2",
+         utils::Vector{
+             Param("p1", ty.pointer<i32>(GetParam().address_space)),
+             Param("p2", ty.pointer<i32>(GetParam().address_space)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Phony(), Deref("p1")),
+             Assign(Deref("p2"), 42_a),
+         });
+    Func("f1",
+         utils::Vector{
+             Param("p1", ty.pointer<i32>(GetParam().address_space)),
+             Param("p2", ty.pointer<i32>(GetParam().address_space)),
+         },
+         ty.void_(),
+         utils::Vector{
+             CallStmt(Call("f2", "p1", "p2")),
+         });
+    Run(
+        {
+            CallStmt(Call("f1", "p1", "p2")),
+        },
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(TwoPointers, ReadWriteAcrossDifferentFunctions) {
+    // fn f1(p1 : ptr<function, i32>) {
+    //   _ = *p1;
+    // }
+    // fn f2(p2 : ptr<function, i32>) {
+    //   *p2 = 42;
+    // }
+    //
+    // f1(p1);
+    // f2(p2);
+    Func("f1",
+         utils::Vector<const ast::Parameter*, 4>{
+             Param("p1", ty.pointer<i32>(GetParam().address_space)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Phony(), Deref("p1")),
+         });
+    Func("f2",
+         utils::Vector<const ast::Parameter*, 4>{
+             Param("p2", ty.pointer<i32>(GetParam().address_space)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Deref("p2"), 42_a),
+         });
+    Run(
+        {
+            CallStmt(Call("f1", "p1")),
+            CallStmt(Call("f2", "p2")),
+        },
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
+                         TwoPointers,
+                         ::testing::Values(TwoPointerConfig{ast::AddressSpace::kFunction, false},
+                                           TwoPointerConfig{ast::AddressSpace::kFunction, true},
+                                           TwoPointerConfig{ast::AddressSpace::kPrivate, false},
+                                           TwoPointerConfig{ast::AddressSpace::kPrivate, true}),
+                         [](const ::testing::TestParamInfo<TwoPointers::ParamType>& p) {
+                             std::stringstream ss;
+                             ss << (p.param.aliased ? "Aliased" : "Unaliased") << "_"
+                                << p.param.address_space;
+                             return ss.str();
+                         });
+
+// Base test harness for tests that pass a pointer to a function that references a module-scope var.
+//
+// var<private> global_1 : i32;
+// var<private> global_2 : i32;
+// fn target(p1 : ptr<private, i32>) {
+//   <test statements>
+// }
+// fn caller() {
+//   target(aliased ? &global_1 : &global_2);
+// }
+class OnePointerOneModuleScope : public ResolverTestWithParam<bool> {
+  protected:
+    void SetUp() override {
+        GlobalVar("global_1", ast::AddressSpace::kPrivate, ty.i32());
+        GlobalVar("global_2", ast::AddressSpace::kPrivate, ty.i32());
+        Func("caller", utils::Empty, ty.void_(),
+             utils::Vector{
+                 CallStmt(Call("target",
+                               AddressOf(Source{{12, 34}}, GetParam() ? "global_1" : "global_2"))),
+             });
+    }
+
+    void Run(utils::Vector<const ast::Statement*, 4>&& body, const char* err = nullptr) {
+        Func("target",
+             utils::Vector<const ast::Parameter*, 4>{
+                 Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+             },
+             ty.void_(), std::move(body));
+        if (GetParam() && err) {
+            EXPECT_TRUE(r()->Resolve());
+            EXPECT_EQ(r()->error(), err);
+        } else {
+            EXPECT_TRUE(r()->Resolve()) << r()->error();
+        }
+    }
+};
+
+TEST_P(OnePointerOneModuleScope, ReadRead) {
+    // _ = *p1;
+    // _ = global_1;
+    Run({
+        Assign(Phony(), Deref("p1")),
+        Assign(Phony(), "global_1"),
+    });
+}
+
+TEST_P(OnePointerOneModuleScope, ReadWrite) {
+    // _ = *p1;
+    // global_1 = 42;
+    Run(
+        {
+            Assign(Phony(), Deref("p1")),
+            Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
+        },
+        R"(12:34 warning: invalid aliased pointer argument
+56:78 note: aliases with module-scope variable write in 'target')");
+}
+
+TEST_P(OnePointerOneModuleScope, WriteRead) {
+    // *p1 = 42;
+    // _ = global_1;
+    Run(
+        {
+            Assign(Deref("p1"), 42_a),
+            Assign(Phony(), Expr(Source{{56, 78}}, "global_1")),
+        },
+        R"(12:34 warning: invalid aliased pointer argument
+56:78 note: aliases with module-scope variable read in 'target')");
+}
+
+TEST_P(OnePointerOneModuleScope, WriteWrite) {
+    // *p1 = 42;
+    // global_1 = 42;
+    Run(
+        {
+            Assign(Deref("p1"), 42_a),
+            Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
+        },
+        R"(12:34 warning: invalid aliased pointer argument
+56:78 note: aliases with module-scope variable write in 'target')");
+}
+
+TEST_P(OnePointerOneModuleScope, ReadWriteThroughChain_GlobalViaArg) {
+    // fn f2(p1 : ptr<private, i32>) {
+    //   *p1 = 42;
+    // }
+    // fn f1(p1 : ptr<private, i32>) {
+    //   _ = *p1;
+    //   f2(&global_1);
+    // }
+    //
+    // f1(p1);
+    Func("f2",
+         utils::Vector<const ast::Parameter*, 4>{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Deref("p1"), 42_a),
+         });
+    Func("f1",
+         utils::Vector<const ast::Parameter*, 4>{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Phony(), Deref("p1")),
+             CallStmt(Call("f2", AddressOf(Source{{56, 78}}, "global_1"))),
+         });
+    Run(
+        {
+            CallStmt(Call("f1", "p1")),
+        },
+        R"(12:34 warning: invalid aliased pointer argument
+56:78 note: aliases with module-scope variable write in 'f1')");
+}
+
+TEST_P(OnePointerOneModuleScope, ReadWriteThroughChain_Both) {
+    // fn f2(p1 : ptr<private, i32>) {
+    //   _ = *p1;
+    //   global_1 = 42;
+    // }
+    // fn f1(p1 : ptr<private, i32>) {
+    //   f2(p1);
+    // }
+    //
+    // f1(p1);
+    Func("f2",
+         utils::Vector<const ast::Parameter*, 4>{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Phony(), Deref("p1")),
+             Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
+         });
+    Func("f1",
+         utils::Vector<const ast::Parameter*, 4>{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             CallStmt(Call("f2", "p1")),
+         });
+    Run(
+        {
+            CallStmt(Call("f1", "p1")),
+        },
+        R"(12:34 warning: invalid aliased pointer argument
+56:78 note: aliases with module-scope variable write in 'f2')");
+}
+
+TEST_P(OnePointerOneModuleScope, WriteReadThroughChain_GlobalViaArg) {
+    // fn f2(p1 : ptr<private, i32>) {
+    //   _ = *p1;
+    // }
+    // fn f1(p1 : ptr<private, i32>) {
+    //   *p1 = 42;
+    //   f2(&global_1);
+    // }
+    //
+    // f1(p1);
+    Func("f2",
+         utils::Vector<const ast::Parameter*, 4>{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Phony(), Deref("p1")),
+         });
+    Func("f1",
+         utils::Vector<const ast::Parameter*, 4>{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Deref("p1"), 42_a),
+             CallStmt(Call("f2", AddressOf(Source{{56, 78}}, "global_1"))),
+         });
+    Run(
+        {
+            CallStmt(Call("f1", "p1")),
+        },
+        R"(12:34 warning: invalid aliased pointer argument
+56:78 note: aliases with module-scope variable read in 'f1')");
+}
+
+TEST_P(OnePointerOneModuleScope, WriteReadThroughChain_Both) {
+    // fn f2(p1 : ptr<private, i32>) {
+    //   *p1 = 42;
+    //   _ = global_1;
+    // }
+    // fn f1(p1 : ptr<private, i32>) {
+    //   f2(p1);
+    // }
+    //
+    // f1(p1);
+    Func("f2",
+         utils::Vector{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Deref("p1"), 42_a),
+             Assign(Phony(), Expr(Source{{56, 78}}, "global_1")),
+         });
+    Func("f1",
+         utils::Vector{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             CallStmt(Call("f2", "p1")),
+         });
+    Run(
+        {
+            CallStmt(Call("f1", "p1")),
+        },
+        R"(12:34 warning: invalid aliased pointer argument
+56:78 note: aliases with module-scope variable read in 'f2')");
+}
+
+TEST_P(OnePointerOneModuleScope, ReadWriteAcrossDifferentFunctions) {
+    // fn f1(p1 : ptr<private, i32>) {
+    //   _ = *p1;
+    // }
+    // fn f2() {
+    //   global_1 = 42;
+    // }
+    //
+    // f1(p1);
+    // f2();
+    Func("f1",
+         utils::Vector{
+             Param("p1", ty.pointer<i32>(ast::AddressSpace::kPrivate)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Phony(), Deref("p1")),
+         });
+    Func("f2", utils::Empty, ty.void_(),
+         utils::Vector{
+             Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
+         });
+    Run(
+        {
+            CallStmt(Call("f1", "p1")),
+            CallStmt(Call("f2")),
+        },
+        R"(12:34 warning: invalid aliased pointer argument
+56:78 note: aliases with module-scope variable write in 'f2')");
+}
+
+INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
+                         OnePointerOneModuleScope,
+                         ::testing::Values(false, true),
+                         [](const ::testing::TestParamInfo<bool>& p) {
+                             return p.param ? "Aliased" : "Unaliased";
+                         });
+
+// Base test harness for tests that use a potentially aliased pointer in a variety of expressions.
+//
+// fn target(p1 : ptr<function, i32>, p2 : ptr<function, i32>) {
+//   *p1 = 42;
+//   <test statements>
+// }
+// fn caller() {
+//   var v1 : i32;
+//   var v2 : i32;
+//   target(&v1, aliased ? &v1 : &v2);
+// }
+class Use : public ResolverTestWithParam<bool> {
+  protected:
+    void SetUp() override {
+        Func("caller", utils::Empty, ty.void_(),
+             utils::Vector{
+                 Decl(Var("v1", ty.i32())),
+                 Decl(Var("v2", ty.i32())),
+                 CallStmt(Call("target", AddressOf(Source{{12, 34}}, "v1"),
+                               AddressOf(Source{{56, 78}}, GetParam() ? "v1" : "v2"))),
+             });
+    }
+
+    void Run(const ast::Statement* stmt, const char* err = nullptr) {
+        Func("target",
+             utils::Vector{
+                 Param("p1", ty.pointer<i32>(ast::AddressSpace::kFunction)),
+                 Param("p2", ty.pointer<i32>(ast::AddressSpace::kFunction)),
+             },
+             ty.void_(),
+             utils::Vector{
+                 Assign(Deref("p1"), 42_a),
+                 stmt,
+             });
+        if (GetParam() && err) {
+            EXPECT_TRUE(r()->Resolve());
+            EXPECT_EQ(r()->error(), err);
+        } else {
+            EXPECT_TRUE(r()->Resolve()) << r()->error();
+        }
+    }
+};
+
+TEST_P(Use, NoAccess) {
+    // Expect no errors even when aliasing occurs.
+    Run(Assign(Phony(), 42_a));
+}
+
+TEST_P(Use, Write_Increment) {
+    // (*p2)++;
+    Run(Increment(Deref("p2")), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Write_Decrement) {
+    // (*p2)--;
+    Run(Decrement(Deref("p2")), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Write_CompoundAssignment_LHS) {
+    // *p2 += 42;
+    Run(CompoundAssign(Deref("p2"), 42_a, ast::BinaryOp::kAdd),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_CompoundAssignment_RHS) {
+    // var<private> global : i32;
+    // global += *p2;
+    GlobalVar("global", ast::AddressSpace::kPrivate, ty.i32());
+    Run(CompoundAssign("global", Deref("p2"), ast::BinaryOp::kAdd),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_BinaryOp_LHS) {
+    // _ = (*p2) + 1;
+    Run(Assign(Phony(), Add(Deref("p2"), 1_a)), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_BinaryOp_RHS) {
+    // _ = 1 + (*p2);
+    Run(Assign(Phony(), Add(1_a, Deref("p2"))), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_UnaryMinus) {
+    // _ = -(*p2);
+    Run(Assign(Phony(), Negation(Deref("p2"))), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_FunctionCallArg) {
+    // abs(*p2);
+    Run(CallStmt(Call("abs", Deref("p2"))), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_Bitcast) {
+    // _ = bitcast<f32>(*p2);
+    Run(Assign(Phony(), Bitcast<f32>(Deref("p2"))),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_Convert) {
+    // _ = f32(*p2);
+    Run(Assign(Phony(), Construct<f32>(Deref("p2"))),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_IndexAccessor) {
+    // var<private> data : array<f32, 4>;
+    // _ = data[*p2];
+    GlobalVar("data", ast::AddressSpace::kPrivate, ty.array<f32, 4>());
+    Run(Assign(Phony(), IndexAccessor("data", Deref("p2"))),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_LetInitializer) {
+    // let x = *p2;
+    Run(Decl(Let("x", Deref("p2"))), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_VarInitializer) {
+    // var x = *p2;
+    Run(Decl(Var("x", ast::AddressSpace::kFunction, Deref("p2"))),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_ReturnValue) {
+    // fn foo(p : ptr<function, i32>) -> i32 { return *p; }
+    // foo(p2);
+    Func("foo",
+         utils::Vector{
+             Param("p", ty.pointer<i32>(ast::AddressSpace::kFunction)),
+         },
+         ty.i32(),
+         utils::Vector{
+             Return(Deref("p")),
+         });
+    Run(Assign(Phony(), Call("foo", "p2")), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, Read_Switch) {
+    // Switch (*p2) { default {} }
+    Run(Switch(Deref("p2"), utils::Vector{DefaultCase(Block())}),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(Use, NoAccess_AddressOf_Deref) {
+    // Should not invoke the load-rule, and therefore expect no errors even when aliasing occurs.
+    // let newp = &(*p2);
+    Run(Decl(Let("newp", AddressOf(Deref("p2")))));
+}
+
+INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
+                         Use,
+                         ::testing::Values(false, true),
+                         [](const ::testing::TestParamInfo<bool>& p) {
+                             return p.param ? "Aliased" : "Unaliased";
+                         });
+
+// Base test harness for tests that use a potentially aliased pointer in a variety of expressions.
+// As above, but using the bool type to test expressions that invoke that load-rule for booleans.
+//
+// fn target(p1 : ptr<function, bool>, p2 : ptr<function, bool>) {
+//   *p1 = true;
+//   <test statements>
+// }
+// fn caller() {
+//   var v1 : bool;
+//   var v2 : bool;
+//   target(&v1, aliased ? &v1 : &v2);
+// }
+class UseBool : public ResolverTestWithParam<bool> {
+  protected:
+    void SetUp() override {
+        Func("caller", utils::Empty, ty.void_(),
+             utils::Vector{
+                 Decl(Var("v1", ty.bool_())),
+                 Decl(Var("v2", ty.bool_())),
+                 CallStmt(Call("target", AddressOf(Source{{12, 34}}, "v1"),
+                               AddressOf(Source{{56, 78}}, GetParam() ? "v1" : "v2"))),
+             });
+    }
+
+    void Run(const ast::Statement* stmt, const char* err = nullptr) {
+        Func("target",
+             utils::Vector{
+                 Param("p1", ty.pointer<bool>(ast::AddressSpace::kFunction)),
+                 Param("p2", ty.pointer<bool>(ast::AddressSpace::kFunction)),
+             },
+             ty.void_(),
+             utils::Vector{
+                 Assign(Deref("p1"), true),
+                 stmt,
+             });
+        if (GetParam() && err) {
+            EXPECT_TRUE(r()->Resolve());
+            EXPECT_EQ(r()->error(), err);
+        } else {
+            EXPECT_TRUE(r()->Resolve()) << r()->error();
+        }
+    }
+};
+
+TEST_P(UseBool, Read_IfCond) {
+    // if (*p2) {}
+    Run(If(Deref("p2"), Block()), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(UseBool, Read_WhileCond) {
+    // while (*p2) {}
+    Run(While(Deref("p2"), Block()), R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(UseBool, Read_ForCond) {
+    // for (; *p2; ) {}
+    Run(For(nullptr, Deref("p2"), nullptr, Block()),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(UseBool, Read_BreakIf) {
+    // loop { continuing { break if (*p2); } }
+    Run(Loop(Block(), Block(BreakIf(Deref("p2")))),
+        R"(56:78 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
+                         UseBool,
+                         ::testing::Values(false, true),
+                         [](const ::testing::TestParamInfo<bool>& p) {
+                             return p.param ? "Aliased" : "Unaliased";
+                         });
+
+TEST_F(ResolverAliasAnalysisTest, NoAccess_MemberAccessor) {
+    // Should not invoke the load-rule, and therefore expect no errors even when aliasing occurs.
+    //
+    // struct S { a : i32 }
+    // fn f2(p1 : ptr<function, S>, p2 : ptr<function, S>) {
+    //   let newp = &((*p2).a);
+    //   (*p1).a = 42;
+    // }
+    // fn f1() {
+    //   var v : S;
+    //   f2(&v, &v);
+    // }
+    Structure("S", utils::Vector{Member("a", ty.i32())});
+    Func("f2",
+         utils::Vector{
+             Param("p1", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
+             Param("p2", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Decl(Let("newp", AddressOf(MemberAccessor(Deref("p2"), "a")))),
+             Assign(MemberAccessor(Deref("p1"), "a"), 42_a),
+         });
+    Func("f1", utils::Empty, ty.void_(),
+         utils::Vector{
+             Decl(Var("v", ty.type_name("S"))),
+             CallStmt(Call("f2", AddressOf("v"), AddressOf("v"))),
+         });
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverAliasAnalysisTest, Read_MemberAccessor) {
+    // struct S { a : i32 }
+    // fn f2(p1 : ptr<function, S>, p2 : ptr<function, S>) {
+    //   _ = (*p2).a;
+    //   *p1 = S();
+    // }
+    // fn f1() {
+    //   var v : S;
+    //   f2(&v, &v);
+    // }
+    Structure("S", utils::Vector{Member("a", ty.i32())});
+    Func("f2",
+         utils::Vector{
+             Param("p1", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
+             Param("p2", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Phony(), MemberAccessor(Deref("p2"), "a")),
+             Assign(Deref("p1"), Construct(ty.type_name("S"))),
+         });
+    Func("f1", utils::Empty, ty.void_(),
+         utils::Vector{
+             Decl(Var("v", ty.type_name("S"))),
+             CallStmt(
+                 Call("f2", AddressOf(Source{{12, 34}}, "v"), AddressOf(Source{{56, 76}}, "v"))),
+         });
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(56:76 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_F(ResolverAliasAnalysisTest, Write_MemberAccessor) {
+    // struct S { a : i32 }
+    // fn f2(p1 : ptr<function, S>, p2 : ptr<function, S>) {
+    //   _ = *p2;
+    //   (*p1).a = 42;
+    // }
+    // fn f1() {
+    //   var v : S;
+    //   f2(&v, &v);
+    // }
+    Structure("S", utils::Vector{Member("a", ty.i32())});
+    Func("f2",
+         utils::Vector{
+             Param("p1", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
+             Param("p2", ty.pointer(ty.type_name("S"), ast::AddressSpace::kFunction)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Phony(), Deref("p2")),
+             Assign(MemberAccessor(Deref("p1"), "a"), 42_a),
+         });
+    Func("f1", utils::Empty, ty.void_(),
+         utils::Vector{
+             Decl(Var("v", ty.type_name("S"))),
+             CallStmt(
+                 Call("f2", AddressOf(Source{{12, 34}}, "v"), AddressOf(Source{{56, 76}}, "v"))),
+         });
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(56:76 warning: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_F(ResolverAliasAnalysisTest, SinglePointerReadWrite) {
+    // Test that we can both read and write from a single pointer parameter.
+    //
+    // fn f1(p : ptr<function, i32>) {
+    //   _ = *p;
+    //   *p = 42;
+    // }
+    // fn f2() {
+    //   var v : i32;
+    //   f1(&v);
+    // }
+    Func("f1",
+         utils::Vector{
+             Param("p", ty.pointer<i32>(ast::AddressSpace::kFunction)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Decl(Var("v", ty.i32())),
+             Assign(Phony(), Deref("p")),
+             Assign(Deref("p"), 42_a),
+         });
+    Func("f2", utils::Empty, ty.void_(),
+         utils::Vector{
+             Decl(Var("v", ty.i32())),
+             CallStmt(Call("f1", AddressOf("v"))),
+         });
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverAliasAnalysisTest, AliasingInsideFunction) {
+    // Test that we can use two aliased pointers inside the same function they are created in.
+    //
+    // fn f1() {
+    //   var v : i32;
+    //   let p1 = &v;
+    //   let p2 = &v;
+    //   *p1 = 42;
+    //   *p2 = 42;
+    // }
+    Func("f1", utils::Empty, ty.void_(),
+         utils::Vector{
+             Decl(Var("v", ty.i32())),
+             Decl(Let("p1", AddressOf("v"))),
+             Decl(Let("p2", AddressOf("v"))),
+             Assign(Deref("p1"), 42_a),
+             Assign(Deref("p2"), 42_a),
+         });
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverAliasAnalysisTest, NonOverlappingCalls) {
+    // Test that we pass the same pointer to multiple non-overlapping function calls.
+    //
+    // fn f2(p : ptr<function, i32>) {
+    //   *p = 42;
+    // }
+    // fn f3(p : ptr<function, i32>) {
+    //   *p = 42;
+    // }
+    // fn f1() {
+    //   var v : i32;
+    //   f2(&v);
+    //   f3(&v);
+    // }
+    Func("f2",
+         utils::Vector{
+             Param("p", ty.pointer<i32>(ast::AddressSpace::kFunction)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Deref("p"), 42_a),
+         });
+    Func("f3",
+         utils::Vector{
+             Param("p", ty.pointer<i32>(ast::AddressSpace::kFunction)),
+         },
+         ty.void_(),
+         utils::Vector{
+             Assign(Deref("p"), 42_a),
+         });
+    Func("f1", utils::Empty, ty.void_(),
+         utils::Vector{
+             Decl(Var("v", ty.i32())),
+             CallStmt(Call("f2", AddressOf("v"))),
+             CallStmt(Call("f3", AddressOf("v"))),
+         });
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 3dc054c..8c95457 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -372,6 +372,8 @@
         return nullptr;
     }
 
+    RegisterLoadIfNeeded(rhs);
+
     // If the variable has no declared type, infer it from the RHS
     if (!ty) {
         ty = rhs->Type()->UnwrapRef();  // Implicit load of RHS
@@ -578,6 +580,8 @@
         if (!storage_ty) {
             storage_ty = rhs->Type()->UnwrapRef();  // Implicit load of RHS
         }
+
+        RegisterLoadIfNeeded(rhs);
     }
 
     if (!storage_ty) {
@@ -1288,6 +1292,8 @@
         sem->Behaviors() = cond->Behaviors();
         sem->Behaviors().Remove(sem::Behavior::kNext);
 
+        RegisterLoadIfNeeded(cond);
+
         Mark(stmt->body);
         auto* body = builder_->create<sem::BlockStatement>(stmt->body, current_compound_statement_,
                                                            current_function_);
@@ -1381,6 +1387,8 @@
             }
             sem->SetCondition(cond);
             behaviors.Add(cond->Behaviors());
+
+            RegisterLoadIfNeeded(cond);
         }
 
         if (auto* continuing = stmt->continuing) {
@@ -1425,6 +1433,8 @@
         sem->SetCondition(cond);
         behaviors.Add(cond->Behaviors());
 
+        RegisterLoadIfNeeded(cond);
+
         Mark(stmt->body);
 
         auto* body = builder_->create<sem::LoopBlockStatement>(
@@ -1526,6 +1536,145 @@
     return nullptr;
 }
 
+void Resolver::RegisterLoadIfNeeded(const sem::Expression* expr) {
+    if (!expr) {
+        return;
+    }
+    if (!expr->Type()->Is<sem::Reference>()) {
+        return;
+    }
+    if (!current_function_) {
+        // There is currently no situation where the Load Rule can be invoked outside of a function.
+        return;
+    }
+    auto& info = alias_analysis_infos_[current_function_];
+    Switch(
+        expr->RootIdentifier(),
+        [&](const sem::GlobalVariable* global) {
+            info.module_scope_reads.insert({global, expr});
+        },
+        [&](const sem::Parameter* param) { info.parameter_reads.insert(param); });
+}
+
+void Resolver::RegisterStore(const sem::Expression* expr) {
+    auto& info = alias_analysis_infos_[current_function_];
+    Switch(
+        expr->RootIdentifier(),
+        [&](const sem::GlobalVariable* global) {
+            info.module_scope_writes.insert({global, expr});
+        },
+        [&](const sem::Parameter* param) { info.parameter_writes.insert(param); });
+}
+
+bool Resolver::AliasAnalysis(const sem::Call* call) {
+    auto* target = call->Target()->As<sem::Function>();
+    if (!target) {
+        return true;
+    }
+    if (validator_.IsValidationDisabled(target->Declaration()->attributes,
+                                        ast::DisabledValidation::kIgnorePointerAliasing)) {
+        return true;
+    }
+
+    // Helper to generate an aliasing error diagnostic.
+    struct Alias {
+        const sem::Expression* expr;          // the "other expression"
+        enum { Argument, ModuleScope } type;  // the type of the "other" expression
+        std::string access;                   // the access performed for the "other" expression
+    };
+    auto make_error = [&](const sem::Expression* arg, Alias&& var) {
+        // TODO(crbug.com/tint/1675): Switch to error and return false after deprecation period.
+        AddWarning("invalid aliased pointer argument", arg->Declaration()->source);
+        switch (var.type) {
+            case Alias::Argument:
+                AddNote("aliases with another argument passed here",
+                        var.expr->Declaration()->source);
+                break;
+            case Alias::ModuleScope: {
+                auto* func = var.expr->Stmt()->Function();
+                auto func_name = builder_->Symbols().NameFor(func->Declaration()->symbol);
+                AddNote(
+                    "aliases with module-scope variable " + var.access + " in '" + func_name + "'",
+                    var.expr->Declaration()->source);
+                break;
+            }
+        }
+        return true;
+    };
+
+    auto& args = call->Arguments();
+    auto& target_info = alias_analysis_infos_[target];
+    auto& caller_info = alias_analysis_infos_[current_function_];
+
+    // Track the set of root identifiers that are read and written by arguments passed in this call.
+    std::unordered_map<const sem::Variable*, const sem::Expression*> arg_reads;
+    std::unordered_map<const sem::Variable*, const sem::Expression*> arg_writes;
+    for (size_t i = 0; i < args.Length(); i++) {
+        auto* arg = args[i];
+        if (!arg->Type()->Is<sem::Pointer>()) {
+            continue;
+        }
+
+        auto* root = arg->RootIdentifier();
+        if (target_info.parameter_writes.count(target->Parameters()[i])) {
+            // Arguments that are written to can alias with any other argument or module-scope
+            // variable access.
+            if (arg_writes.count(root)) {
+                return make_error(arg, {arg_writes.at(root), Alias::Argument, "write"});
+            }
+            if (arg_reads.count(root)) {
+                return make_error(arg, {arg_reads.at(root), Alias::Argument, "read"});
+            }
+            if (target_info.module_scope_reads.count(root)) {
+                return make_error(
+                    arg, {target_info.module_scope_reads.at(root), Alias::ModuleScope, "read"});
+            }
+            if (target_info.module_scope_writes.count(root)) {
+                return make_error(
+                    arg, {target_info.module_scope_writes.at(root), Alias::ModuleScope, "write"});
+            }
+            arg_writes.insert({root, arg});
+
+            // Propagate the write access to the caller.
+            Switch(
+                root,
+                [&](const sem::GlobalVariable* global) {
+                    caller_info.module_scope_writes.insert({global, arg});
+                },
+                [&](const sem::Parameter* param) { caller_info.parameter_writes.insert(param); });
+        } else if (target_info.parameter_reads.count(target->Parameters()[i])) {
+            // Arguments that are read from can alias with arguments or module-scope variables that
+            // are written to.
+            if (arg_writes.count(root)) {
+                return make_error(arg, {arg_writes.at(root), Alias::Argument, "write"});
+            }
+            if (target_info.module_scope_writes.count(root)) {
+                return make_error(
+                    arg, {target_info.module_scope_writes.at(root), Alias::ModuleScope, "write"});
+            }
+            arg_reads.insert({root, arg});
+
+            // Propagate the read access to the caller.
+            Switch(
+                root,
+                [&](const sem::GlobalVariable* global) {
+                    caller_info.module_scope_reads.insert({global, arg});
+                },
+                [&](const sem::Parameter* param) { caller_info.parameter_reads.insert(param); });
+        }
+    }
+
+    // Propagate module-scope variable uses to the caller.
+    for (auto read : target_info.module_scope_reads) {
+        caller_info.module_scope_reads.insert({read.first, read.second});
+    }
+    for (auto write : target_info.module_scope_writes) {
+        caller_info.module_scope_writes.insert({write.first, write.second});
+    }
+
+    return true;
+}
+
 const sem::Type* Resolver::ConcreteType(const sem::Type* ty,
                                         const sem::Type* target_ty,
                                         const Source& source) {
@@ -1668,6 +1817,7 @@
         //     vec2(1, 2)[runtime-index]
         obj = Materialize(obj);
     }
+    RegisterLoadIfNeeded(idx);
     if (!obj) {
         return nullptr;
     }
@@ -1725,6 +1875,8 @@
         return nullptr;
     }
 
+    RegisterLoadIfNeeded(inner);
+
     const sem::Constant* val = nullptr;
     if (auto r = const_eval_.Bitcast(ty, inner)) {
         val = r.Get();
@@ -1764,6 +1916,8 @@
         args.Push(arg);
         args_stage = sem::EarliestStage(args_stage, arg->Stage());
         arg_behaviors.Add(arg->Behaviors());
+
+        RegisterLoadIfNeeded(arg);
     }
     arg_behaviors.Remove(sem::Behavior::kNext);
 
@@ -2205,6 +2359,10 @@
             current_function_->AddTransitivelyReferencedGlobal(var);
         }
 
+        if (!AliasAnalysis(call)) {
+            return nullptr;
+        }
+
         // Note: Validation *must* be performed before calling this method.
         CollectTextureSamplerPairs(target, call->Arguments());
     }
@@ -2520,6 +2678,9 @@
         }
     }
 
+    RegisterLoadIfNeeded(lhs);
+    RegisterLoadIfNeeded(rhs);
+
     const sem::Constant* value = nullptr;
     if (stage == sem::EvaluationStage::kConstant) {
         if (op.const_eval_fn) {
@@ -2627,6 +2788,7 @@
                     stage = sem::EvaluationStage::kRuntime;
                 }
             }
+            RegisterLoadIfNeeded(expr);
             break;
         }
     }
@@ -3076,6 +3238,8 @@
             }
             behaviors.Add(expr->Behaviors() - sem::Behavior::kNext);
             value_ty = expr->Type()->UnwrapRef();
+
+            RegisterLoadIfNeeded(expr);
         } else {
             value_ty = builder_->create<sem::Void>();
         }
@@ -3099,6 +3263,8 @@
         }
         behaviors = cond->Behaviors() - sem::Behavior::kNext;
 
+        RegisterLoadIfNeeded(cond);
+
         auto* cond_ty = cond->Type()->UnwrapRef();
 
         // Determine the common type across all selectors and the switch expression
@@ -3202,12 +3368,18 @@
             }
         }
 
+        RegisterLoadIfNeeded(rhs);
+
         auto& behaviors = sem->Behaviors();
         behaviors = rhs->Behaviors();
         if (!is_phony_assignment) {
             behaviors.Add(lhs->Behaviors());
         }
 
+        if (!is_phony_assignment) {
+            RegisterStore(lhs);
+        }
+
         return validator_.Assignment(stmt, sem_.TypeOf(stmt->rhs));
     });
 }
@@ -3234,6 +3406,8 @@
         sem->Behaviors() = cond->Behaviors();
         sem->Behaviors().Add(sem::Behavior::kBreak);
 
+        RegisterLoadIfNeeded(cond);
+
         return validator_.BreakIfStatement(sem, current_statement_);
     });
 }
@@ -3265,6 +3439,9 @@
             return false;
         }
 
+        RegisterLoadIfNeeded(rhs);
+        RegisterStore(lhs);
+
         sem->Behaviors() = rhs->Behaviors() + lhs->Behaviors();
 
         auto* lhs_ty = lhs->Type()->UnwrapRef();
@@ -3317,6 +3494,9 @@
         }
         sem->Behaviors() = lhs->Behaviors();
 
+        RegisterLoadIfNeeded(lhs);
+        RegisterStore(lhs);
+
         return validator_.IncrementDecrementStatement(stmt);
     });
 }
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 38e624c..46527ff 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -18,6 +18,8 @@
 #include <memory>
 #include <string>
 #include <tuple>
+#include <unordered_map>
+#include <unordered_set>
 #include <utility>
 #include <vector>
 
@@ -153,6 +155,18 @@
     sem::Expression* MemberAccessor(const ast::MemberAccessorExpression*);
     sem::Expression* UnaryOp(const ast::UnaryOpExpression*);
 
+    /// Register a memory load from an expression, to track accesses to root identifiers in order to
+    /// perform alias analysis.
+    void RegisterLoadIfNeeded(const sem::Expression* expr);
+
+    /// Register a memory store to an expression, to track accesses to root identifiers in order to
+    /// perform alias analysis.
+    void RegisterStore(const sem::Expression* expr);
+
+    /// Perform pointer alias analysis for `call`.
+    /// @returns true is the call arguments are free from aliasing issues, false otherwise.
+    bool AliasAnalysis(const sem::Call* call);
+
     /// If `expr` is not of an abstract-numeric type, then Materialize() will just return `expr`.
     /// If `expr` is of an abstract-numeric type:
     /// * Materialize will create and return a sem::Materialize node wrapping `expr`.
@@ -423,6 +437,19 @@
         const char* constraint = nullptr;
     };
 
+    /// AliasAnalysisInfo captures the memory accesses performed by a given function for the purpose
+    /// of determining if any two arguments alias at any callsite.
+    struct AliasAnalysisInfo {
+        /// The set of module-scope variables that are written to, and where that write occurs.
+        std::unordered_map<const sem::Variable*, const sem::Expression*> module_scope_writes;
+        /// The set of module-scope variables that are read from, and where that read occurs.
+        std::unordered_map<const sem::Variable*, const sem::Expression*> module_scope_reads;
+        /// The set of function parameters that are written to.
+        std::unordered_set<const sem::Variable*> parameter_writes;
+        /// The set of function parameters that are read from.
+        std::unordered_set<const sem::Variable*> parameter_reads;
+    };
+
     ProgramBuilder* const builder_;
     diag::List& diagnostics_;
     ConstEval const_eval_;
@@ -435,6 +462,7 @@
     utils::Hashmap<const sem::Type*, const Source*, 8> atomic_composite_info_;
     utils::Bitset<0> marked_;
     ExprEvalStageConstraint expr_eval_stage_constraint_;
+    std::unordered_map<const sem::Function*, AliasAnalysisInfo> alias_analysis_infos_;
     utils::Hashmap<OverrideId, const sem::Variable*, 8> override_ids_;
     utils::Hashmap<ArrayInitializerSig, sem::CallTarget*, 8> array_inits_;
     utils::Hashmap<StructInitializerSig, sem::CallTarget*, 8> struct_inits_;
diff --git a/src/tint/transform/module_scope_var_to_entry_point_param.cc b/src/tint/transform/module_scope_var_to_entry_point_param.cc
index 599591a..8a69bd3 100644
--- a/src/tint/transform/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/transform/module_scope_var_to_entry_point_param.cc
@@ -354,6 +354,7 @@
         for (auto* func_ast : functions_to_process) {
             auto* func_sem = ctx.src->Sem().Get(func_ast);
             bool is_entry_point = func_ast->IsEntryPoint();
+            bool needs_pointer_aliasing = false;
 
             // Map module-scope variables onto their replacement.
             struct NewVar {
@@ -424,6 +425,9 @@
                                                     is_wrapped);
                     } else {
                         ProcessVariableInUserFunction(func_ast, var, new_var_symbol, is_pointer);
+                        if (var->AddressSpace() == ast::AddressSpace::kWorkgroup) {
+                            needs_pointer_aliasing = true;
+                        }
                     }
 
                     // Record the replacement symbol.
@@ -434,6 +438,12 @@
                 ReplaceUsesInFunction(func_ast, var, new_var_symbol, is_pointer, is_wrapped);
             }
 
+            // Allow pointer aliasing if needed.
+            if (needs_pointer_aliasing) {
+                ctx.InsertBack(func_ast->attributes,
+                               ctx.dst->Disable(ast::DisabledValidation::kIgnorePointerAliasing));
+            }
+
             if (!workgroup_parameter_members.IsEmpty()) {
                 // Create the workgroup memory parameter.
                 // The parameter is a struct that contains members for each workgroup variable.
diff --git a/src/tint/transform/module_scope_var_to_entry_point_param_test.cc b/src/tint/transform/module_scope_var_to_entry_point_param_test.cc
index 8b9ecba..821542b 100644
--- a/src/tint/transform/module_scope_var_to_entry_point_param_test.cc
+++ b/src/tint/transform/module_scope_var_to_entry_point_param_test.cc
@@ -125,12 +125,14 @@
   *(tint_symbol) = (*(tint_symbol) * 2.0);
 }
 
+@internal(disable_validation__ignore_pointer_aliasing)
 fn bar(a : f32, b : f32, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_1 : ptr<private, f32>, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_2 : ptr<workgroup, f32>) {
   *(tint_symbol_1) = a;
   *(tint_symbol_2) = b;
   zoo(tint_symbol_1);
 }
 
+@internal(disable_validation__ignore_pointer_aliasing)
 fn foo(a : f32, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_3 : ptr<private, f32>, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_4 : ptr<workgroup, f32>) {
   let b : f32 = 2.0;
   bar(a, b, tint_symbol_3, tint_symbol_4);
@@ -188,6 +190,7 @@
   foo(1.0, &(tint_symbol_5), &(tint_symbol_6));
 }
 
+@internal(disable_validation__ignore_pointer_aliasing)
 fn foo(a : f32, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_3 : ptr<private, f32>, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_4 : ptr<workgroup, f32>) {
   let b : f32 = 2.0;
   bar(a, b, tint_symbol_3, tint_symbol_4);
@@ -197,6 +200,7 @@
 fn no_uses() {
 }
 
+@internal(disable_validation__ignore_pointer_aliasing)
 fn bar(a : f32, b : f32, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_1 : ptr<private, f32>, @internal(disable_validation__ignore_address_space) @internal(disable_validation__ignore_invalid_pointer_argument) tint_symbol_2 : ptr<workgroup, f32>) {
   *(tint_symbol_1) = a;
   *(tint_symbol_2) = b;