mattermost/server/channels/app/webhub_fuzz.go
2025-11-12 13:00:51 +01:00

280 lines
7.4 KiB
Go

//go:build gofuzz
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"math/rand"
"net"
"net/http"
"net/http/httptest"
"os"
"strconv"
"sync"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
)
// This is a file used to fuzz test the web_hub code.
// It performs a high-level fuzzing of the web_hub by spawning a hub
// and creating connections to it with a fixed concurrency.
//
// During the fuzz test, we create the server just once, and we send
// the random byte slice through a channel and perform some actions depending
// on the random data.
// The actions are decided in the getActionData function which decides
// which user, team, channel should the message go to and some other stuff too.
//
// Since this requires help of the testing library, we have to duplicate some code
// over here because go-fuzz cannot take code from _test.go files. It won't affect
// the main build because it's behind a build tag.
//
// To run this:
// 1. go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
// 2. mv app/helper_test.go app/helper.go
// (Also reduce the number of push notification workers to 1 to debug stack traces easily.)
// 3. go-fuzz-build github.com/mattermost/mattermost/server/v8/channels/app
// 4. Generate a corpus dir. It's just a directory with files containing random data
// for go-fuzz to use as an initial seed. Use the generateInitialCorpus function for that.
// 5. go-fuzz -bin=app-fuzz.zip -workdir=./workdir
var mainHelper *testlib.MainHelper
func init() {
testing.Init()
var options = testlib.HelperOptions{
EnableStore: true,
EnableResources: true,
}
mainHelper = testlib.NewMainHelperWithOptions(&options)
}
func dummyWebsocketHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
upgrader := &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
conn, err := upgrader.Upgrade(w, req, nil)
for err == nil {
_, _, err = conn.ReadMessage()
}
if _, ok := err.(*websocket.CloseError); !ok {
panic(err)
}
}
}
func registerDummyWebConn(a *App, addr net.Addr, userID string) *WebConn {
session, appErr := a.CreateSession(&model.Session{
UserId: userID,
})
if appErr != nil {
panic(appErr)
}
d := websocket.Dialer{}
c, _, err := d.Dial("ws://"+addr.String()+"/ws", nil)
if err != nil {
panic(err)
}
wc := a.NewWebConn(c, *session, i18n.IdentityTfunc(), "en")
a.HubRegister(wc)
go wc.Pump()
return wc
}
type actionData struct {
event string
createUserID string
selectChannelID string
selectTeamID string
invalidateConnUserID string
updateConnUserID string
attachment map[string]any
}
func getActionData(data []byte, userIDs, teamIDs, channelIDs []string) *actionData {
// Some sample events
events := []string{
model.WebsocketEventChannelCreated,
model.WebsocketEventChannelDeleted,
model.WebsocketEventUserAdded,
model.WebsocketEventUserUpdated,
model.WebsocketEventStatusChange,
model.WebsocketEventHello,
model.WebsocketAuthenticationChallenge,
model.WebsocketEventReactionAdded,
model.WebsocketEventReactionRemoved,
model.WebsocketEventResponse,
}
// We need atleast 10 bytes to get all the data we need
if len(data) < 10 {
return nil
}
input := &actionData{}
// Assign userID, channelID, teamID randomly from respective byte indices
input.createUserID = userIDs[int(data[0])%len(userIDs)]
input.selectChannelID = channelIDs[int(data[1])%len(channelIDs)]
input.selectTeamID = teamIDs[int(data[2])%len(teamIDs)]
input.invalidateConnUserID = userIDs[int(data[3])%len(userIDs)]
input.updateConnUserID = userIDs[int(data[4])%len(userIDs)]
input.event = events[int(data[5])%len(events)]
data = data[6:]
input.attachment = make(map[string]any)
for len(data) >= 4 { // 2 bytes key, 2 bytes value
k := data[:2]
v := data[2:4]
input.attachment[string(k)] = v
data = data[4:]
}
return input
}
var startServerOnce sync.Once
var dataChan chan []byte
var resChan = make(chan int, 4) // buffer of 4 to keep reading results.
func Fuzz(data []byte) int {
// We don't want to close anything down as the fuzzer will keep on running forever.
startServerOnce.Do(func() {
t := &testing.T{}
th := Setup(t).InitBasic(t)
s := httptest.NewServer(dummyWebsocketHandler())
th.Server.HubStart()
u1 := th.CreateUser(t)
u2 := th.CreateUser(t)
u3 := th.CreateUser(t)
t1 := th.CreateTeam(t)
t2 := th.CreateTeam(t)
ch1 := th.CreateDmChannel(u1)
ch2 := th.CreateChannel(t1)
ch3 := th.CreateChannel(t2)
th.LinkUserToTeam(u1, t1)
th.LinkUserToTeam(u1, t2)
th.LinkUserToTeam(u2, t1)
th.LinkUserToTeam(u2, t2)
th.LinkUserToTeam(u3, t1)
th.LinkUserToTeam(u3, t2)
th.AddUserToChannel(u1, ch2)
th.AddUserToChannel(u2, ch2)
th.AddUserToChannel(u3, ch2)
th.AddUserToChannel(u1, ch3)
th.AddUserToChannel(u2, ch3)
th.AddUserToChannel(u3, ch3)
sema := make(chan struct{}, 4) // A counting semaphore with concurrency of 4.
dataChan = make(chan []byte)
go func() {
for {
// get data
data, ok := <-dataChan
if !ok {
return
}
// acquire semaphore
sema <- struct{}{}
go func(data []byte) {
defer func() {
// release semaphore
<-sema
}()
var returnCode int
defer func() {
resChan <- returnCode
}()
// assign data randomly
// 3 users, 2 teams, 3 channels
input := getActionData(data,
[]string{u1.Id, u2.Id, u3.Id, ""},
[]string{t1.Id, t2.Id, ""},
[]string{ch1.Id, ch2.Id, ""})
if input == nil {
returnCode = 0
return
}
// We get the input from the random data.
// Now we perform some actions based on that.
conn := registerDummyWebConn(th.App, s.Listener.Addr(), input.createUserID)
defer func() {
conn.Close()
// A sleep of 2 seconds to allow other connections
// from the same user to be created, before unregistering them.
// This hits some additional code paths.
go func() {
time.Sleep(2 * time.Second)
th.App.HubUnregister(conn)
}()
}()
msg := model.NewWebSocketEvent(input.event,
input.selectTeamID,
input.selectChannelID,
input.createUserID, nil, "")
for k, v := range input.attachment {
msg.Add(k, v)
}
th.App.Publish(msg)
th.App.InvalidateWebConnSessionCacheForUser(input.invalidateConnUserID)
sessions, err := th.App.GetSessions(input.updateConnUserID)
if err != nil {
panic(err)
}
if len(sessions) > 0 {
th.App.UpdateWebConnUserActivity(*sessions[0], model.GetMillis())
}
returnCode = 1
}(data)
}
}()
})
// send data to dataChan
dataChan <- data
// get data from res chan
result := <-resChan
return result
}
// generateInitialCorpus generates the corpus for go-fuzz.
// Place this function in any main.go file and run it.
// Use the generated directory as the corpus.
func generateInitialCorpus() error {
err := os.MkdirAll("workdir/corpus", 0755)
if err != nil {
return err
}
for i := 0; i < 100; i++ {
data := make([]byte, 25)
_, err = rand.Read(data)
if err != nil {
return err
}
err = os.WriteFile("./workdir/corpus"+strconv.Itoa(i), data, 0644)
if err != nil {
return err
}
}
return nil
}