forgejo/modules/graceful/manager_common.go
Mathieu Fenniak f93d2cb261 ci: detect and prevent empty case statements in Go code (#11593)
One of the security patches released 2026-03-09 [fixed a vulnerability](d1c7b04d09) caused by a misapplication of Go `case` statements, where the implementation would have been correct if Go `case` statements automatically fall through to the next case block, but they do not.  This PR adds a semgrep rule which detects any empty `case` statement and raises an error, in order to prevent this coding mistake in the future.

For example, code like this will now trigger a build error:
```go
	switch setting.Protocol {
	case setting.HTTPUnix:
	case setting.FCGI:
	case setting.FCGIUnix:
	default:
		defaultLocalURL := string(setting.Protocol) + "://"
	}
```

Example error:
```
    cmd/web.go
   ❯❯❱ semgrep.config.forgejo-switch-empty-case
          switch has a case block with no content. This is treated as "break" by Go, but developers may
          confuse it for "fallthrough".  To fix this error, disambiguate by using "break" or
          "fallthrough".

          279┆ switch setting.Protocol {
          280┆ case setting.HTTPUnix:
          281┆ case setting.FCGI:
          282┆ case setting.FCGIUnix:
          283┆ default:
          284┆   defaultLocalURL := string(setting.Protocol) + "://"
          285┆   if setting.HTTPAddr == "0.0.0.0" {
          286┆           defaultLocalURL += "localhost"
          287┆   } else {
          288┆           defaultLocalURL += setting.HTTPAddr
```

As described in the error output, this error can be fixed by explicitly listing `break` (the real Go behaviour, to do nothing in the block), or by listing `fallthrough` (if the intent was to fall through).

All existing code triggering this detection has been changed to `break` (or, rarely, irrelevant cases have been removed), which should maintain the same code functionality.  While performing this fixup, a light analysis was performed on each case and they *appeared* correct, but with ~65 cases I haven't gone into extreme depth.

Tests are present for the semgrep rule in `.semgrep/tests/go.go`.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11593
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2026-03-10 02:50:28 +01:00

109 lines
3.4 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package graceful
import (
"context"
"runtime/pprof"
"sync"
"time"
)
// FIXME: it seems that there is a bug when using systemd Type=notify: the "Install Page" (INSTALL_LOCK=false) doesn't notify properly.
// At the moment, no idea whether it also affects Windows Service, or whether it's a regression bug. It needs to be investigated later.
type systemdNotifyMsg string
const (
readyMsg systemdNotifyMsg = "READY=1"
stoppingMsg systemdNotifyMsg = "STOPPING=1"
reloadingMsg systemdNotifyMsg = "RELOADING=1"
watchdogMsg systemdNotifyMsg = "WATCHDOG=1"
)
func statusMsg(msg string) systemdNotifyMsg {
return systemdNotifyMsg("STATUS=" + msg)
}
// Manager manages the graceful shutdown process
type Manager struct {
ctx context.Context
isChild bool
forked bool
lock sync.RWMutex
state state
shutdownCtx context.Context
hammerCtx context.Context
terminateCtx context.Context
managerCtx context.Context
shutdownCtxCancel context.CancelFunc
hammerCtxCancel context.CancelFunc
terminateCtxCancel context.CancelFunc
managerCtxCancel context.CancelFunc
runningServerWaitGroup sync.WaitGroup
terminateWaitGroup sync.WaitGroup
createServerCond sync.Cond
createdServer int
shutdownRequested chan struct{}
toRunAtShutdown []func()
toRunAtTerminate []func()
}
func newGracefulManager(ctx context.Context) *Manager {
manager := &Manager{ctx: ctx, shutdownRequested: make(chan struct{})}
manager.createServerCond.L = &sync.Mutex{}
manager.prepare(ctx)
manager.start()
return manager
}
func (g *Manager) prepare(ctx context.Context) {
g.terminateCtx, g.terminateCtxCancel = context.WithCancel(ctx)
g.shutdownCtx, g.shutdownCtxCancel = context.WithCancel(ctx)
g.hammerCtx, g.hammerCtxCancel = context.WithCancel(ctx)
g.managerCtx, g.managerCtxCancel = context.WithCancel(ctx)
g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels("gracefulLifecycle", "with-terminate"))
g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels("gracefulLifecycle", "with-shutdown"))
g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels("gracefulLifecycle", "with-hammer"))
g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels("gracefulLifecycle", "with-manager"))
if !g.setStateTransition(stateInit, stateRunning) {
panic("invalid graceful manager state: transition from init to running failed")
}
}
// DoImmediateHammer causes an immediate hammer
func (g *Manager) DoImmediateHammer() {
g.notify(statusMsg("Sending immediate hammer"))
g.doHammerTime(0 * time.Second)
}
// DoGracefulShutdown causes a graceful shutdown
func (g *Manager) DoGracefulShutdown() {
g.lock.Lock()
select {
case <-g.shutdownRequested:
break
default:
close(g.shutdownRequested)
}
forked := g.forked
g.lock.Unlock()
if !forked {
g.notify(stoppingMsg)
} else {
g.notify(statusMsg("Shutting down after fork"))
}
g.doShutdown()
}
// RegisterServer registers the running of a listening server, in the case of unix this means that the parent process can now die.
// Any call to RegisterServer must be matched by a call to ServerDone
func (g *Manager) RegisterServer() {
KillParent()
g.runningServerWaitGroup.Add(1)
}