Skip to content

os: Hostname fails for a 64-character hostname on Linux when /proc is unavailable #81616

Description

@krisiasty

Go version

go version go1.27.1 darwin/amd64

Output of go env in your module/workspace:

AR='ar'
CC='cc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='c++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN='/Users/krisiasty/go/bin'
GOCACHE='/Users/krisiasty/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/krisiasty/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/n_/jgj3h3q52vb61vgpz11b_7400000gn/T/go-build425683523=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/Users/krisiasty/go/pkg/mod'
GONOPROXY='github.com/krisiasty/*'
GONOSUMDB='github.com/krisiasty/*'
GOOS='darwin'
GOPACKAGESDRIVER=''
GOPATH='/Users/krisiasty/go'
GOPRIVATE='github.com/krisiasty/*'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/Cellar/go/1.27.1/libexec'
GOSUMDB='sum.golang.org'
GOTELEMETRY='off'
GOTELEMETRYDIR='/Users/krisiasty/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/Cellar/go/1.27.1/libexec/pkg/tool/darwin_amd64'
GOVCS=''
GOVERSION='go1.27.1'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Go version

go1.27.1

What operating system and processor architecture are you using?

Affects linux (all architectures). Observed by inspection of src/os/sys_linux.go; the
implementation for other GOOS values is unrelated.

What did you do?

Called os.Hostname() on a Linux host whose hostname is exactly 64 characters, in a process
where /proc/sys is not visible for any reason (for example a systemd unit with ProcSubset=pid, or just /proc not being mounted)

# set the hostname to EXACTLY 64 characters, the maximum Linux permits
sudo hostname "$(printf 'a%.0s' $(seq 64))"
# sample code
cat > /tmp/h.go <<'EOF'
package main

import (
	"fmt"
	"os"
)

func main() {
	name, err := os.Hostname()
	fmt.Printf("hostname=%q len=%d err=%v\n", name, len(name), err)
}
EOF

# build binary
go build -o /tmp/h /tmp/h.go

# run it without any restrictions
# prints the hostname — read from /proc/sys/kernel/hostname
/tmp/h




# run under systemd with only /proc/pid available
# fails
sudo systemd-run --pty --property=ProcSubset=pid /tmp/h

What did you see happen?

with full /proc available and 64-character hostname, the result was:

hostname="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" len=64 err=<nil>

with restricted /proc and the same valid 64-byte hostname:

Running as unit: run-p17183-i17183.service; invocation ID: 5da5c8cd8d114c1eb957925ff1b02b5e
Press ^] three times within 1s to disconnect TTY.
hostname="" len=0 err=open /proc/sys/kernel/hostname: no such file or directory

What did you expect to see?

What did you expect to see?

The hostname, obtained from uname(2), which returned it completely and correctly.

What did you see instead?

 open /proc/sys/kernel/hostname: no such file or directory

Analysis

hostname() in src/os/sys_linux.go calls uname(2) first, then decides whether to trust the
result, but the len(name) < 64 condition should be rather len(name) <= 64.

	if err == nil && len(name) > 0 && len(name) < 64 {
		return name, nil
	}

The kernel struct is defined like this in utsname.h:

#define __NEW_UTS_LEN 64

struct new_utsname {
	char sysname[__NEW_UTS_LEN + 1];
	char nodename[__NEW_UTS_LEN + 1];
	char release[__NEW_UTS_LEN + 1];
	char version[__NEW_UTS_LEN + 1];
	char machine[__NEW_UTS_LEN + 1];
	char domainname[__NEW_UTS_LEN + 1];
};

So nodename can hold 64-characters with additional byte (+11) reserved for terminating zero.

The uname syscall result is converted to go sting by this code fragment:

	var un syscall.Utsname
	err = syscall.Uname(&un)

	var buf [512]byte // Enough for a DNS name.
	for i, b := range un.Nodename[:] {
		buf[i] = uint8(b)
		if b == 0 {
			name = string(buf[:i])
			break
		}
	}

name is assigned solely inside the if b == 0 branch, so a Nodename with no NUL in all 65
bytes — the only representation of a truncated name — leaves name empty and is already caught
by len(name) > 0. Conversely, a 64-character hostname is NUL-terminated at index 64, so the
loop extracts all 64 characters and nothing is lost. Simulating the loop over a [65]int8:

Input len(name) Accepted by guard
63 chars, NUL-terminated 63 yes
64 chars, NUL at index 64 64 no
65 bytes, no NUL (truncated) 0 no
empty hostname 0 no

Because the loop breaks at the first NUL and Nodename is 65 bytes, len(name) cannot exceed 64
by construction. The < 64 clause therefore excludes exactly one value, and that value is legal:
__NEW_UTS_LEN is 64 (include/uapi/linux/utsname.h) and sys_sethostname in kernel/sys.c
returns EINVAL for anything longer, so on Linux a 64-character name cannot be a truncation of
something longer.

Now, in case the above condition resolves to false(i.e. syscall failed or the hostname is empty or 64-characters or longer), the function falls backs to reading /proc/sys/kernel/hostname, which essentially returns the value of the same Nodename field from kernel, so the fallback is basically redundant and useless here.

Suggested fix

Dropping the len(name) < 64 clause would be sufficient and appears safe: len(name) is bounded
by 64 by construction, and len(name) > 0 already distinguishes a complete name from a truncated
one.

if err == nil && len(name) > 0 {
	return name, nil
}

Alternatively, if the length check is wanted for platforms where Nodename might be shorter than
the real hostname, len(name) <= 64 would still admit every complete Linux value.

Also, the fallback should removed as unnecessary redundant code, unless any supported linux platform / kernel combination exists where the /proc/sys/kernel/hostname is implemented differently and can return different value than Nodename field from Uname syscall.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions