Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/nvidia-container-runtime-hook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/NVIDIA/nvidia-container-toolkit/internal/info"
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
)

var (
Expand Down Expand Up @@ -150,8 +151,8 @@ func doPrestart() {
args = append(args, rootfs)

env := append(os.Environ(), cli.Environment...)
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection?
err = syscall.Exec(args[0], args, env)
args = oci.Escape(args)
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to syscall.Exec is incorrect. The syscall.Exec function directly invokes the execve system call without shell interpretation, so shell metacharacter escaping will cause literal backslashes to be passed to the program. Remove the oci.Escape call.

Suggested change
args = oci.Escape(args)

Copilot uses AI. Check for mistakes.
err = syscall.Exec(args[0], args, env) //nolint:gosec
log.Panicln("exec failed:", err)
}

Expand Down
20 changes: 10 additions & 10 deletions cmd/nvidia-container-runtime/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/NVIDIA/nvidia-container-toolkit/internal/modifier"
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
"github.com/NVIDIA/nvidia-container-toolkit/internal/test"
)

Expand Down Expand Up @@ -87,8 +88,7 @@ func TestBadInput(t *testing.T) {
t.Fatal(err)
}

//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle")
cmdCreate := exec.Command(oci.Escape([]string{nvidiaRuntime})[0], oci.Escape([]string{"create", "--bundle"})...) //nolint:gosec
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, breaking the command. Remove the oci.Escape calls.

Suggested change
cmdCreate := exec.Command(oci.Escape([]string{nvidiaRuntime})[0], oci.Escape([]string{"create", "--bundle"})...) //nolint:gosec
cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle") //nolint:gosec

Copilot uses AI. Check for mistakes.
t.Logf("executing: %s\n", strings.Join(cmdCreate.Args, " "))
err = cmdCreate.Run()
require.Error(t, err, "runtime should return an error")
Expand All @@ -105,8 +105,8 @@ func TestGoodInput(t *testing.T) {
t.Fatalf("error generating runtime spec: %v", err)
}

//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
cmdRun := exec.Command(nvidiaRuntime, "run", "--bundle", cfg.bundlePath(), "testcontainer")
//nolint:gosec
cmdRun := exec.Command(oci.Escape([]string{nvidiaRuntime})[0], oci.Escape([]string{"run", "--bundle", cfg.bundlePath(), "testcontainer"})...)
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, breaking the command. Remove the oci.Escape calls.

