Repository: prometheus/procfs (go)

Issue:
/proc/cpuinfo parsing crashes on s390x
The current logic for parsing /proc/cpuinfo is written for x86 platforms, but doesn't really handle other formats currently.  The worst case is probably s390x which causes an array index out of bounds panic.

```
goroutine 39 [running]:
github.com/prometheus/procfs.parseCPUInfo(0xc000203000, 0x4c3, 0xe00, 0x4c3, 0xe00, 0x0, 0x0, 0x1)
	/go/src/github.com/prometheus/node_exporter/vendor/github.com/prometheus/procfs/cpuinfo.go:85 +0x1d32
github.com/prometheus/procfs.FS.CPUInfo(0x3ffcbd7f550, 0xa, 0x24000000000f4684, 0x1d394, 0xc0001eb6c0, 0x70, 0x70)
	/go/src/github.com/prometheus/node_exporter/vendor/github.com/prometheus/procfs/cpuinfo.go:61 +0x180
github.com/prometheus/node_exporter/collector.(*cpuCollector).updateInfo(0xc00005d180, 0xc0000de180, 0x32ea661f00000000, 0x325e45f98388)
	/go/src/github.com/prometheus/node_exporter/collector/cpu_linux.go:96 +0x3c
github.com/prometheus/node_exporter/collector.(*cpuCollector).Update(0xc00005d180, 0xc0000de180, 0xc311a0, 0x0)
	/go/src/github.com/prometheus/node_exporter/collector/cpu_linux.go:81 +0xdc
github.com/prometheus/node_exporter/collector.execute(0x6e60b2, 0x3, 0x7cb140, 0xc00005d180, 0xc0000de180)
	/go/src/github.com/prometheus/node_exporter/collector/collector.go:127 +0x68
github.com/prometheus/node_exporter/collector.NodeCollector.Collect.func1(0xc0000de180, 0xc0000d82f0, 0x6e60b2, 0x3, 0x7cb140, 0xc00005d180)
	/go/src/github.com/prometheus/node_exporter/collector/collector.go:118 +0x48
created by github.com/prometheus/node_exporter/collector.NodeCollector.Collect
	/go/src/github.com/prometheus/node_exporter/collector/collector.go:117 +0xe2
```

This is due to format of `/proc/cpuinfo` being significantly different from the x86 version.  Here's an example which causes the panic:
```
# cat /proc/cpuinfo
vendor_id       : IBM/S390
# processors    : 2
bogomips per cpu: 11061.00
features	: esan3 zarch stfle msa ldisp eimm dfp etf3eh highgprs 
processor 0: version = FF,  identification = 32C5C2,  machine = 2097
processor 1: version = FF,  identification = 32C5C2,  machine = 2097
```

Pull request description:
multiplatform support for cpuinfo
This adds basic handling of reading /proc/cpuinfo for ARM, PPC, and
s390x.
Fixes #256 

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index ca2082c..f9c167b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -34,7 +34,7 @@ jobs:
     steps:
     - checkout
     - run: sudo pip install codespell
-    - run: codespell --skip=".git,./vendor,ttar,go.mod,go.sum" -L uint,packages\',ded,alo,als
+    - run: codespell --skip=".git,./vendor,ttar,go.mod,go.sum" -L uint,packages\',ded,alo,als,te
 
 workflows:
   version: 2
