Repository: elastic/go-sysinfo
Pull request #200 (2024-02-05 08:31:39)

adds HostInfo.NativeArchitecture
Adds `NativeArchitecture` to `HostInfo` to empower applications to detect if they are run in emulation.

Modern desktop operating systems ported to the ARM architecture include capabilities to run x86 applications via binary translation to ease the technological transition. This is usually fine, except for applications that rely on a specific native platform, such as for installing native drivers.

# Testing

Tested in the following configurations:

### ARM64 CPU

#### macOS

- native binary
```
GOARCH arm64
Architecture arm64
NativeArchitecture arm64
```

- AMD64 binary
```
GOARCH amd64
Architecture x64_64
NativeArchitecture arm64
```

#### Linux

- native binary
```
GOARCH arm64
Architecture aarch64
NativeArchitecture aarch64
```

- AMD64 binary
```
GOARCH amd64
Architecture x64_64
NativeArchitecture aarch64
```

#### Windows

- native binary

N/A as Go doesn't support GOOS=Windows GOARCH=arm64

- AMD64 binary
```
GOARCH amd64
Architecture x64_64
NativeArchitecture arm64
```

### AMD64 CPU

#### macOS

- native binary
```
GOARCH amd64
Architecture x64_64
NativeArchitecture x64_64
```

- ARM64 binary

N/A as Rosetta 2 is not supported on Intel platform

#### Linux

- native binary
```
GOARCH amd64
Architecture x64_64
NativeArchitecture x64_64
```

- ARM64 binary
```
GOARCH amd64
Architecture aarch64
NativeArchitecture x64_64
```

#### Windows

- native binary
```
GOARCH amd64
Architecture x64_64
NativeArchitecture x64_64
```

- ARM64 binary

N/A as Go doesn't support GOOS=Windows GOARCH=arm64


intxgo: > Code looks fine, but I'm personally curious if we have a way to test the behavior of this under linux? On Darwin we have Rosetta, but is there some similar linux utility we're hoping to support?

