reject a channel parent that is its own descendant

handleChannelState applied whatever parent the server named, so a channel
could be made its own ancestor. That leaves Parent/Children cyclic, and
everything that walks the channel tree afterwards recurses until it exhausts
memory.

Ignore such a move and keep the existing parent rather than corrupting the
graph. The ancestry walk is itself bounded so a graph that is already cyclic
cannot hang the check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon McGinty
2026-08-24 12:30:44 -04:00
co-authored by Claude Opus 5
parent 07c923c4c9
commit 1e73d192ba
2 changed files with 119 additions and 9 deletions
@@ -0,0 +1,84 @@
package gumble
import (
"testing"
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
"google.golang.org/protobuf/proto"
)
// Regression: handleChannelState accepted any parent the server named, so a
// channel could be made its own ancestor. Everything that walks the resulting
// Parent/Children graph then recurses until it exhausts memory.
func TestChannelStateRejectsSelfParent(t *testing.T) {
c := &Client{Config: NewConfig(), Channels: make(Channels)}
root := c.Channels.create(0)
child := c.Channels.create(1)
child.Parent = root
root.Children[child.ID] = child
id, parent := child.ID, child.ID
data, _ := proto.Marshal(&MumbleProto.ChannelState{ChannelId: &id, Parent: &parent})
if err := c.handleChannelState(data); err != nil {
t.Fatal(err)
}
if child.Parent == child {
t.Fatal("channel was made its own parent")
}
if _, ok := child.Children[child.ID]; ok {
t.Fatal("channel was made its own child")
}
if child.Parent != root {
t.Fatal("the rejected move should have left the original parent intact")
}
}
// A channel must not be reparented under one of its own descendants either.
func TestChannelStateRejectsDescendantParent(t *testing.T) {
c := &Client{Config: NewConfig(), Channels: make(Channels)}
root := c.Channels.create(0)
middle := c.Channels.create(1)
leaf := c.Channels.create(2)
middle.Parent, root.Children[middle.ID] = root, middle
leaf.Parent, middle.Children[leaf.ID] = middle, leaf
id, parent := middle.ID, leaf.ID
data, _ := proto.Marshal(&MumbleProto.ChannelState{ChannelId: &id, Parent: &parent})
if err := c.handleChannelState(data); err != nil {
t.Fatal(err)
}
if middle.Parent != root {
t.Fatal("a cyclic reparent was applied instead of ignored")
}
if isChannelDescendant(middle.Parent, middle) {
t.Fatal("channel graph is cyclic")
}
}
// A legitimate move must still be applied.
func TestChannelStateAllowsNonCyclicMove(t *testing.T) {
c := &Client{Config: NewConfig(), Channels: make(Channels)}
root := c.Channels.create(0)
a := c.Channels.create(1)
b := c.Channels.create(2)
a.Parent, root.Children[a.ID] = root, a
b.Parent, root.Children[b.ID] = root, b
id, parent := b.ID, a.ID
data, _ := proto.Marshal(&MumbleProto.ChannelState{ChannelId: &id, Parent: &parent})
if err := c.handleChannelState(data); err != nil {
t.Fatal(err)
}
if b.Parent != a {
t.Fatal("a valid reparent was rejected")
}
if a.Children[b.ID] != b {
t.Fatal("child link missing after a valid reparent")
}
if _, ok := root.Children[b.ID]; ok {
t.Fatal("stale child link left on the old parent")
}
}
+27 -1
View File
@@ -447,6 +447,23 @@ func (c *Client) handleChannelRemove(buffer []byte) error {
return nil return nil
} }
// maxChannelDepth bounds ancestry walks over a channel tree that may already
// be cyclic. Real Mumble trees are far shallower than this.
const maxChannelDepth = 1024
// isChannelDescendant reports whether candidate is channel itself or sits
// below it in the channel tree. The walk is bounded so an already-cyclic
// graph cannot hang the caller.
func isChannelDescendant(candidate, channel *Channel) bool {
for i := 0; candidate != nil && i <= maxChannelDepth; i++ {
if candidate == channel {
return true
}
candidate = candidate.Parent
}
return false
}
func (c *Client) handleChannelState(buffer []byte) error { func (c *Client) handleChannelState(buffer []byte) error {
var packet MumbleProto.ChannelState var packet MumbleProto.ChannelState
if err := proto.Unmarshal(buffer, &packet); err != nil { if err := proto.Unmarshal(buffer, &packet); err != nil {
@@ -473,10 +490,18 @@ func (c *Client) handleChannelState(buffer []byte) error {
} }
event.Channel = channel event.Channel = channel
if packet.Parent != nil { if packet.Parent != nil {
newParent := c.Channels[*packet.Parent]
// Reparenting a channel under itself or one of its own
// descendants makes Parent/Children cyclic, and anything that
// walks the tree then recurses until it exhausts memory. Ignore
// the move rather than corrupt the channel graph.
if isChannelDescendant(newParent, channel) {
log.Warn("handleChannelState: ignoring cyclic parent %d for channel %d",
*packet.Parent, channelID)
} else {
if channel.Parent != nil { if channel.Parent != nil {
delete(channel.Parent.Children, channelID) delete(channel.Parent.Children, channelID)
} }
newParent := c.Channels[*packet.Parent]
if newParent != channel.Parent { if newParent != channel.Parent {
event.Type |= ChannelChangeMoved event.Type |= ChannelChangeMoved
} }
@@ -485,6 +510,7 @@ func (c *Client) handleChannelState(buffer []byte) error {
channel.Parent.Children[channel.ID] = channel channel.Parent.Children[channel.ID] = channel
} }
} }
}
if packet.Name != nil { if packet.Name != nil {
if *packet.Name != channel.Name { if *packet.Name != channel.Name {
event.Type |= ChannelChangeName event.Type |= ChannelChangeName