Go (often called Golang) is popular because it makes building fast and reliable software easy. But sometimes, even the best tools have tricky or dangerous edges. CVE-2023-29403 is one such vulnerability in the Go programming language. If you’re working with Unix systems—Linux, macOS, *BSD—this could matter a lot.

Let’s break down what happened, how it works, why it’s dangerous, and what you should do if you’ve got Go binaries with setuid or setgid in the wild.

What’s the Problem? (Executive Summary)

On Unix-like systems, Go programs do not change their behavior when run with the setuid (or setgid) bits set. This means the Go runtime doesn’t take any special precautions when it runs privileged as another user—especially root!

That’s bad. Here’s why

- If stdin, stdout, or stderr are closed when a privileged Go binary runs, it might accidentally open a new file using one of those standard I/O file descriptors, letting normal users hijack content—read or write files with root access.
- If the Go binary panics or crashes (e.g., from a bad signal), sensitive memory or register data might leak. An attacker can turn that into real compromise.

A Simple Example: How Can It Go Wrong?

Let’s look at a code snippet showing what might happen.

Suppose you have a Go utility meant to be run as root (using setuid), and someone closes stdin, stdout, and stderr before running it:

package main

import (
    "os"
    "fmt"
)

func main() {
    // Assume stdin/stdout/stderr are closed!

    f, err := os.Open("/tmp/somefile")
    if err != nil {
        fmt.Printf("Failed: %v\n", err)
        return
    }

    // Write something (accidentally to stdout, which is now referring to /tmp/somefile!)
    fmt.Println("Hello, world!") // This line writes to /tmp/somefile instead of the actual terminal!
    f.Close()
}

Someone closes file descriptors  (stdin), 1 (stdout), 2 (stderr).

2. Your Go program opens /tmp/somefile. The first unused file descriptor is , so /tmp/somefile gets assigned to stdin.
3. Later, the program writes with fmt.Println. But since stdout (fd 1) points to something _unintended_, it writes to an attacker-controlled file—perhaps allowing privilege escalation or leaking secrets.

Attackers can use this behavior for privilege escalation

1. Prepare an attack script that closes stdin, stdout, stderr, and launches the vulnerable Go setuid binary.
2. As the binary opens new files, the attacker can predict or control which files end up as fd , 1, or 2.
3. Now, anything your Go binary reads or writes on these streams leaks info or allows arbitrary modification with elevated privileges.

This is particularly nasty if the binary reads secrets from a privileged file or dumps sensitive data on panic.

Real-World Exploit Flow

# Create a malicious file that will be "hijacked"
touch /tmp/evilfile
chmod 666 /tmp/evilfile

# Close stdin, stdout, stderr and run the setuid Go binary
exec <&- 1<&- 2<&-
/path/to/vulnerable-setuid-go-binary

Now, any attempt inside the Go binary to os.Open() files (especially before actively reopening stdin/stdout/stderr safely) will result in misdirecting privileged actions to the attacker’s control.

- Official Go Security Advisory for CVE-2023-29403
- GoLang Release Notes Discussing CVE-2023-29403
- NIST CVE Entry

Why Did This Happen in Go?

Go’s promise of portability hides complexity: unlike C or POSIX-compliant runtimes, Go’s runtime doesn’t emulate setuid/setgid "sanitization" (which is where other languages or shells might handle these edge cases by clearing sensitive data, re-opening stdio, dropping privileges, etc).

Other languages often include special code for when a binary is run setuid/setgid—such as reopening stdin/stdout/stderr to /dev/null, or even refusing to run entirely. Go…just runs your code. That’s the danger.

Panic and Data Leaks

Another rarely considered angle: register or memory dumps on panic.

When a setuid Go binary panics or is terminated by a signal (like SIGSEGV), Go’s runtime may dump the current stack or registers to stderr—again, if stderr is compromised, this data can leak to unintended recipients, revealing secrets or state.

How Do I Fix This?

Do not use setuid/setgid Go binaries unless you absolutely understand what you’re doing! Most Go experts advise against ever using setuid root for Go binaries.

Immediately sanitize file descriptors:

- Open /dev/null as , 1, 2 if they are closed.
- Drop elevated privileges (setuid/setgid) as soon as possible in the program flow.
- Use dedicated wrappers (in C or shell) to sanitize the process environment before starting your Go binary.
- Update Go: Apply patches—this was fixed in Go 1.19.9 and Go 1.20.4. Always use the latest stable Go.

Example: Reopening Standard File Descriptors

for i := ; i <= 2; i++ {
    if _, err := os.Stat(fmt.Sprintf("/dev/fd/%d", i)); os.IsNotExist(err) {
        f, _ := os.OpenFile("/dev/null", os.O_RDWR, )
        syscall.Dup2(int(f.Fd()), i)
        f.Close()
    }
}


*(Note: proper error checks and imports needed, this is for illustration!)*

Takeaway

If your Go program is ever run with setuid/setgid (especially as root), don’t assume the runtime will protect you. Always control your standard file descriptors, and _never_ dump secrets anywhere that might be hijacked by a user.

TL;DR

- CVE-2023-29403 lets attackers hijack stdin/stdout/stderr in privileged Go binaries.
- Writing or reading from these descriptors can read/write privileged files with root access.

More Reading

- “Secure Coding of Setuid Binaries” - Linux Man Page
- Go Blog: “Catching Common Bugs with Go”
- CVE-2023-29403 report on GitHub Security Advisory

Timeline

Published on: 06/08/2023 21:15:00 UTC
Last modified on: 07/21/2023 04:15:00 UTC