Copilot uses AI. Check for mistakes.
t.Logf("executing: %s\n", strings.Join(cmdRun.Args, " "))
output, err := cmdRun.CombinedOutput()
require.NoErrorf(t, err, "runtime should not return an error", "output=%v", string(output))
Expand All @@ -116,8 +116,8 @@ func TestGoodInput(t *testing.T) {
require.NoError(t, err, "should be no errors when reading and parsing spec from config.json")
require.Empty(t, spec.Hooks, "there should be no hooks in config.json")

//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle", cfg.bundlePath(), "testcontainer")
//nolint:gosec
cmdCreate := exec.Command(oci.Escape([]string{nvidiaRuntime})[0], oci.Escape([]string{"create", "--bundle", cfg.bundlePath(), "testcontainer"})...)
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, breaking the command. Remove the oci.Escape calls.

Copilot uses AI. Check for mistakes.
t.Logf("executing: %s\n", strings.Join(cmdCreate.Args, " "))
err = cmdCreate.Run()
require.NoError(t, err, "runtime should not return an error")
Expand Down Expand Up @@ -161,8 +161,8 @@ func TestDuplicateHook(t *testing.T) {
}

// Test how runtime handles already existing prestart hook in config.json
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle", cfg.bundlePath(), "testcontainer")
//nolint:gosec
cmdCreate := exec.Command(oci.Escape([]string{nvidiaRuntime})[0], oci.Escape([]string{"create", "--bundle", cfg.bundlePath(), "testcontainer"})...)
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, breaking the command. Remove the oci.Escape calls.

Copilot uses AI. Check for mistakes.
t.Logf("executing: %s\n", strings.Join(cmdCreate.Args, " "))
output, err := cmdCreate.CombinedOutput()
require.NoErrorf(t, err, "runtime should not return an error", "output=%v", string(output))
Expand Down Expand Up @@ -230,8 +230,8 @@ func (c testConfig) generateNewRuntimeSpec() error {
return err
}

//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
cmd := exec.Command("cp", c.unmodifiedSpecFile(), c.specFilePath())
//nolint:gosec
cmd := exec.Command("cp", oci.Escape([]string{c.unmodifiedSpecFile(), c.specFilePath()})...)
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, which will be interpreted as part of the file paths, causing the cp command to fail. Remove the oci.Escape call.

Suggested change
cmd := exec.Command("cp", oci.Escape([]string{c.unmodifiedSpecFile(), c.specFilePath()})...)
cmd := exec.Command("cp", c.unmodifiedSpecFile(), c.specFilePath())

Copilot uses AI. Check for mistakes.
err = cmd.Run()
if err != nil {
return err
Expand Down
5 changes: 3 additions & 2 deletions cmd/nvidia-ctk-installer/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/sirupsen/logrus"

"github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk-installer/container/operator"
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
"github.com/NVIDIA/nvidia-container-toolkit/pkg/config/engine"
)

Expand Down Expand Up @@ -147,8 +148,8 @@ func (o Options) SystemdRestart(service string) error {

logrus.Infof("Restarting %v%v using systemd: %v", service, msg, args)

//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
cmd := exec.Command(args[0], args[1:]...)
args = oci.Escape(args)
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to exec.Command is incorrect. The exec.Command function directly executes the binary without shell interpretation, so adding backslash escaping will cause literal backslashes to be passed to the program. Shell escaping is only needed when using exec.CommandContext with a shell interpreter (e.g., sh -c). Remove the oci.Escape call.

Suggested change
args = oci.Escape(args)

Copilot uses AI. Check for mistakes.
cmd := exec.Command(args[0], args[1:]...) //nolint:gosec
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
Expand Down
8 changes: 4 additions & 4 deletions internal/ldconfig/safe-exec_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ import (
"syscall"

"github.com/opencontainers/runc/libcontainer/exeseal"

"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
)

// SafeExec attempts to clone the specified binary (as an memfd, for example) before executing it.
func SafeExec(path string, args []string, envv []string) error {
safeExe, err := cloneBinary(path)
if err != nil {
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
return syscall.Exec(path, args, envv)
return syscall.Exec(path, oci.Escape(args), envv) //nolint:gosec
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to syscall.Exec is incorrect. The syscall.Exec function directly invokes the execve system call without shell interpretation, so shell metacharacter escaping will cause literal backslashes to be passed to the program. Remove the oci.Escape call.

Copilot uses AI. Check for mistakes.
}
defer safeExe.Close()

exePath := "/proc/self/fd/" + strconv.Itoa(int(safeExe.Fd()))
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
return syscall.Exec(exePath, args, envv)
return syscall.Exec(exePath, oci.Escape(args), envv) //nolint:gosec
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to syscall.Exec is incorrect. The syscall.Exec function directly invokes the execve system call without shell interpretation, so shell metacharacter escaping will cause literal backslashes to be passed to the program. Remove the oci.Escape call.

Copilot uses AI. Check for mistakes.
}

func cloneBinary(path string) (*os.File, error) {
Expand Down
9 changes: 6 additions & 3 deletions internal/ldconfig/safe-exec_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@

package ldconfig

import "syscall"
import (
"syscall"

"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
)

// SafeExec is not implemented on non-linux systems and forwards directly to the
// Exec syscall.
func SafeExec(path string, args []string, envv []string) error {
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
return syscall.Exec(path, args, envv)
return syscall.Exec(path, oci.Escape(args), envv) //nolint:gosec
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to syscall.Exec is incorrect. The syscall.Exec function directly invokes the execve system call without shell interpretation, so shell metacharacter escaping will cause literal backslashes to be passed to the program. Remove the oci.Escape call.

Suggested change
return syscall.Exec(path, oci.Escape(args), envv) //nolint:gosec
return syscall.Exec(path, args, envv) //nolint:gosec

Copilot uses AI. Check for mistakes.
}
2 changes: 1 addition & 1 deletion internal/nvsandboxutils/cgo_helpers_static.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package nvsandboxutils
var cgoAllocsUnknown = new(struct{})

func clen(n []byte) int {
for i := 0; i < len(n); i++ {
for i := range n {
if n[i] == 0 {
return i
}
Expand Down
12 changes: 6 additions & 6 deletions internal/nvsandboxutils/zz_generated.api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ package nvsandboxutils

// The variables below represent package level methods from the library type.
var (
ErrorString = libnvsandboxutils.ErrorString
ErrorString = libnvsandboxutils.ErrorString
GetDriverVersion = libnvsandboxutils.GetDriverVersion
GetFileContent = libnvsandboxutils.GetFileContent
GetGpuResource = libnvsandboxutils.GetGpuResource
Init = libnvsandboxutils.Init
LookupSymbol = libnvsandboxutils.LookupSymbol
Shutdown = libnvsandboxutils.Shutdown
GetFileContent = libnvsandboxutils.GetFileContent
GetGpuResource = libnvsandboxutils.GetGpuResource
Init = libnvsandboxutils.Init
LookupSymbol = libnvsandboxutils.LookupSymbol
Shutdown = libnvsandboxutils.Shutdown
)

// Interface represents the interface for the library type.
Expand Down
34 changes: 32 additions & 2 deletions internal/oci/runtime_syscall_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,28 @@ package oci
import (
"fmt"
"os"
"regexp"
"strings"
"syscall"
)

// shellMetachars represents a set of shell metacharacters that are commonly
// used for shell scripting and may lead to security vulnerabilities if not
// properly handled.
//
// These metacharacters include: | & ; ( ) < > \t \n $ \ `
const shellMetachars = "|&;()<> \t\n$\\`'\""

// metacharRegex matches any shell metacharcter.
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'metacharcter' to 'metacharacter'.

Suggested change
// metacharRegex matches any shell metacharcter.
// metacharRegex matches any shell metacharacter.

Copilot uses AI. Check for mistakes.
var metacharRegex = regexp.MustCompile(`([` + regexp.QuoteMeta(shellMetachars) + `])`)

type syscallExec struct{}

var _ Runtime = (*syscallExec)(nil)

func (r syscallExec) Exec(args []string) error {
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
err := syscall.Exec(args[0], args, os.Environ())
args = Escape(args)
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping arguments passed to syscall.Exec is incorrect. The syscall.Exec function directly invokes the execve system call, which does not interpret shell metacharacters - it passes arguments as-is to the new process. Adding backslash escaping will cause the literal backslashes to be passed to the program, breaking argument parsing. Shell escaping is only needed when passing commands through a shell interpreter (e.g., /bin/sh -c). Remove the Escape call here.

Suggested change
args = Escape(args)
// Do not escape arguments; syscall.Exec passes them as-is to the new process.

Copilot uses AI. Check for mistakes.
err := syscall.Exec(args[0], args, os.Environ()) //nolint:gosec
if err != nil {
return fmt.Errorf("could not exec '%v': %v", args[0], err)
}
Expand All @@ -41,3 +53,21 @@ func (r syscallExec) Exec(args []string) error {
func (r syscallExec) String() string {
return "exec"
}

// escapeArg escapes shell metacharacters in a single command-line argument.
func escapeArg(arg string) string {
if strings.ContainsAny(arg, shellMetachars) {
return metacharRegex.ReplaceAllString(arg, `\$1`)
}
return arg
}

// Escape escapes shell metacharacters in a slice of command-line arguments
// and returns a new slice containing the escaped arguments.
func Escape(args []string) []string {
escaped := make([]string, len(args))
for i := range args {
escaped[i] = escapeArg(args[i])
}
return escaped
}
55 changes: 55 additions & 0 deletions internal/oci/runtime_syscall_exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*/

package oci

import (
"reflect"
"testing"
)

func TestEscape(t *testing.T) {
testCases := []struct {
name string
input []string
expected []string
}{
{
name: "Empty Slice",
input: []string{},
expected: []string{},
},
{
name: "Slice with no Metacharacters",
input: []string{"ls", "-l", "/home/user"},
expected: []string{"ls", "-l", "/home/user"},
},
{
name: "Slice with some Metacharacters",
input: []string{"echo", "Hello World", "and", "goodbye | cat"},
expected: []string{"echo", `Hello\ World`, `and`, `goodbye\ \|\ cat`},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := Escape(tc.input)
if !reflect.DeepEqual(actual, tc.expected) {
t.Errorf("Escape(%q) = %q; want %q", tc.input, actual, tc.expected)
}
})
}
}