From 62177de2b42c35bee645909cc074a2b6c4b67d5a Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 21 Apr 2021 10:19:08 +0200 Subject: [PATCH] fix(android): read hwcap using getauxval (v1) There are Android devices where reading /proc/self/auxv fails with permission denied. Android is increasingly preventing apps and tools from accessing system functionality. Go uses /proc/self/auxv to fill cpu.ARM64. In turn, it uses the values in this data structure to choose whether to enable an assembly implementation of AES as well as to choose the order of the cipher suites in the TLS ClientHello. Unfortunately, as documented in https://github.com/ooni/probe/issues/1444, reordering the ciphers in the ClientHello triggers censorship in Iran. Because new(?) Android devices do not allow Go to access /proc/self/auxv, we therefore see that OONI is not WAI. The most obvious fix to this issue could seem to teach crypto/tls to always put AES ciphers on top. Unfortunately, the pure Go AES implementation in the standard library is not constant time (while the one in assembly is constant time). Therefore, forcing AES to always be at the top of the ciphers list does not seem right. Thus, we can say that our real core problem is that Go is not able to know whether we support AES and PMULL on android/arm64. This diff attempts to solve this problem. The solution I have chosen is to implement a cpuproxy package exposing predicate functions describing the CPU's support for AES and PMULL. This cpuproxy package will use the existing logic for every GOOS and GOARCH except android/arm64. When we are in android/arm64, we use `getauxval(3)` to obtain the hardware capabilities and we use this information to implement the predicates. My initial attempt at solving this problem was to call `getauxval(3)` directly during the initialization of the `runtime` package. But it seems that when you use CGO you depend on the `syscall` package, and in turn `syscall` depends on `runtime`. So, when I was compiling OONI for Android, I always got a circular-import hard failure. Another approach could be that of reading `/proc/cpuinfo` inside of the `runtime` initialization. I did not explore it. This solution based on `libc` and `getauxval(3)` is not perfect but we already build OONI for Android with CGO enabled, so we should be fine in terms of the functionality that we require as OONI. I also adjusted the tests to reflect the fact that now there are more packages that depend on CGO (when using Android). --- src/crypto/aes/cipher_asm.go | 6 +- src/crypto/tls/common.go | 5 +- src/go/build/deps_test.go | 10 ++- .../cpuproxy/cpuproxy_android_arm64.go | 79 +++++++++++++++++++ src/internal/cpuproxy/cpuproxy_otherwise.go | 16 ++++ src/internal/cpuproxy/doc.go | 22 ++++++ 6 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 src/internal/cpuproxy/cpuproxy_android_arm64.go create mode 100644 src/internal/cpuproxy/cpuproxy_otherwise.go create mode 100644 src/internal/cpuproxy/doc.go diff --git a/src/crypto/aes/cipher_asm.go b/src/crypto/aes/cipher_asm.go index 646bdfa5c0e6ce..2b0a0758473216 100644 --- a/src/crypto/aes/cipher_asm.go +++ b/src/crypto/aes/cipher_asm.go @@ -9,7 +9,7 @@ package aes import ( "crypto/cipher" "crypto/internal/subtle" - "internal/cpu" + "internal/cpuproxy" ) // defined in asm_*.s @@ -27,8 +27,8 @@ type aesCipherAsm struct { aesCipher } -var supportsAES = cpu.X86.HasAES || cpu.ARM64.HasAES -var supportsGFMUL = cpu.X86.HasPCLMULQDQ || cpu.ARM64.HasPMULL +var supportsAES = cpuproxy.HasAES() +var supportsGFMUL = cpuproxy.HasGFMUL() func newCipher(key []byte) (cipher.Block, error) { if !supportsAES { diff --git a/src/crypto/tls/common.go b/src/crypto/tls/common.go index eec6e1ebbd9060..ea79e4647b694d 100644 --- a/src/crypto/tls/common.go +++ b/src/crypto/tls/common.go @@ -17,6 +17,7 @@ import ( "crypto/x509" "errors" "fmt" + "internal/cpuproxy" "internal/cpu" "io" "net" @@ -1436,8 +1437,8 @@ func defaultCipherSuitesTLS13() []uint16 { } var ( - hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ - hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL + hasGCMAsmAMD64 = cpuproxy.HasAES() && cpuproxy.HasGFMUL() + hasGCMAsmARM64 = cpuproxy.HasAES() && cpuproxy.HasGFMUL() // Keep in sync with crypto/aes/cipher_s390x.go. hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index c97c668cc49229..3ee0dc8abc1486 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -323,7 +323,7 @@ var depsRules = ` # Bulk of the standard library must not use cgo. # The prohibition stops at net and os/user. - C !< fmt, go/types, CRYPTO-MATH; + C !< fmt, go/types; CGO, OS < plugin; @@ -377,9 +377,11 @@ var depsRules = ` NET, log < net/mail; + CGO, internal/cpu, sync < internal/cpuproxy; + # CRYPTO is core crypto algorithms - no cgo, fmt, net. # Unfortunately, stuck with reflect via encoding/binary. - encoding/binary, golang.org/x/sys/cpu, hash + encoding/binary, golang.org/x/sys/cpu, hash, internal/cpuproxy < crypto < crypto/subtle < crypto/internal/subtle @@ -388,7 +390,7 @@ var depsRules = ` crypto/sha1, crypto/sha256, crypto/sha512 < CRYPTO; - CGO, fmt, net !< CRYPTO; + fmt, net !< CRYPTO; # CRYPTO-MATH is core bignum-based crypto - no cgo, net; fmt now ok. CRYPTO, FMT, math/big @@ -404,7 +406,7 @@ var depsRules = ` < crypto/ecdsa < CRYPTO-MATH; - CGO, net !< CRYPTO-MATH; + net !< CRYPTO-MATH; # TLS, Prince of Dependencies. CRYPTO-MATH, NET, container/list, encoding/hex, encoding/pem diff --git a/src/internal/cpuproxy/cpuproxy_android_arm64.go b/src/internal/cpuproxy/cpuproxy_android_arm64.go new file mode 100644 index 00000000000000..8a25f2bbb9f1c9 --- /dev/null +++ b/src/internal/cpuproxy/cpuproxy_android_arm64.go @@ -0,0 +1,79 @@ +// +build android +// +build arm64 + +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// This file is based on the diff available at +// https://go-review.googlesource.com/c/sys/+/197540/ + +package cpuproxy + +/* +#include + +// getauxval is not available on Android until API level 20. Link it as a weak +// symbol and check whether it is not NULL before using it. +unsigned long getauxval(unsigned long type) __attribute__((weak)); +*/ +import "C" + +import "sync" + +// These constants are Linux specific. +const ( + _AT_HWCAP = 16 // hardware capability bit vector +) + +// get returns the value of the getauxval auxiliary vector, or +// zero where the functionality is unavailable. +func get(t uint) uint { + if C.getauxval == C.NULL { + return 0 + } + return uint(C.getauxval(C.ulong(t))) +} + +// dogethwcap returns the value of _AT_HWCAP by calling +// the getauxval(3) function in libc (if available). +func dogethwcap() uint { + return get(_AT_HWCAP) +} + +// These variables allow to cache getauxval(3) results. +var ( + once sync.Once + hwcap uint +) + +// gethwcap is like dogethwcap except that this function +// ensures we call getauxval(3) just once. After the first +// invocation we memoize the result. +func gethwcap() uint { + once.Do(func() { + hwcap = dogethwcap() + }) + return hwcap +} + +// HWCAP bits. These are exposed by Linux. +const ( + hwcap_AES = 1 << 3 + hwcap_PMULL = 1 << 4 + hwcap_SHA1 = 1 << 5 + hwcap_SHA2 = 1 << 6 + hwcap_CRC32 = 1 << 7 + hwcap_ATOMICS = 1 << 8 + hwcap_CPUID = 1 << 11 +) + +// HasAES returns whether the CPU supports AES. +func HasAES() bool { + return (gethwcap() & hwcap_AES) != 0 +} + +// HasGFMUL returns whether the CPU supports GFMUL. +func HasGFMUL() bool { + return (gethwcap() & hwcap_PMULL) != 0 +} diff --git a/src/internal/cpuproxy/cpuproxy_otherwise.go b/src/internal/cpuproxy/cpuproxy_otherwise.go new file mode 100644 index 00000000000000..abcb55d09e0f82 --- /dev/null +++ b/src/internal/cpuproxy/cpuproxy_otherwise.go @@ -0,0 +1,16 @@ +// +build !android !arm64 + +package cpuproxy + +import "internal/cpu" + +// HasAES returns whether the CPU supports AES. +func HasAES() bool { + return cpu.X86.HasAES || cpu.ARM64.HasAES +} + +// HasGFMUL returns whether the CPU supports GFMUL. +func HasGFMUL() bool { + return cpu.X86.HasPCLMULQDQ || cpu.ARM64.HasPMULL +} + diff --git a/src/internal/cpuproxy/doc.go b/src/internal/cpuproxy/doc.go new file mode 100644 index 00000000000000..cb85775b974baf --- /dev/null +++ b/src/internal/cpuproxy/doc.go @@ -0,0 +1,22 @@ +// Package cpuproxy is a getauxval aware proxy for internal/cpu. +// +// The problem that we want to solve is that on Android there are +// cases where reading /proc/self/auxv is not possible. +// +// This causes crypto/tls to not choose AES where it would otherwise +// be possible, in turn causing censorship. See also the +// https://github.com/ooni/probe/issues/1444 issue for more details. +// +// Ideally we would like to call getauxval(3) when initializing +// the runtime package. However, runtime cannot use CGO. Doing that +// leads to an import loop, so we cannot build. +// +// We could also try to parse /proc/cpuinfo (I didn't explore this route). +// +// The solution chosen here is to export predicates on the CPU +// functionality. We limit ourselves to what we need in order to +// choose AES in crypto/tls when the CPU supports it. +// +// The predicates use internal/cpu values for every GOOS/GOARCH except +// android/arm64, where we call getauxval(3). +package cpuproxy