diff --git a/cpuinfo.go b/cpuinfo.go
index 2e02215..bb3be99 100644
--- a/cpuinfo.go
+++ b/cpuinfo.go
@@ -11,11 +11,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// +build linux
+
 package procfs
 
 import (
 	"bufio"
 	"bytes"
+	"errors"
+	"regexp"
 	"strconv"
 	"strings"
 
@@ -52,6 +56,11 @@ type CPUInfo struct {
 	PowerManagement string
 }
 
+var (
+	cpuinfoClockRegexp          = regexp.MustCompile(`([\d.]+)`)
+	cpuinfoS390XProcessorRegexp = regexp.MustCompile(`^processor\s+(\d+):.*`)
+)
+
 // CPUInfo returns information about current system CPUs.
 // See https://www.kernel.org/doc/Documentation/filesystems/proc.txt
 func (fs FS) CPUInfo() ([]CPUInfo, error) {
@@ -62,14 +71,26 @@ func (fs FS) CPUInfo() ([]CPUInfo, error) {
 	return parseCPUInfo(data)
 }
 
-// parseCPUInfo parses data from /proc/cpuinfo
-func parseCPUInfo(info []byte) ([]CPUInfo, error) {
-	cpuinfo := []CPUInfo{}
-	i := -1
+func parseCPUInfoX86(info []byte) ([]CPUInfo, error) {
 	scanner := bufio.NewScanner(bytes.NewReader(info))
+
+	// find the first "processor" line
+	firstLine := firstNonEmptyLine(scanner)
+	if !strings.HasPrefix(firstLine, "processor") || !strings.Contains(firstLine, ":") {
+		return nil, errors.New("invalid cpuinfo file: " + firstLine)
+	}
+	field := strings.SplitN(firstLine, ": ", 2)
+	v, err := strconv.ParseUint(field[1], 0, 32)
+	if err != nil {
+		return nil, err
+	}
+	firstcpu := CPUInfo{Processor: uint(v)}
+	cpuinfo := []CPUInfo{firstcpu}
+	i := 0
+
 	for scanner.Scan() {
 		line := scanner.Text()
-		if strings.TrimSpace(line) == "" {
+		if !strings.Contains(line, ":") {
 			continue
 		}
 		field := strings.SplitN(line, ": ", 2)
@@ -82,7 +103,7 @@ func parseCPUInfo(info []byte) ([]CPUInfo, error) {
 				return nil, err
 			}
 			cpuinfo[i].Processor = uint(v)
-		case "vendor_id":
+		case "vendor", "vendor_id":
 			cpuinfo[i].VendorID = field[1]
 		case "cpu family":
 			cpuinfo[i].CPUFamily = field[1]
@@ -163,5 +184,175 @@ func parseCPUInfo(info []byte) ([]CPUInfo, error) {
 		}
 	}
 	return cpuinfo, nil
+}
+
+func parseCPUInfoARM(info []byte) ([]CPUInfo, error) {
+	scanner := bufio.NewScanner(bytes.NewReader(info))
+
+	firstLine := firstNonEmptyLine(scanner)
+	if !strings.HasPrefix(firstLine, "Processor") || !strings.Contains(firstLine, ":") {
+		return nil, errors.New("invalid cpuinfo file: " + firstLine)
+	}
+	field := strings.SplitN(firstLine, ": ", 2)
+	commonCPUInfo := CPUInfo{VendorID: field[1]}
+
+	cpuinfo := []CPUInfo{}
+	i := -1
+	featuresLine := ""
+
+	for scanner.Scan() {
+		line := scanner.Text()
+		if !strings.Contains(line, ":") {
+			continue
+		}
+		field := strings.SplitN(line, ": ", 2)
+		switch strings.TrimSpace(field[0]) {
+		case "processor":
+			cpuinfo = append(cpuinfo, commonCPUInfo) // start of the next processor
+			i++
+			v, err := strconv.ParseUint(field[1], 0, 32)
+			if err != nil {
+				return nil, err
+			}
+			cpuinfo[i].Processor = uint(v)
+		case "BogoMIPS":
+			v, err := strconv.ParseFloat(field[1], 64)
+			if err != nil {
+				return nil, err
+			}
+			cpuinfo[i].BogoMips = v
+		case "Features":
+			featuresLine = line
+		}
+	}
+	fields := strings.SplitN(featuresLine, ": ", 2)
+	for i := range cpuinfo {
+		cpuinfo[i].Flags = strings.Fields(fields[1])
+	}
+	return cpuinfo, nil
+}
+
+func parseCPUInfoS390X(info []byte) ([]CPUInfo, error) {
+	scanner := bufio.NewScanner(bytes.NewReader(info))
 
+	firstLine := firstNonEmptyLine(scanner)
+	if !strings.HasPrefix(firstLine, "vendor_id") || !strings.Contains(firstLine, ":") {
+		return nil, errors.New("invalid cpuinfo file: " + firstLine)
+	}
+	field := strings.SplitN(firstLine, ": ", 2)
+	cpuinfo := []CPUInfo{}
+	commonCPUInfo := CPUInfo{VendorID: field[1]}
+
+	for scanner.Scan() {
+		line := scanner.Text()
+		if !strings.Contains(line, ":") {
+			continue
+		}
+		field := strings.SplitN(line, ": ", 2)
+		switch strings.TrimSpace(field[0]) {
+		case "bogomips per cpu":
+			v, err := strconv.ParseFloat(field[1], 64)
+			if err != nil {
+				return nil, err
+			}
+			commonCPUInfo.BogoMips = v
+		case "features":
+			commonCPUInfo.Flags = strings.Fields(field[1])
+		}
+		if strings.HasPrefix(line, "processor") {
+			match := cpuinfoS390XProcessorRegexp.FindStringSubmatch(line)
+			if len(match) < 2 {
+				return nil, errors.New("Invalid line found in cpuinfo: " + line)
+			}
+			cpu := commonCPUInfo
+			v, err := strconv.ParseUint(match[1], 0, 32)
+			if err != nil {
+				return nil, err
+			}
+			cpu.Processor = uint(v)
+			cpuinfo = append(cpuinfo, cpu)
+		}
+		if strings.HasPrefix(line, "cpu number") {
+			break
+		}
+	}
+
+	i := 0
+	for scanner.Scan() {
+		line := scanner.Text()
+		if !strings.Contains(line, ":") {
+			continue
+		}
+		field := strings.SplitN(line, ": ", 2)
+		switch strings.TrimSpace(field[0]) {
+		case "cpu number":
+			i++
+		case "cpu MHz dynamic":
+			clock := cpuinfoClockRegexp.FindString(strings.TrimSpace(field[1]))
+			v, err := strconv.ParseFloat(clock, 64)
+			if err != nil {
+				return nil, err
+			}
+			cpuinfo[i].CPUMHz = v
+		}
+	}
+
+	return cpuinfo, nil
+}
+
+func parseCPUInfoPPC(info []byte) ([]CPUInfo, error) {
+	scanner := bufio.NewScanner(bytes.NewReader(info))
+
+	firstLine := firstNonEmptyLine(scanner)
+	if !strings.HasPrefix(firstLine, "processor") || !strings.Contains(firstLine, ":") {
+		return nil, errors.New("invalid cpuinfo file: " + firstLine)
+	}
+	field := strings.SplitN(firstLine, ": ", 2)
+	v, err := strconv.ParseUint(field[1], 0, 32)
+	if err != nil {
+		return nil, err
+	}
+	firstcpu := CPUInfo{Processor: uint(v)}
+	cpuinfo := []CPUInfo{firstcpu}
+	i := 0
+
+	for scanner.Scan() {
+		line := scanner.Text()
+		if !strings.Contains(line, ":") {
+			continue
+		}
+		field := strings.SplitN(line, ": ", 2)
+		switch strings.TrimSpace(field[0]) {
+		case "processor":
+			cpuinfo = append(cpuinfo, CPUInfo{}) // start of the next processor
+			i++
+			v, err := strconv.ParseUint(field[1], 0, 32)
+			if err != nil {
+				return nil, err
+			}
+			cpuinfo[i].Processor = uint(v)
+		case "cpu":
+			cpuinfo[i].VendorID = field[1]
+		case "clock":
+			clock := cpuinfoClockRegexp.FindString(strings.TrimSpace(field[1]))
+			v, err := strconv.ParseFloat(clock, 64)
+			if err != nil {
+				return nil, err
+			}
+			cpuinfo[i].CPUMHz = v
+		}
+	}
+	return cpuinfo, nil
+}
+
+// firstNonEmptyLine advances the scanner to the first non-empty line
+// and returns the contents of that line
+func firstNonEmptyLine(scanner *bufio.Scanner) string {
+	for scanner.Scan() {
+		line := scanner.Text()
+		if strings.TrimSpace(line) != "" {
+			return line
+		}
+	}
+	return ""
 }
diff --git a/cpuinfo_amd64.go b/cpuinfo_amd64.go
new file mode 100644
index 0000000..42e995e
--- /dev/null
+++ b/cpuinfo_amd64.go
@@ -0,0 +1,19 @@
+// Copyright 2020 The Prometheus Authors
+// 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.
+
+// +build linux
+// +build amd64
+
+package procfs
+
+var parseCPUInfo = parseCPUInfoX86
diff --git a/cpuinfo_arm64.go b/cpuinfo_arm64.go
new file mode 100644
index 0000000..4f5d172
--- /dev/null
+++ b/cpuinfo_arm64.go
@@ -0,0 +1,19 @@
+// Copyright 2020 The Prometheus Authors
+// 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.
+
+// +build linux
+// +build arm64
+
+package procfs
+
+var parseCPUInfo = parseCPUInfoARM
diff --git a/cpuinfo_ppc64.go b/cpuinfo_ppc64.go
new file mode 100644
index 0000000..3184151
--- /dev/null
+++ b/cpuinfo_ppc64.go
@@ -0,0 +1,19 @@
+// Copyright 2020 The Prometheus Authors
+// 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.
+
+// +build linux
+// +build ppc64,ppc64le
+
+package procfs
+
+var parseCPUInfo = parseCPUInfoPPC
diff --git a/cpuinfo_s390x.go b/cpuinfo_s390x.go
new file mode 100644
index 0000000..1d7019d
--- /dev/null
+++ b/cpuinfo_s390x.go
@@ -0,0 +1,19 @@
+// Copyright 2020 The Prometheus Authors
+// 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.
+
+// +build linux
+// +build s390x
+
+package procfs
+
+var parseCPUInfo = parseCPUInfoS390x


Test patch:
diff --git a/cpuinfo_test.go b/cpuinfo_test.go
index a5687f9..b6869ac 100644
--- a/cpuinfo_test.go
+++ b/cpuinfo_test.go
@@ -11,11 +11,106 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// +build linux
+
 package procfs
 
 import "testing"
 
-func TestCPUInfo(t *testing.T) {
+const (
+	cpuinfoArm7 = `
+Processor : ARMv7 Processor rev 5 (v7l)
+processor : 0
+BogoMIPS : 2400.00
+
+processor : 1
+BogoMIPS : 2400.00
+
+Features : swp half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4 idiva idivt
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xc07
+CPU revision : 5
+
+Hardware : sun8i
+Revision : 0000
+Serial : 5400503583203c3c040e`
+
+	cpuinfoS390x = `
+vendor_id       : IBM/S390
+# processors    : 4
+bogomips per cpu: 3033.00
+max thread id   : 0
+features	: esan3 zarch stfle msa ldisp eimm dfp edat etf3eh highgprs te vx sie
+facilities      : 0 1 2 3 4 6 7 8 9 10 12 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 30 31 32 33 34 35 36 37 40 41 42 43 44 45 46 47 48 49 50 51 52 53 55 57 73 74 75 76 77 80 81 82 128 129 131
+cache0          : level=1 type=Data scope=Private size=128K line_size=256 associativity=8
+cache1          : level=1 type=Instruction scope=Private size=96K line_size=256 associativity=6
+cache2          : level=2 type=Data scope=Private size=2048K line_size=256 associativity=8
+cache3          : level=2 type=Instruction scope=Private size=2048K line_size=256 associativity=8
+cache4          : level=3 type=Unified scope=Shared size=65536K line_size=256 associativity=16
+cache5          : level=4 type=Unified scope=Shared size=491520K line_size=256 associativity=30
+processor 0: version = FF,  identification = 2733E8,  machine = 2964
+processor 1: version = FF,  identification = 2733E8,  machine = 2964
+processor 2: version = FF,  identification = 2733E8,  machine = 2964
+processor 3: version = FF,  identification = 2733E8,  machine = 2964
+
+cpu number      : 0
+cpu MHz dynamic : 5000
+cpu MHz static  : 5000
+
+cpu number      : 1
+cpu MHz dynamic : 5000
+cpu MHz static  : 5000
+
+cpu number      : 2
+cpu MHz dynamic : 5000
+cpu MHz static  : 5000
+
+cpu number      : 3
+cpu MHz dynamic : 5000
+cpu MHz static  : 5000
+`
+
+	cpuinfoPpc64 = `
+processor	: 0
+cpu		: POWER7 (architected), altivec supported
+clock		: 3000.000000MHz
+revision	: 2.1 (pvr 003f 0201)
+
+processor	: 1
+cpu		: POWER7 (architected), altivec supported
+clock		: 3000.000000MHz
+revision	: 2.1 (pvr 003f 0201)
+
+processor	: 2
+cpu		: POWER7 (architected), altivec supported
+clock		: 3000.000000MHz
+revision	: 2.1 (pvr 003f 0201)
+
+processor	: 3
+cpu		: POWER7 (architected), altivec supported
+clock		: 3000.000000MHz
+revision	: 2.1 (pvr 003f 0201)
+
+processor	: 4
+cpu		: POWER7 (architected), altivec supported
+clock		: 3000.000000MHz
+revision	: 2.1 (pvr 003f 0201)
+
+processor	: 5
+cpu		: POWER7 (architected), altivec supported
+clock		: 3000.000000MHz
+revision	: 2.1 (pvr 003f 0201)
+
+timebase	: 512000000
+platform	: pSeries
+model		: IBM,8233-E8B
+machine		: CHRP IBM,8233-E8B
+`
+)
+
+func TestCPUInfoX86(t *testing.T) {
 	cpuinfo, err := getProcFixtures(t).CPUInfo()
 	if err != nil {
 		t.Fatal(err)
@@ -56,5 +151,52 @@ func TestCPUInfo(t *testing.T) {
 	if want, have := "vme", cpuinfo[7].Flags[1]; want != have {
 		t.Errorf("want flag %v, have %v", want, have)
 	}
+}
 
+func TestCPUInfoParseARM(t *testing.T) {
+	cpuinfo, err := parseCPUInfoARM([]byte(cpuinfoArm7))
+	if err != nil || cpuinfo == nil {
+		t.Fatalf("unable to parse arm cpu info: %v", err)
+	}
+	if want, have := 2, len(cpuinfo); want != have {
+		t.Errorf("want number of processors %v, have %v", want, have)
+	}
+	if want, have := "ARMv7 Processor rev 5 (v7l)", cpuinfo[0].VendorID; want != have {
+		t.Errorf("want vendor %v, have %v", want, have)
+	}
+	if want, have := "thumb", cpuinfo[1].Flags[2]; want != have {
+		t.Errorf("want flag %v, have %v", want, have)
+	}
+}
+
+func TestCPUInfoParseS390X(t *testing.T) {
+	cpuinfo, err := parseCPUInfoS390X([]byte(cpuinfoS390x))
+	if err != nil || cpuinfo == nil {
+		t.Fatalf("unable to parse s390x cpu info: %v", err)
+	}
+	if want, have := 4, len(cpuinfo); want != have {
+		t.Errorf("want number of processors %v, have %v", want, have)
+	}
+	if want, have := "IBM/S390", cpuinfo[0].VendorID; want != have {
+		t.Errorf("want vendor %v, have %v", want, have)
+	}
+	if want, have := "ldisp", cpuinfo[1].Flags[4]; want != have {
+		t.Errorf("want flag %v, have %v", want, have)
+	}
+	if want, have := 5000.0, cpuinfo[2].CPUMHz; want != have {
+		t.Errorf("want cpu MHz %v, have %v", want, have)
+	}
+}
+
+func TestCPUInfoParsePPC(t *testing.T) {
+	cpuinfo, err := parseCPUInfoPPC([]byte(cpuinfoPpc64))
+	if err != nil || cpuinfo == nil {
+		t.Fatalf("unable to parse ppc cpu info: %v", err)
+	}
+	if want, have := 6, len(cpuinfo); want != have {
+		t.Errorf("want number of processors %v, have %v", want, have)
+	}
+	if want, have := 3000.00, cpuinfo[2].CPUMHz; want != have {
+		t.Errorf("want cpu mhz %v, have %v", want, have)
+	}
 }