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.
Go version
go version go1.27.1 darwin/amd64
Output of
go envin your module/workspace: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; theimplementation 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 processwhere
/proc/sysis not visible for any reason (for example a systemd unit withProcSubset=pid, or just /proc not being mounted)What did you see happen?
with full /proc available and 64-character hostname, the result was:
with restricted /proc and the same valid 64-byte hostname:
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?
Analysis
hostname()insrc/os/sys_linux.gocallsuname(2)first, then decides whether to trust theresult, but the
len(name) < 64condition should be ratherlen(name) <= 64.The kernel struct is defined like this in utsname.h:
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:
nameis assigned solely inside theif b == 0branch, so aNodenamewith no NUL in all 65bytes — the only representation of a truncated name — leaves
nameempty and is already caughtby
len(name) > 0. Conversely, a 64-character hostname is NUL-terminated at index 64, so theloop extracts all 64 characters and nothing is lost. Simulating the loop over a
[65]int8:Because the loop breaks at the first NUL and
Nodenameis 65 bytes,len(name)cannot exceed 64by construction. The
< 64clause therefore excludes exactly one value, and that value is legal:__NEW_UTS_LENis 64 (include/uapi/linux/utsname.h) andsys_sethostnameinkernel/sys.creturns
EINVALfor anything longer, so on Linux a 64-character name cannot be a truncation ofsomething 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) < 64clause would be sufficient and appears safe:len(name)is boundedby 64 by construction, and
len(name) > 0already distinguishes a complete name from a truncatedone.
Alternatively, if the length check is wanted for platforms where
Nodenamemight be shorter thanthe real hostname,
len(name) <= 64would 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.