From 99f3aac02a570b11bac83fcdf9b92501b25dce5c Mon Sep 17 00:00:00 2001
From: bwzhang <zhangbowei@kylinos.cn>
Date: Tue, 23 Apr 2024 16:15:29 +0800
Subject: [PATCH] fix CVE-2023-32082

---
 etcdserver/v3_server.go        | 28 ++++++++++-
 integration/v3_auth_test.go    | 91 ++++++++++++++++++++++++++++++++--
 pkg/ioutil/pagewriter_test.go  |  2 +-
 tests/e2e/ctl_v3_auth_test.go  | 49 ++++++++++++++++++
 tests/e2e/ctl_v3_lease_test.go |  8 +++
 5 files changed, 171 insertions(+), 7 deletions(-)

diff --git a/etcdserver/v3_server.go b/etcdserver/v3_server.go
index 1fa8e4e..dee5c20 100644
--- a/etcdserver/v3_server.go
+++ b/etcdserver/v3_server.go
@@ -298,7 +298,33 @@ func (s *EtcdServer) LeaseRenew(ctx context.Context, id lease.LeaseID) (int64, e
 	return -1, ErrCanceled
 }
 
-func (s *EtcdServer) LeaseTimeToLive(ctx context.Context, r *pb.LeaseTimeToLiveRequest) (*pb.LeaseTimeToLiveResponse, error) {
+func (s *EtcdServer) checkLeaseTimeToLive(ctx context.Context, leaseID lease.LeaseID) (error, uint64) {
+	rev := s.AuthStore().Revision()
+	if !s.AuthStore().IsAuthEnabled() {
+		return nil, rev
+	}
+	authInfo, err := s.AuthInfoFromCtx(ctx)
+	if err != nil {
+		return err, rev
+	}
+	if authInfo == nil {
+		return auth.ErrUserEmpty, rev
+	}
+
+	l := s.lessor.Lookup(leaseID)
+	if l != nil {
+		for _, key := range l.Keys() {
+			if err := s.AuthStore().IsRangePermitted(authInfo, []byte(key), []byte{}); err != nil {
+				return err, 0
+			}
+		}
+	}
+
+
+	return nil, rev
+}
+
+func (s *EtcdServer) leaseTimeToLive(ctx context.Context, r *pb.LeaseTimeToLiveRequest) (*pb.LeaseTimeToLiveResponse, error) {
 	if s.Leader() == s.ID() {
 		// primary; timetolive directly from leader
 		le := s.lessor.Lookup(lease.LeaseID(r.ID))
diff --git a/integration/v3_auth_test.go b/integration/v3_auth_test.go
index ee386ff..b473053 100644
--- a/integration/v3_auth_test.go
+++ b/integration/v3_auth_test.go
@@ -150,12 +150,10 @@ func testV3AuthWithLeaseRevokeWithRoot(t *testing.T, ccfg ClusterConfig) {
 	// wait for lease expire
 	time.Sleep(3 * time.Second)
 
-	tresp, terr := api.Lease.LeaseTimeToLive(
+	tresp, terr := rootc.TimeToLive(
 		context.TODO(),
-		&pb.LeaseTimeToLiveRequest{
-			ID:   int64(leaseID),
-			Keys: true,
-		},
+		leaseID,
+		clientv3.WithAttachedKeys(),
 	)
 	if terr != nil {
 		t.Error(terr)
@@ -394,3 +392,86 @@ func TestV3AuthOldRevConcurrent(t *testing.T) {
 	}
 	wg.Wait()
 }
+
+func TestV3AuthWithLeaseTimeToLive(t *testing.T) {
+	integration.BeforeTest(t)
+	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+	defer clus.Terminate(t)
+
+	users := []user{
+		{
+			name:     "user1",
+			password: "user1-123",
+			role:     "role1",
+			key:      "k1",
+			end:      "k3",
+		},
+		{
+			name:     "user2",
+			password: "user2-123",
+			role:     "role2",
+			key:      "k2",
+			end:      "k4",
+		},
+	}
+	authSetupUsers(t, integration.ToGRPC(clus.Client(0)).Auth, users)
+
+	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
+
+	user1c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user1", Password: "user1-123"})
+	if cerr != nil {
+		t.Fatal(cerr)
+	}
+	defer user1c.Close()
+
+	user2c, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user2", Password: "user2-123"})
+	if cerr != nil {
+		t.Fatal(cerr)
+	}
+	defer user2c.Close()
+
+	leaseResp, err := user1c.Grant(context.TODO(), 90)
+	if err != nil {
+		t.Fatal(err)
+	}
+	leaseID := leaseResp.ID
+	_, err = user1c.Put(context.TODO(), "k1", "val", clientv3.WithLease(leaseID))
+	if err != nil {
+		t.Fatal(err)
+	}
+	// k2 can be accessed from both user1 and user2
+	_, err = user1c.Put(context.TODO(), "k2", "val", clientv3.WithLease(leaseID))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = user1c.TimeToLive(context.TODO(), leaseID)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = user2c.TimeToLive(context.TODO(), leaseID)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = user2c.TimeToLive(context.TODO(), leaseID, clientv3.WithAttachedKeys())
+	if err == nil {
+		t.Fatal("timetolive from user2 should be failed with permission denied")
+	}
+
+	rootc, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "root", Password: "123"})
+	if cerr != nil {
+		t.Fatal(cerr)
+	}
+	defer rootc.Close()
+
+	if _, err := rootc.RoleRevokePermission(context.TODO(), "role1", "k1", "k3"); err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = user1c.TimeToLive(context.TODO(), leaseID, clientv3.WithAttachedKeys())
+	if err == nil {
+		t.Fatal("timetolive from user2 should be failed with permission denied")
+	}
+}
diff --git a/pkg/ioutil/pagewriter_test.go b/pkg/ioutil/pagewriter_test.go
index 1061069..ee2fa0d 100644
--- a/pkg/ioutil/pagewriter_test.go
+++ b/pkg/ioutil/pagewriter_test.go
@@ -37,7 +37,7 @@ func TestPageWriterRandom(t *testing.T) {
 	if cw.writeBytes > n {
 		t.Fatalf("wrote %d bytes to io.Writer, but only wrote %d bytes", cw.writeBytes, n)
 	}
-	if n-cw.writeBytes > pageBytes {
+	if maxPendingBytes := pageBytes + defaultBufferBytes; n-cw.writeBytes > maxPendingBytes {
 		t.Fatalf("got %d bytes pending, expected less than %d bytes", n-cw.writeBytes, pageBytes)
 	}
 	t.Logf("total writes: %d", cw.writes)
diff --git a/tests/e2e/ctl_v3_auth_test.go b/tests/e2e/ctl_v3_auth_test.go
index 2142394..ca2524b 100644
--- a/tests/e2e/ctl_v3_auth_test.go
+++ b/tests/e2e/ctl_v3_auth_test.go
@@ -69,6 +69,7 @@ func TestCtlV3AuthJWTExpire(t *testing.T) { testCtl(t, authTestJWTExpire, withCf
 func TestCtlV3AuthCertCNAndUsernameNoPassword(t *testing.T) {
 	testCtl(t, authTestCertCNAndUsernameNoPassword, withCfg(configClientTLSCertAuth))
 }
+func TestCtlV3AuthLeaseTimeToLive(t *testing.T) { testCtl(t, authTestLeaseTimeToLive) }
 
 func authEnableTest(cx ctlCtx) {
 	if err := authEnable(cx); err != nil {
@@ -1130,3 +1131,51 @@ func authTestJWTExpire(cx ctlCtx) {
 		cx.t.Error(err)
 	}
 }
+
+func authTestLeaseTimeToLive(cx ctlCtx) {
+	if err := authEnable(cx); err != nil {
+		cx.t.Fatal(err)
+	}
+	cx.user, cx.pass = "root", "root"
+
+	authSetupTestUser(cx)
+
+	cx.user = "test-user"
+	cx.pass = "pass"
+
+	leaseID, err := ctlV3LeaseGrant(cx, 10)
+	if err != nil {
+		cx.t.Fatal(err)
+	}
+
+	err = ctlV3Put(cx, "foo", "val", leaseID)
+	if err != nil {
+		cx.t.Fatal(err)
+	}
+
+	err = ctlV3LeaseTimeToLive(cx, leaseID, true)
+	if err != nil {
+		cx.t.Fatal(err)
+	}
+
+	cx.user = "root"
+	cx.pass = "root"
+	err = ctlV3Put(cx, "bar", "val", leaseID)
+	if err != nil {
+		cx.t.Fatal(err)
+	}
+
+	cx.user = "test-user"
+	cx.pass = "pass"
+	// the lease is attached to bar, which test-user cannot access
+	err = ctlV3LeaseTimeToLive(cx, leaseID, true)
+	if err == nil {
+		cx.t.Fatal("test-user must not be able to access to the lease, because it's attached to the key bar")
+	}
+
+	// without --keys, access should be allowed
+	err = ctlV3LeaseTimeToLive(cx, leaseID, false)
+	if err != nil {
+		cx.t.Fatal(err)
+	}
+}
diff --git a/tests/e2e/ctl_v3_lease_test.go b/tests/e2e/ctl_v3_lease_test.go
index 608b8ca..64d2579 100644
--- a/tests/e2e/ctl_v3_lease_test.go
+++ b/tests/e2e/ctl_v3_lease_test.go
@@ -294,3 +294,11 @@ func ctlV3LeaseRevoke(cx ctlCtx, leaseID string) error {
 	cmdArgs := append(cx.PrefixArgs(), "lease", "revoke", leaseID)
 	return spawnWithExpect(cmdArgs, fmt.Sprintf("lease %s revoked", leaseID))
 }
+
+func ctlV3LeaseTimeToLive(cx ctlCtx, leaseID string, withKeys bool) error {
+	cmdArgs := append(cx.PrefixArgs(), "lease", "timetolive", leaseID)
+	if withKeys {
+		cmdArgs = append(cmdArgs, "--keys")
+	}
+	return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, fmt.Sprintf("lease %s granted with", leaseID))
+}
-- 
2.20.1