Linux [multiarch](https://wiki.debian.org/Multiarch/HOWTO), that's how we run x86-64 tools in linux on Apple macbooks

Patch:
diff --git a/.changelog/200.txt b/.changelog/200.txt
new file mode 100644
index 0000000..51afa52
--- /dev/null
+++ b/.changelog/200.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+Adds NativeArchitecture to HostInfo to allow applications to detect whether they are running in emulation.
+```
\ No newline at end of file
diff --git a/providers/darwin/arch_darwin.go b/providers/darwin/arch_darwin.go
index 8b3ed91..92251c3 100644
--- a/providers/darwin/arch_darwin.go
+++ b/providers/darwin/arch_darwin.go
@@ -21,11 +21,17 @@ package darwin
 
 import (
 	"fmt"
+	"os"
 
 	"golang.org/x/sys/unix"
 )
 
-const hardwareMIB = "hw.machine"
+const (
+	hardwareMIB    = "hw.machine"
+	procTranslated = "sysctl.proc_translated"
+	archIntel      = "x86_64"
+	archApple      = "arm64"
+)
 
 func Architecture() (string, error) {
 	arch, err := unix.Sysctl(hardwareMIB)
@@ -35,3 +41,33 @@ func Architecture() (string, error) {
 
 	return arch, nil
 }
+
+func NativeArchitecture() (string, error) {
+	processArch, err := Architecture()
+	if err != nil {
+		return "", err
+	}
+
+	// https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment
+
+	translated, err := unix.SysctlUint32(procTranslated)
+	if err != nil {
+		// macos without Rosetta installed doesn't have sysctl.proc_translated
+		if os.IsNotExist(err) {
+			return processArch, nil
+		}
+		return "", fmt.Errorf("failed to read sysctl.proc_translated: %w", err)
+	}
+
+	var nativeArch string
+
+	switch translated {
+	case 0:
+		nativeArch = processArch
+	case 1:
+		// Rosetta 2 is supported only on Apple silicon
+		nativeArch = archApple
+	}
+
+	return nativeArch, nil
+}
diff --git a/providers/darwin/host_darwin.go b/providers/darwin/host_darwin.go
index 70862e8..4a6adc1 100644
--- a/providers/darwin/host_darwin.go
+++ b/providers/darwin/host_darwin.go
@@ -167,6 +167,7 @@ func newHost() (*host, error) {
 	h := &host{}
 	r := &reader{}
 	r.architecture(h)
+	r.nativeArchitecture(h)
 	r.bootTime(h)
 	r.hostname(h)
 	r.network(h)
@@ -206,6 +207,14 @@ func (r *reader) architecture(h *host) {
 	h.info.Architecture = v
 }
 
+func (r *reader) nativeArchitecture(h *host) {
+	v, err := NativeArchitecture()
+	if r.addErr(err) {
+		return
+	}
+	h.info.NativeArchitecture = v
+}
+
 func (r *reader) bootTime(h *host) {
 	v, err := BootTime()
 	if r.addErr(err) {
diff --git a/providers/linux/arch_linux.go b/providers/linux/arch_linux.go
index e1d2893..f1d807f 100644
--- a/providers/linux/arch_linux.go
+++ b/providers/linux/arch_linux.go
@@ -19,9 +19,19 @@ package linux
 
 import (
 	"fmt"
+	"os"
+	"strings"
 	"syscall"
 )
 
+const (
+	procSysKernelArch = "/proc/sys/kernel/arch"
+	procVersion       = "/proc/version"
+	archAmd64         = "amd64"
+	archArm64         = "arm64"
+	archAarch64       = "aarch64"
+)
+
 func Architecture() (string, error) {
 	var uname syscall.Utsname
 	if err := syscall.Uname(&uname); err != nil {
@@ -38,3 +48,38 @@ func Architecture() (string, error) {
 
 	return string(data), nil
 }
+
+func NativeArchitecture() (string, error) {
+	// /proc/sys/kernel/arch was introduced in Kernel 6.1
+	// https://www.kernel.org/doc/html/v6.1/admin-guide/sysctl/kernel.html#arch
+	// It's the same as uname -m, except that for a process running in emulation
+	// machine returned from syscall reflects the emulated machine, whilst /proc
+	// filesystem is read as file so its value is not emulated
+	data, err := os.ReadFile(procSysKernelArch)
+	if err != nil {
+		if os.IsNotExist(err) {
+			// fallback to checking version string for older kernels
+			version, err := os.ReadFile(procVersion)
+			if err != nil {
+				return "", nil
+			}
+
+			versionStr := string(version)
+			if strings.Contains(versionStr, archAmd64) {
+				return archAmd64, nil
+			} else if strings.Contains(versionStr, archArm64) {
+				// for parity with Architecture() and /proc/sys/kernel/arch
+				// as aarch64 and arm64 are used interchangeably
+				return archAarch64, nil
+			}
+			return "", nil
+		}
+
+		return "", fmt.Errorf("failed to read kernel arch: %w", err)
+	}
+
+	nativeArch := string(data)
+	nativeArch = strings.TrimRight(nativeArch, "\n")
+
+	return string(data), nil
+}
diff --git a/providers/linux/host_linux.go b/providers/linux/host_linux.go
index c2bdeb4..2e0d604 100644
--- a/providers/linux/host_linux.go
+++ b/providers/linux/host_linux.go
@@ -156,6 +156,7 @@ func newHost(fs procFS) (*host, error) {
 	h := &host{stat: stat, procFS: fs}
 	r := &reader{}
 	r.architecture(h)
+	r.nativeArchitecture(h)
 	r.bootTime(h)
 	r.containerized(h)
 	r.hostname(h)
@@ -197,6 +198,14 @@ func (r *reader) architecture(h *host) {
 	h.info.Architecture = v
 }
 
+func (r *reader) nativeArchitecture(h *host) {
+	v, err := NativeArchitecture()
+	if r.addErr(err) {
+		return
+	}
+	h.info.NativeArchitecture = v
+}
+
 func (r *reader) bootTime(h *host) {
 	v, err := bootTime(h.procFS.FS)
 	if r.addErr(err) {
diff --git a/providers/windows/arch_windows.go b/providers/windows/arch_windows.go
index 0edfc4d..fc4b9a9 100644
--- a/providers/windows/arch_windows.go
+++ b/providers/windows/arch_windows.go
@@ -18,14 +18,46 @@
 package windows
 
 import (
-	windows "github.com/elastic/go-windows"
+	"golang.org/x/sys/windows"
+
+	gowindows "github.com/elastic/go-windows"
+)
+
+const (
+	imageFileMachineAmd64 = 0x8664
+	imageFileMachineArm64 = 0xAA64
+	archIntel             = "x86_64"
+	archArm64             = "arm64"
 )
 
 func Architecture() (string, error) {
-	systemInfo, err := windows.GetNativeSystemInfo()
+	systemInfo, err := gowindows.GetNativeSystemInfo()
 	if err != nil {
 		return "", err
 	}
 
 	return systemInfo.ProcessorArchitecture.String(), nil
 }
+
+func NativeArchitecture() (string, error) {
+	var processMachine, nativeMachine uint16
+	// the pseudo handle doesn't need to be closed
+	var currentProcessHandle = windows.CurrentProcess()
+
+	err := windows.IsWow64Process2(currentProcessHandle, &processMachine, &nativeMachine)
+	if err != nil {
+		return "", err
+	}
+
+	var nativeArch string
+
+	switch nativeMachine {
+	case imageFileMachineAmd64:
+		// for parity with Architecture() as amd64 and x86_64 are used interchangeably
+		nativeArch = archIntel
+	case imageFileMachineArm64:
+		nativeArch = archArm64
+	}
+
+	return nativeArch, nil
+}
diff --git a/providers/windows/host_windows.go b/providers/windows/host_windows.go
index f7b527b..e476778 100644
--- a/providers/windows/host_windows.go
+++ b/providers/windows/host_windows.go
@@ -102,6 +102,7 @@ func newHost() (*host, error) {
 	h := &host{}
 	r := &reader{}
 	r.architecture(h)
+	r.nativeArchitecture(h)
 	r.bootTime(h)
 	r.hostname(h)
 	r.network(h)
@@ -141,6 +142,14 @@ func (r *reader) architecture(h *host) {
 	h.info.Architecture = v
 }
 
+func (r *reader) nativeArchitecture(h *host) {
+	v, err := NativeArchitecture()
+	if r.addErr(err) {
+		return
+	}
+	h.info.NativeArchitecture = v
+}
+
 func (r *reader) bootTime(h *host) {
 	v, err := BootTime()
 	if r.addErr(err) {
diff --git a/types/host.go b/types/host.go
index 27ea488..9f3bc3b 100644
--- a/types/host.go
+++ b/types/host.go
@@ -73,17 +73,18 @@ type VMStat interface {
 
 // HostInfo contains basic host information.
 type HostInfo struct {
-	Architecture      string    `json:"architecture"`            // Hardware architecture (e.g. x86_64, arm, ppc, mips).
-	BootTime          time.Time `json:"boot_time"`               // Host boot time.
-	Containerized     *bool     `json:"containerized,omitempty"` // Is the process containerized.
-	Hostname          string    `json:"name"`                    // Hostname, lowercased.
-	IPs               []string  `json:"ip,omitempty"`            // List of all IPs.
-	KernelVersion     string    `json:"kernel_version"`          // Kernel version.
-	MACs              []string  `json:"mac"`                     // List of MAC addresses.
-	OS                *OSInfo   `json:"os"`                      // OS information.
-	Timezone          string    `json:"timezone"`                // System timezone.
-	TimezoneOffsetSec int       `json:"timezone_offset_sec"`     // Timezone offset (seconds from UTC).
-	UniqueID          string    `json:"id,omitempty"`            // Unique ID of the host (optional).
+	Architecture       string    `json:"architecture"`            // Process hardware architecture (e.g. x86_64, arm, ppc, mips).
+	NativeArchitecture string    `json:"native_architecture"`     // Native OS hardware architecture (e.g. x86_64, arm, ppc, mips).
+	BootTime           time.Time `json:"boot_time"`               // Host boot time.
+	Containerized      *bool     `json:"containerized,omitempty"` // Is the process containerized.
+	Hostname           string    `json:"name"`                    // Hostname, lowercased.
+	IPs                []string  `json:"ip,omitempty"`            // List of all IPs.
+	KernelVersion      string    `json:"kernel_version"`          // Kernel version.
+	MACs               []string  `json:"mac"`                     // List of MAC addresses.
+	OS                 *OSInfo   `json:"os"`                      // OS information.
+	Timezone           string    `json:"timezone"`                // System timezone.
+	TimezoneOffsetSec  int       `json:"timezone_offset_sec"`     // Timezone offset (seconds from UTC).
+	UniqueID           string    `json:"id,omitempty"`            // Unique ID of the host (optional).
 }
 
 // Uptime returns the system uptime


Test patch:
diff --git a/providers/darwin/arch_darwin_test.go b/providers/darwin/arch_darwin_test.go
index 01a25a5..340f7e8 100644
--- a/providers/darwin/arch_darwin_test.go
+++ b/providers/darwin/arch_darwin_test.go
@@ -28,3 +28,9 @@ func TestArchitecture(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotEmpty(t, a)
 }
+
+func TestNativeArchitecture(t *testing.T) {
+	a, err := NativeArchitecture()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, a)
+}
diff --git a/providers/linux/arch_linux_test.go b/providers/linux/arch_linux_test.go
new file mode 100644
index 0000000..be46203
--- /dev/null
+++ b/providers/linux/arch_linux_test.go
@@ -0,0 +1,36 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you 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 linux
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestArchitecture(t *testing.T) {
+	a, err := Architecture()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, a)
+}
+
+func TestNativeArchitecture(t *testing.T) {
+	a, err := NativeArchitecture()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, a)
+}
diff --git a/providers/windows/arch_windows_test.go b/providers/windows/arch_windows_test.go
new file mode 100644
index 0000000..1e60ac9
--- /dev/null
+++ b/providers/windows/arch_windows_test.go
@@ -0,0 +1,36 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you 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 windows
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestArchitecture(t *testing.T) {
+	a, err := Architecture()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, a)
+}
+
+func TestNativeArchitecture(t *testing.T) {
+	a, err := NativeArchitecture()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, a)
+}