Bug report criteria
What happened?
client/v3/naming/endpoints.Manager.DeleteEndpoint is documented as deleting a
single endpoint. It accepts clientv3.OpOption values and forwards them
directly to clientv3.OpDelete. Passing a range option such as WithFromKey
therefore turns the operation into a multi-key delete.
The manager validates only the starting key against target + "/"; it does not
validate the resulting delete range before adding it to the transaction. In the
reproduction, one call deletes the requested endpoint, another endpoint under
the same target, and an endpoint owned by a different target manager.
What did you expect to happen?
DeleteEndpoint should affect only the named endpoint. Range options should be
rejected before the transaction is committed, or otherwise constrained to
preserve the method's single-endpoint contract and target boundary.
How can we reproduce it (as minimally and precisely as possible)?
Add this integration test to
tests/integration/clientv3/naming/endpoints_test.go and run:
cd tests
go test ./integration/clientv3/naming -run '^TestEndpointManagerDeleteEndpointRejectsRangeOptions$' -count=1
func TestEndpointManagerDeleteEndpointRejectsRangeOptions(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
defer clus.Terminate(t)
c := clus.RandClient()
em, err := endpoints.NewManager(c, "foo")
require.NoError(t, err)
emOther, err := endpoints.NewManager(c, "foo_other")
require.NoError(t, err)
err = em.AddEndpoint(context.TODO(), "foo/a", endpoints.Endpoint{Addr: "127.0.0.1:2000"})
require.NoError(t, err)
err = em.AddEndpoint(context.TODO(), "foo/b", endpoints.Endpoint{Addr: "127.0.0.1:2001"})
require.NoError(t, err)
err = emOther.AddEndpoint(context.TODO(), "foo_other/a", endpoints.Endpoint{Addr: "127.0.0.1:2002"})
require.NoError(t, err)
err = em.DeleteEndpoint(context.TODO(), "foo/a", etcd.WithFromKey())
if err == nil {
t.Errorf("expected range delete option to be rejected")
}
for _, key := range []string{"foo/a", "foo/b", "foo_other/a"} {
resp, err := c.Get(context.TODO(), key)
require.NoError(t, err)
if len(resp.Kvs) != 1 {
t.Errorf("expected %q to remain after rejected delete, got %d keys", key, len(resp.Kvs))
}
}
}
Anything else we need to know?
The public interface documents DeleteEndpoint as deleting a single
endpoint,
but the implementation forwards all delete options into
OpDelete.
The current delete code path is:
case Delete:
ops = append(ops, clientv3.OpDelete(update.Key, update.Opts...))
One possible fix is to build the delete operation and reject it if
op.RangeBytes() is nonempty before committing any update in the batch.
Etcd version (please run commands below)
Confirmed on:
$ git rev-parse HEAD
12caed621b38312cc1084113415bf135c59e6457
$ go version
go version go1.26.5 linux/amd64
embedded integration server version: 3.8.0-alpha.0
also reproduced at v3.6.13 (b0f9ef190952e6e66a778513097a02ee41220727)
Etcd configuration (command line flags or environment variables)
Single-node embedded integration cluster from the etcd test framework.
Etcd debug information
No external cluster or separately installed etcdctl was used. The
reproduction builds and starts an embedded server from the source checkout.
Relevant log output
--- FAIL: TestEndpointManagerDeleteEndpointRejectsRangeOptions
endpoints_test.go: expected range delete option to be rejected
expected "foo/a" to remain after rejected delete, got 0 keys
expected "foo/b" to remain after rejected delete, got 0 keys
expected "foo_other/a" to remain after rejected delete, got 0 keys
FAIL go.etcd.io/etcd/tests/v3/integration/clientv3/naming
Bug report criteria
What happened?
client/v3/naming/endpoints.Manager.DeleteEndpointis documented as deleting asingle endpoint. It accepts
clientv3.OpOptionvalues and forwards themdirectly to
clientv3.OpDelete. Passing a range option such asWithFromKeytherefore turns the operation into a multi-key delete.
The manager validates only the starting key against
target + "/"; it does notvalidate the resulting delete range before adding it to the transaction. In the
reproduction, one call deletes the requested endpoint, another endpoint under
the same target, and an endpoint owned by a different target manager.
What did you expect to happen?
DeleteEndpointshould affect only the named endpoint. Range options should berejected before the transaction is committed, or otherwise constrained to
preserve the method's single-endpoint contract and target boundary.
How can we reproduce it (as minimally and precisely as possible)?
Add this integration test to
tests/integration/clientv3/naming/endpoints_test.goand run:Anything else we need to know?
The public interface documents
DeleteEndpointas deleting a singleendpoint,
but the implementation forwards all delete options into
OpDelete.The current delete code path is:
One possible fix is to build the delete operation and reject it if
op.RangeBytes()is nonempty before committing any update in the batch.Etcd version (please run commands below)
Confirmed on:
Etcd configuration (command line flags or environment variables)
Single-node embedded integration cluster from the etcd test framework.
Etcd debug information
No external cluster or separately installed
etcdctlwas used. Thereproduction builds and starts an embedded server from the source checkout.
Relevant log output