Skip to content

Commit 4a008a8

Browse files
authored
chore: prevent nil dereferences on cmd handlers (#8319)
* chore: detect nil cmd handlers Prevent nil panic dereferences on cmd handlers. Add a unit test to prevent future mistakes
1 parent d70e2d9 commit 4a008a8

File tree

5 files changed

+59
-0
lines changed

5 files changed

+59
-0
lines changed

cli/clitest/handlers.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package clitest
2+
3+
import (
4+
"testing"
5+
6+
"github.com/coder/coder/cli/clibase"
7+
)
8+
9+
// HandlersOK asserts that all commands have a handler.
10+
// Without a handler, the command has no default behavior. Even for
11+
// non-root commands (like 'groups' or 'users'), a handler is required.
12+
// These handlers are likely just the 'help' handler, but this must be
13+
// explicitly set.
14+
func HandlersOK(t *testing.T, cmd *clibase.Cmd) {
15+
cmd.Walk(func(cmd *clibase.Cmd) {
16+
if cmd.Handler == nil {
17+
// If you see this error, make the Handler a helper invoker.
18+
// Handler: func(inv *clibase.Invocation) error {
19+
// return inv.Command.HelpHandler(inv)
20+
// },
21+
t.Errorf("command %q has no handler, change to a helper invoker using: 'inv.Command.HelpHandler(inv)'", cmd.Name())
22+
}
23+
})
24+
}

cli/root.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,18 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
260260
// Add a wrapper to every command to enable debugging options.
261261
cmd.Walk(func(cmd *clibase.Cmd) {
262262
h := cmd.Handler
263+
if h == nil {
264+
// We should never have a nil handler, but if we do, do not
265+
// wrap it. Wrapping it just hides a nil pointer dereference.
266+
// If a nil handler exists, this is a developer bug. If no handler
267+
// is required for a command such as command grouping (e.g. `users'
268+
// and 'groups'), then the handler should be set to the helper
269+
// function.
270+
// func(inv *clibase.Invocation) error {
271+
// return inv.Command.HelpHandler(inv)
272+
// }
273+
return
274+
}
263275
cmd.Handler = func(i *clibase.Invocation) error {
264276
if !debugOptions {
265277
return h(i)

cli/root_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,13 @@ func TestDERPHeaders(t *testing.T) {
181181

182182
require.Greater(t, atomic.LoadInt64(&derpCalled), int64(0), "expected /derp to be called at least once")
183183
}
184+
185+
func TestHandlersOK(t *testing.T) {
186+
t.Parallel()
187+
188+
var root cli.RootCmd
189+
cmd, err := root.Command(root.Core())
190+
require.NoError(t, err)
191+
192+
clitest.HandlersOK(t, cmd)
193+
}

enterprise/cli/provisionerdaemons.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ func (r *RootCmd) provisionerDaemons() *clibase.Cmd {
2727
cmd := &clibase.Cmd{
2828
Use: "provisionerd",
2929
Short: "Manage provisioner daemons",
30+
Handler: func(inv *clibase.Invocation) error {
31+
return inv.Command.HelpHandler(inv)
32+
},
3033
Children: []*clibase.Cmd{
3134
r.provisionerDaemonStart(),
3235
},

enterprise/cli/root_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@ func newCLI(t *testing.T, args ...string) (*clibase.Invocation, config.Root) {
1717
require.NoError(t, err)
1818
return clitest.NewWithCommand(t, cmd, args...)
1919
}
20+
21+
func TestEnterpriseHandlersOK(t *testing.T) {
22+
t.Parallel()
23+
24+
var root cli.RootCmd
25+
cmd, err := root.Command(root.EnterpriseSubcommands())
26+
require.NoError(t, err)
27+
28+
clitest.HandlersOK(t, cmd)
29+
}

0 commit comments

Comments
 (0)