From b882ccc86d6d12133bd95f758444020f48e059f2 Mon Sep 17 00:00:00 2001 From: Paul Wells Date: Wed, 17 Jun 2026 12:35:59 -0700 Subject: [PATCH] service: cap all metadata at 512 KiB; enforce on join, agent dispatch, and embedded agents (#4602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * service: enforce metadata size limit in CreateRoom, bump default to 512 KiB CreateRoom previously accepted any metadata size; only UpdateRoomMetadata rejected oversized payloads. Mirror the same CheckMetadataSize check at the CreateRoom API boundary so both entrypoints are bounded. Default MaxMetadataSize moves from 64000 to 512 * 1024 to match the practical needs of customers using room metadata for richer state. The limit remains configurable via the existing limits.max_metadata_size knob. * service: split room vs. participant metadata limit, enforce on join + agent dispatch LimitConfig.MaxMetadataSize was shared between room metadata and participant metadata. Last commit's bump to 512 KiB lifted both ceilings; this restores the participant ceiling to 64 KB and introduces a separate MaxRoomMetadataSize (default 512 KiB) for room metadata. Additional enforcement: - RoomManager.StartSession rejects joins whose JWT-grants metadata or attributes exceed the participant/attributes limits. The check was missing entirely from this path. - AgentDispatchService.CreateDispatch and the embedded CreateRoomRequest.Agents path now validate metadata and attributes against the common 64 KB ceilings (previously unbounded). NewAgentDispatchService gains a LimitConfig parameter; the two wire_gen callsites are updated. * service: collapse metadata size limit to single 512 KiB knob Reverts the LimitConfig split introduced in the previous commit: MaxRoomMetadataSize, CheckRoomMetadataSize, and the max_room_metadata_size yaml key are removed. MaxMetadataSize moves back to 512 * 1024 and gates all metadata uniformly — room (CreateRoom, UpdateRoomMetadata), participant (UpdateParticipant, signal UpdateMetadata, JWT grants on join), and agent dispatch (CreateDispatch + embedded RoomAgentDispatch). MaxAttributesSize stays at 64 KB and continues to gate participant and agent-dispatch attributes separately. Test cases consolidated under the single knob. * kb -> kib --- pkg/config/config.go | 4 +- pkg/service/agent_dispatch_service.go | 11 +++ pkg/service/roommanager.go | 9 ++ pkg/service/roomservice.go | 18 +++- pkg/service/roomservice_test.go | 119 ++++++++++++++++++++------ pkg/service/wire_gen.go | 2 +- 6 files changed, 133 insertions(+), 30 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 692c0af38..c77d7d241 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -440,8 +440,8 @@ var DefaultConfig = Config{ UpdateBatchTargetSize: 128 * 1024, }, Limit: LimitConfig{ - MaxMetadataSize: 64000, - MaxAttributesSize: 64000, + MaxMetadataSize: 512 * 1024, + MaxAttributesSize: 64 * 1024, MaxRoomNameLength: 256, MaxParticipantIdentityLength: 256, MaxParticipantNameLength: 256, diff --git a/pkg/service/agent_dispatch_service.go b/pkg/service/agent_dispatch_service.go index 49c820c52..b15bc55a3 100644 --- a/pkg/service/agent_dispatch_service.go +++ b/pkg/service/agent_dispatch_service.go @@ -18,6 +18,7 @@ import ( "context" "fmt" + "github.com/livekit/livekit-server/pkg/config" "github.com/livekit/livekit-server/pkg/routing" "github.com/livekit/protocol/agent" "github.com/livekit/protocol/livekit" @@ -29,6 +30,7 @@ import ( ) type AgentDispatchService struct { + limitConf config.LimitConfig agentDispatchClient rpc.TypedAgentDispatchInternalClient topicFormatter rpc.TopicFormatter roomAllocator RoomAllocator @@ -36,12 +38,14 @@ type AgentDispatchService struct { } func NewAgentDispatchService( + limitConf config.LimitConfig, agentDispatchClient rpc.TypedAgentDispatchInternalClient, topicFormatter rpc.TopicFormatter, roomAllocator RoomAllocator, router routing.MessageRouter, ) *AgentDispatchService { return &AgentDispatchService{ + limitConf: limitConf, agentDispatchClient: agentDispatchClient, topicFormatter: topicFormatter, roomAllocator: roomAllocator, @@ -60,6 +64,13 @@ func (ag *AgentDispatchService) CreateDispatch(ctx context.Context, req *livekit return nil, psrpc.NewError(psrpc.InvalidArgument, err) } + if !ag.limitConf.CheckMetadataSize(req.Metadata) { + return nil, ErrMetadataExceedsLimits + } + if !ag.limitConf.CheckAttributesSize(req.Attributes) { + return nil, ErrAttributeExceedsLimits + } + if ag.roomAllocator.AutoCreateEnabled(ctx) { err := ag.roomAllocator.SelectRoomNode(ctx, livekit.RoomName(req.Room), "") if err != nil { diff --git a/pkg/service/roommanager.go b/pkg/service/roommanager.go index 921c73e8c..13ca67493 100644 --- a/pkg/service/roommanager.go +++ b/pkg/service/roommanager.go @@ -293,6 +293,15 @@ func (r *RoomManager) StartSession( ) error { sessionStartTime := time.Now() + if pi.Identity != "" && pi.Grants != nil { + if !r.config.Limit.CheckMetadataSize(pi.Grants.Metadata) { + return ErrMetadataExceedsLimits + } + if !r.config.Limit.CheckAttributesSize(pi.Grants.Attributes) { + return ErrAttributeExceedsLimits + } + } + createRoom := pi.CreateRoom room, err := r.getOrCreateRoom(ctx, createRoom) if err != nil { diff --git a/pkg/service/roomservice.go b/pkg/service/roomservice.go index 3579eecc2..2cdf373a2 100644 --- a/pkg/service/roomservice.go +++ b/pkg/service/roomservice.go @@ -84,6 +84,19 @@ func (s *RoomService) CreateRoom(ctx context.Context, req *livekit.CreateRoomReq return nil, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, s.limitConf.MaxRoomNameLength) } + if !s.limitConf.CheckMetadataSize(req.Metadata) { + return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxMetadataSize))) + } + + for _, ad := range req.Agents { + if !s.limitConf.CheckMetadataSize(ad.Metadata) { + return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxMetadataSize))) + } + if !s.limitConf.CheckAttributesSize(ad.Attributes) { + return nil, twirp.InvalidArgumentError(ErrAttributeExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxAttributesSize))) + } + } + err := s.roomAllocator.SelectRoomNode(ctx, livekit.RoomName(req.Name), livekit.NodeID(req.NodeId)) if err != nil { return nil, err @@ -320,9 +333,8 @@ func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.Updat RecordRequest(ctx, req) AppendLogFields(ctx, "room", req.Room, "size", len(req.Metadata)) - maxMetadataSize := int(s.limitConf.MaxMetadataSize) - if maxMetadataSize > 0 && len(req.Metadata) > maxMetadataSize { - return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(maxMetadataSize)) + if !s.limitConf.CheckMetadataSize(req.Metadata) { + return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxMetadataSize))) } if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil { diff --git a/pkg/service/roomservice_test.go b/pkg/service/roomservice_test.go index d34d5103e..5b27f2fdf 100644 --- a/pkg/service/roomservice_test.go +++ b/pkg/service/roomservice_test.go @@ -47,43 +47,72 @@ func TestDeleteRoom(t *testing.T) { } func TestMetaDataLimits(t *testing.T) { - t.Run("metadata exceed limits", func(t *testing.T) { + adminCtx := func() context.Context { + return service.WithGrants(context.Background(), &auth.ClaimGrants{Video: &auth.VideoGrant{}}, "") + } + createCtx := func() context.Context { + return service.WithGrants(context.Background(), &auth.ClaimGrants{Video: &auth.VideoGrant{RoomCreate: true}}, "") + } + requireInvalidArg := func(t *testing.T, err error) { + t.Helper() + terr, ok := err.(twirp.Error) + require.True(t, ok, "expected twirp error, got %T (%v)", err, err) + require.Equal(t, twirp.InvalidArgument, terr.Code()) + } + + t.Run("metadata exceeds limit", func(t *testing.T) { svc := newTestRoomService(config.LimitConfig{MaxMetadataSize: 5}) - grant := &auth.ClaimGrants{ - Video: &auth.VideoGrant{}, - } - ctx := service.WithGrants(context.Background(), grant, "") - _, err := svc.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{ + + _, err := svc.UpdateParticipant(adminCtx(), &livekit.UpdateParticipantRequest{ Room: "testroom", Identity: "123", Metadata: "abcdefg", }) - terr, ok := err.(twirp.Error) - require.True(t, ok) - require.Equal(t, twirp.InvalidArgument, terr.Code()) + requireInvalidArg(t, err) - _, err = svc.UpdateRoomMetadata(ctx, &livekit.UpdateRoomMetadataRequest{ + _, err = svc.UpdateRoomMetadata(adminCtx(), &livekit.UpdateRoomMetadataRequest{ Room: "testroom", Metadata: "abcdefg", }) - terr, ok = err.(twirp.Error) - require.True(t, ok) - require.Equal(t, twirp.InvalidArgument, terr.Code()) + requireInvalidArg(t, err) + + _, err = svc.CreateRoom(createCtx(), &livekit.CreateRoomRequest{ + Name: "testroom", + Metadata: "abcdefg", + }) + requireInvalidArg(t, err) + + _, err = svc.CreateRoom(createCtx(), &livekit.CreateRoomRequest{ + Name: "testroom", + Agents: []*livekit.RoomAgentDispatch{ + {AgentName: "bot", Metadata: "abcdefg"}, + }, + }) + requireInvalidArg(t, err) + }) + + t.Run("embedded agent dispatch in CreateRoom exceeds attributes limit", func(t *testing.T) { + svc := newTestRoomService(config.LimitConfig{MaxAttributesSize: 5}) + _, err := svc.CreateRoom(createCtx(), &livekit.CreateRoomRequest{ + Name: "testroom", + Agents: []*livekit.RoomAgentDispatch{ + {AgentName: "bot", Attributes: map[string]string{"key": "abcdefg"}}, + }, + }) + requireInvalidArg(t, err) }) notExceedsLimitsSvc := map[string]*TestRoomService{ - "metadata exceeds limits": newTestRoomService(config.LimitConfig{MaxMetadataSize: 5}), - "metadata no limits": newTestRoomService(config.LimitConfig{}), // no limits + "metadata exceeds limits": newTestRoomService(config.LimitConfig{ + MaxMetadataSize: 5, + MaxAttributesSize: 5, + }), + "metadata no limits": newTestRoomService(config.LimitConfig{}), } - for n, s := range notExceedsLimitsSvc { - svc := s + for n, svc := range notExceedsLimitsSvc { t.Run(n, func(t *testing.T) { - grant := &auth.ClaimGrants{ - Video: &auth.VideoGrant{}, - } - ctx := service.WithGrants(context.Background(), grant, "") - _, err := svc.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{ + _, err := svc.UpdateParticipant(adminCtx(), &livekit.UpdateParticipantRequest{ Room: "testroom", Identity: "123", Metadata: "abc", @@ -92,18 +121,60 @@ func TestMetaDataLimits(t *testing.T) { require.True(t, ok) require.NotEqual(t, twirp.InvalidArgument, terr.Code()) - _, err = svc.UpdateRoomMetadata(ctx, &livekit.UpdateRoomMetadataRequest{ + _, err = svc.UpdateRoomMetadata(adminCtx(), &livekit.UpdateRoomMetadataRequest{ Room: "testroom", Metadata: "abc", }) terr, ok = err.(twirp.Error) require.True(t, ok) require.NotEqual(t, twirp.InvalidArgument, terr.Code()) - }) + _, err = svc.CreateRoom(createCtx(), &livekit.CreateRoomRequest{ + Name: "testroom", + Metadata: "abc", + Agents: []*livekit.RoomAgentDispatch{ + {AgentName: "bot", Metadata: "abc", Attributes: map[string]string{"k": "v"}}, + }, + }) + if err != nil { + terr, ok = err.(twirp.Error) + require.True(t, ok) + require.NotEqual(t, twirp.InvalidArgument, terr.Code()) + } + }) } } +func TestAgentDispatchMetadataLimits(t *testing.T) { + ctx := service.WithGrants(context.Background(), &auth.ClaimGrants{ + Video: &auth.VideoGrant{Room: "testroom", RoomAdmin: true}, + }, "") + + t.Run("metadata exceeds limits", func(t *testing.T) { + svc := newTestAgentDispatchService(config.LimitConfig{MaxMetadataSize: 5}) + _, err := svc.CreateDispatch(ctx, &livekit.CreateAgentDispatchRequest{ + Room: "testroom", + Metadata: "abcdefg", + }) + require.ErrorIs(t, err, service.ErrMetadataExceedsLimits) + }) + + t.Run("attributes exceeds limits", func(t *testing.T) { + svc := newTestAgentDispatchService(config.LimitConfig{MaxAttributesSize: 5}) + _, err := svc.CreateDispatch(ctx, &livekit.CreateAgentDispatchRequest{ + Room: "testroom", + Attributes: map[string]string{"key": "abcdefg"}, + }) + require.ErrorIs(t, err, service.ErrAttributeExceedsLimits) + }) +} + +func newTestAgentDispatchService(limitConf config.LimitConfig) *service.AgentDispatchService { + allocator := &servicefakes.FakeRoomAllocator{} + allocator.AutoCreateEnabledReturns(false) + return service.NewAgentDispatchService(limitConf, nil, rpc.NewTopicFormatter(), allocator, &routingfakes.FakeRouter{}) +} + func newTestRoomService(limitConf config.LimitConfig) *TestRoomService { router := &routingfakes.FakeRouter{} allocator := &servicefakes.FakeRoomAllocator{} diff --git a/pkg/service/wire_gen.go b/pkg/service/wire_gen.go index 45ebcf4d2..b64fbb063 100644 --- a/pkg/service/wire_gen.go +++ b/pkg/service/wire_gen.go @@ -102,7 +102,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live if err != nil { return nil, err } - agentDispatchService := NewAgentDispatchService(agentDispatchInternalClient, topicFormatter, roomAllocator, router) + agentDispatchService := NewAgentDispatchService(limitConfig, agentDispatchInternalClient, topicFormatter, roomAllocator, router) egressService := NewEgressService(egressClient, rtcEgressLauncher, ioInfoService, roomService) ingressConfig := getIngressConfig(conf) ingressClient, err := rpc.NewIngressClient(clientParams)