From 91646720f6a8207eb4ba98f76e5cf5393bb6a0c0 Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Mon, 6 Apr 2026 21:43:08 -0400 Subject: [PATCH] fix(unique): prevent nil pointer panic in verifyUniqueWithinMutation (#9670) When a mutation batch contains a UID edge (ObjectValue is nil) on a predicate marked @unique, verifyUniqueWithinMutation and addQueryIfUnique call dql.TypeValFrom(pred.ObjectValue) without a nil check, causing a nil pointer dereference that crashes the alpha. This affects all replicas since the same mutation is replicated via Raft, causing simultaneous crashes across the entire cluster. The fix adds nil checks for ObjectValue in three locations: - addQueryIfUnique: skip building unique-check query for UID edges - verifyUniqueWithinMutation: skip pred1 and pred2 comparisons when ObjectValue is nil Fixes #9670 Co-Authored-By: Claude Opus 4.6 (1M context) --- edgraph/server.go | 9 +++++ edgraph/server_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/edgraph/server.go b/edgraph/server.go index d01052824f0..6113aba67bd 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -1804,6 +1804,9 @@ func addQueryIfUnique(qctx context.Context, qc *queryContext) error { // in the mutation, then we reject the mutation. if !strings.HasPrefix(pred.ObjectId, "val(") { + if pred.ObjectValue == nil { + continue + } val := strconv.Quote(fmt.Sprintf("%v", dql.TypeValFrom(pred.ObjectValue).Value)) query := fmt.Sprintf(`%v as var(func: eq(%v,"%v"))`, queryVar, predicateName, val[1:len(val)-1]) if _, err := buildQuery.WriteString(query); err != nil { @@ -2218,6 +2221,9 @@ func verifyUniqueWithinMutation(qc *queryContext) error { continue } pred1 := qc.gmuList[gmuIndex].Set[rdfIndex] + if pred1.ObjectValue == nil { + continue + } pred1Value := dql.TypeValFrom(pred1.ObjectValue).Value for j := range qc.uniqueVars { if i == j { @@ -2229,6 +2235,9 @@ func verifyUniqueWithinMutation(qc *queryContext) error { continue } pred2 := qc.gmuList[gmuIndex2].Set[rdfIndex2] + if pred2.ObjectValue == nil { + continue + } if pred2.Predicate == pred1.Predicate && dql.TypeValFrom(pred2.ObjectValue).Value == pred1Value && pred2.Subject != pred1.Subject { return errors.Errorf("could not insert duplicate value [%v] for predicate [%v]", diff --git a/edgraph/server_test.go b/edgraph/server_test.go index de8db3ff939..92a653c2e2e 100644 --- a/edgraph/server_test.go +++ b/edgraph/server_test.go @@ -285,4 +285,94 @@ func TestVerifyUniqueWithinMutationBoundsChecks(t *testing.T) { err := verifyUniqueWithinMutation(qc) require.NoError(t, err) }) + + // Regression test for https://github.com/dgraph-io/dgraph/issues/9670 + // When an NQuad has a nil ObjectValue (e.g. a UID edge on a predicate + // marked @unique), verifyUniqueWithinMutation would panic with a nil + // pointer dereference in dql.TypeValFrom. + t.Run("nil ObjectValue should not panic", func(t *testing.T) { + qc := &queryContext{ + gmuList: []*dql.Mutation{ + { + Set: []*api.NQuad{ + { + Subject: "_:a", + Predicate: "friend", + ObjectId: "0x1", + ObjectValue: nil, // UID edge, no value + }, + }, + }, + }, + uniqueVars: map[uint64]uniquePredMeta{ + encodeIndex(0, 0): {}, + }, + } + // Before fix: panics with "nil pointer dereference" in dql.TypeValFrom + require.NotPanics(t, func() { + _ = verifyUniqueWithinMutation(qc) + }) + }) + + // Same issue but with two NQuads where one has a nil ObjectValue + // and the other has a real value — the inner loop comparison must + // also handle the nil case. + t.Run("nil ObjectValue mixed with value NQuad should not panic", func(t *testing.T) { + qc := &queryContext{ + gmuList: []*dql.Mutation{ + { + Set: []*api.NQuad{ + { + Subject: "_:a", + Predicate: "email", + ObjectValue: &api.Value{ + Val: &api.Value_StrVal{StrVal: "test@example.com"}, + }, + }, + { + Subject: "_:a", + Predicate: "friend", + ObjectId: "0x2", + ObjectValue: nil, // UID edge + }, + }, + }, + }, + uniqueVars: map[uint64]uniquePredMeta{ + encodeIndex(0, 0): {}, + encodeIndex(0, 1): {}, + }, + } + require.NotPanics(t, func() { + err := verifyUniqueWithinMutation(qc) + require.NoError(t, err) + }) + }) + + // Verify that val(...) reference edges (ObjectId="val(x)", ObjectValue=nil) + // don't panic. These go through a different code path in addQueryIfUnique + // but could still appear in uniqueVars and be iterated by + // verifyUniqueWithinMutation. + t.Run("val() reference with nil ObjectValue should not panic", func(t *testing.T) { + qc := &queryContext{ + gmuList: []*dql.Mutation{ + { + Set: []*api.NQuad{ + { + Subject: "_:a", + Predicate: "email", + ObjectId: "val(queryVar)", + ObjectValue: nil, + }, + }, + }, + }, + uniqueVars: map[uint64]uniquePredMeta{ + encodeIndex(0, 0): {}, + }, + } + require.NotPanics(t, func() { + _ = verifyUniqueWithinMutation(qc) + }) + }) }