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:
co-authored by
Claude Opus 5
parent
07c923c4c9
commit
1e73d192ba
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user