Skip to content

Commit 150fae7

Browse files
committed
crypto/x509: don't force system roots load in SetFallbackRoots
This change removes the need from SetFallbackRoots to force loading of all system CAs, it postpones that to initSystemRoots. This change also introduces few tests for SetFallbackRoots (linux only), with the use of user and mount namespaces, such that we can control the system CAs in the test. Updates golang#73691 Change-Id: Ic37270f7825b96d5c3ed8358bbf1895a760a1312 Reviewed-on: https://go-review.googlesource.com/c/go/+/677496 Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Emmanuel Odeke <[email protected]>
1 parent 4f7bbc6 commit 150fae7

File tree

3 files changed

+337
-14
lines changed

3 files changed

+337
-14
lines changed

src/crypto/x509/root.go

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import (
2020
//
2121
//go:linkname systemRoots
2222
var (
23-
once sync.Once
24-
systemRootsMu sync.RWMutex
25-
systemRoots *CertPool
26-
systemRootsErr error
27-
fallbacksSet bool
23+
once sync.Once
24+
systemRootsMu sync.RWMutex
25+
systemRoots *CertPool
26+
systemRootsErr error
27+
fallbacksSet bool
28+
useFallbackRoots bool
2829
)
2930

3031
func systemRootsPool() *CertPool {
@@ -37,10 +38,28 @@ func systemRootsPool() *CertPool {
3738
func initSystemRoots() {
3839
systemRootsMu.Lock()
3940
defer systemRootsMu.Unlock()
41+
42+
fallbackRoots := systemRoots
4043
systemRoots, systemRootsErr = loadSystemRoots()
4144
if systemRootsErr != nil {
4245
systemRoots = nil
4346
}
47+
48+
if fallbackRoots == nil {
49+
return // no fallbacks to try
50+
}
51+
52+
systemCertsAvail := systemRoots != nil && (systemRoots.len() > 0 || systemRoots.systemPool)
53+
54+
if !useFallbackRoots && systemCertsAvail {
55+
return
56+
}
57+
58+
if useFallbackRoots && systemCertsAvail {
59+
x509usefallbackroots.IncNonDefault() // overriding system certs with fallback certs.
60+
}
61+
62+
systemRoots, systemRootsErr = fallbackRoots, nil
4463
}
4564

4665
var x509usefallbackroots = godebug.New("x509usefallbackroots")
@@ -63,10 +82,6 @@ func SetFallbackRoots(roots *CertPool) {
6382
panic("roots must be non-nil")
6483
}
6584

66-
// trigger initSystemRoots if it hasn't already been called before we
67-
// take the lock
68-
_ = systemRootsPool()
69-
7085
systemRootsMu.Lock()
7186
defer systemRootsMu.Unlock()
7287

@@ -75,11 +90,28 @@ func SetFallbackRoots(roots *CertPool) {
7590
}
7691
fallbacksSet = true
7792

78-
if systemRoots != nil && (systemRoots.len() > 0 || systemRoots.systemPool) {
79-
if x509usefallbackroots.Value() != "1" {
80-
return
81-
}
82-
x509usefallbackroots.IncNonDefault()
93+
// Handle case when initSystemRoots was not yet executed.
94+
// We handle that specially instead of calling loadSystemRoots, to avoid
95+
// spending excessive amount of cpu here, since the SetFallbackRoots in most cases
96+
// is going to be called at program startup.
97+
if systemRoots == nil && systemRootsErr == nil {
98+
systemRoots = roots
99+
useFallbackRoots = x509usefallbackroots.Value() == "1"
100+
return
101+
}
102+
103+
once.Do(func() { panic("unreachable") }) // asserts that system roots were indeed loaded before.
104+
105+
forceFallbackRoots := x509usefallbackroots.Value() == "1"
106+
systemCertsAvail := systemRoots != nil && (systemRoots.len() > 0 || systemRoots.systemPool)
107+
108+
if !forceFallbackRoots && systemCertsAvail {
109+
return
110+
}
111+
112+
if forceFallbackRoots && systemCertsAvail {
113+
x509usefallbackroots.IncNonDefault() // overriding system certs with fallback certs.
83114
}
115+
84116
systemRoots, systemRootsErr = roots, nil
85117
}

src/crypto/x509/root_linux_test.go

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build linux
6+
7+
package x509
8+
9+
import (
10+
"encoding/pem"
11+
"fmt"
12+
"internal/testenv"
13+
"os"
14+
"os/exec"
15+
"syscall"
16+
"testing"
17+
)
18+
19+
func TestSetFallbackRoots(t *testing.T) {
20+
if testing.Short() {
21+
t.Skip("skipping test in short mode")
22+
}
23+
testenv.MustHaveExec(t)
24+
25+
test := func(t *testing.T, name string, f func(t *testing.T)) {
26+
t.Run(name, func(t *testing.T) {
27+
if os.Getenv("CRYPTO_X509_SETFALLBACKROOTS_TEST") != "1" {
28+
// Execute test in a separate process with CRYPTO_X509_SETFALBACKROOTS_TEST env.
29+
cmd := exec.Command(os.Args[0], fmt.Sprintf("-test.run=^%v$", t.Name()))
30+
cmd.Env = append(os.Environ(), "CRYPTO_X509_SETFALLBACKROOTS_TEST=1")
31+
cmd.SysProcAttr = &syscall.SysProcAttr{
32+
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
33+
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getuid(), Size: 1}},
34+
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getgid(), Size: 1}},
35+
}
36+
out, err := cmd.CombinedOutput()
37+
if err != nil {
38+
if testenv.SyscallIsNotSupported(err) {
39+
t.Skipf("skipping: could not start process with CLONE_NEWNS and CLONE_NEWUSER: %v", err)
40+
}
41+
t.Errorf("%v\n%s", err, out)
42+
}
43+
return
44+
}
45+
46+
// This test is executed in a separate user and mount namespace, thus
47+
// we can mount a separate "/etc" empty bind mount, without the need for root access.
48+
// On linux all certs reside in /etc, so as we bind an empty dir, we
49+
// get a full control over the system CAs, required for this test.
50+
if err := syscall.Mount(t.TempDir(), "/etc", "", syscall.MS_BIND, ""); err != nil {
51+
if testenv.SyscallIsNotSupported(err) {
52+
t.Skipf("Failed to mount /etc: %v", err)
53+
}
54+
t.Fatalf("Failed to mount /etc: %v", err)
55+
}
56+
57+
t.Cleanup(func() {
58+
if err := syscall.Unmount("/etc", 0); err != nil {
59+
t.Errorf("failed to unmount /etc: %v", err)
60+
}
61+
})
62+
63+
f(t)
64+
})
65+
}
66+
67+
newFallbackCertPool := func(t *testing.T) *CertPool {
68+
t.Helper()
69+
70+
const fallbackCert = `-----BEGIN CERTIFICATE-----
71+
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
72+
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
73+
R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
74+
MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
75+
ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
76+
EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
77+
+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
78+
ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
79+
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
80+
zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
81+
tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
82+
/q4AaOeMSQ+2b1tbFfLn
83+
-----END CERTIFICATE-----
84+
`
85+
b, _ := pem.Decode([]byte(fallbackCert))
86+
cert, err := ParseCertificate(b.Bytes)
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
p := NewCertPool()
91+
p.AddCert(cert)
92+
return p
93+
}
94+
95+
installSystemRootCAs := func(t *testing.T) {
96+
t.Helper()
97+
98+
const systemCAs = `-----BEGIN CERTIFICATE-----
99+
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
128+
-----END CERTIFICATE-----
129+
`
130+
if err := os.MkdirAll("/etc/ssl/certs", 06660); err != nil {
131+
t.Fatal(err)
132+
}
133+
134+
if err := os.WriteFile("/etc/ssl/certs/ca-certificates.crt", []byte(systemCAs), 0666); err != nil {
135+
t.Fatal(err)
136+
}
137+
}
138+
139+
test(t, "after_first_load_no_system_CAs", func(t *testing.T) {
140+
SystemCertPool() // load system certs, before setting fallbacks
141+
fallback := newFallbackCertPool(t)
142+
SetFallbackRoots(fallback)
143+
got, err := SystemCertPool()
144+
if err != nil {
145+
t.Fatal(err)
146+
}
147+
if !got.Equal(fallback) {
148+
t.Fatal("SystemCertPool returned a non-fallback CertPool")
149+
}
150+
})
151+
152+
test(t, "after_first_load_system_CA_read_error", func(t *testing.T) {
153+
// This will fail to load in SystemCertPool since this is a directory,
154+
// rather than a file with certificates.
155+
if err := os.MkdirAll("/etc/ssl/certs/ca-certificates.crt", 0666); err != nil {
156+
t.Fatal(err)
157+
}
158+
159+
_, err := SystemCertPool() // load system certs, before setting fallbacks
160+
if err == nil {
161+
t.Fatal("unexpected success")
162+
}
163+
164+
fallback := newFallbackCertPool(t)
165+
SetFallbackRoots(fallback)
166+
got, err := SystemCertPool()
167+
if err != nil {
168+
t.Fatal(err)
169+
}
170+
if !got.Equal(fallback) {
171+
t.Fatal("SystemCertPool returned a non-fallback CertPool")
172+
}
173+
})
174+
175+
test(t, "after_first_load_with_system_CAs", func(t *testing.T) {
176+
installSystemRootCAs(t)
177+
178+
SystemCertPool() // load system certs, before setting fallbacks
179+
180+
fallback := newFallbackCertPool(t)
181+
SetFallbackRoots(fallback)
182+
got, err := SystemCertPool()
183+
if err != nil {
184+
t.Fatal(err)
185+
}
186+
if got.Equal(fallback) {
187+
t.Fatal("SystemCertPool returned the fallback CertPool")
188+
}
189+
})
190+
191+
test(t, "before_first_load_no_system_CAs", func(t *testing.T) {
192+
fallback := newFallbackCertPool(t)
193+
SetFallbackRoots(fallback)
194+
got, err := SystemCertPool()
195+
if err != nil {
196+
t.Fatal(err)
197+
}
198+
if !got.Equal(fallback) {
199+
t.Fatal("SystemCertPool returned a non-fallback CertPool")
200+
}
201+
})
202+
203+
test(t, "before_first_load_system_CA_read_error", func(t *testing.T) {
204+
// This will fail to load in SystemCertPool since this is a directory,
205+
// rather than a file with certificates.
206+
if err := os.MkdirAll("/etc/ssl/certs/ca-certificates.crt", 0666); err != nil {
207+
t.Fatal(err)
208+
}
209+
210+
fallback := newFallbackCertPool(t)
211+
SetFallbackRoots(fallback)
212+
got, err := SystemCertPool()
213+
if err != nil {
214+
t.Fatal(err)
215+
}
216+
if !got.Equal(fallback) {
217+
t.Fatal("SystemCertPool returned a non-fallback CertPool")
218+
}
219+
})
220+
221+
test(t, "before_first_load_with_system_CAs", func(t *testing.T) {
222+
installSystemRootCAs(t)
223+
224+
fallback := newFallbackCertPool(t)
225+
SetFallbackRoots(fallback)
226+
got, err := SystemCertPool()
227+
if err != nil {
228+
t.Fatal(err)
229+
}
230+
if got.Equal(fallback) {
231+
t.Fatal("SystemCertPool returned the fallback CertPool")
232+
}
233+
})
234+
235+
test(t, "before_first_load_force_godebug", func(t *testing.T) {
236+
if err := os.Setenv("GODEBUG", "x509usefallbackroots=1"); err != nil {
237+
t.Fatal(err)
238+
}
239+
240+
installSystemRootCAs(t)
241+
242+
fallback := newFallbackCertPool(t)
243+
SetFallbackRoots(fallback)
244+
got, err := SystemCertPool()
245+
if err != nil {
246+
t.Fatal(err)
247+
}
248+
if !got.Equal(fallback) {
249+
t.Fatal("SystemCertPool returned a non-fallback CertPool")
250+
}
251+
})
252+
253+
test(t, "after_first_load_force_godebug", func(t *testing.T) {
254+
if err := os.Setenv("GODEBUG", "x509usefallbackroots=1"); err != nil {
255+
t.Fatal(err)
256+
}
257+
258+
installSystemRootCAs(t)
259+
SystemCertPool() // load system certs, before setting fallbacks
260+
261+
fallback := newFallbackCertPool(t)
262+
SetFallbackRoots(fallback)
263+
got, err := SystemCertPool()
264+
if err != nil {
265+
t.Fatal(err)
266+
}
267+
if !got.Equal(fallback) {
268+
t.Fatal("SystemCertPool returned a non-fallback CertPool")
269+
}
270+
})
271+
272+
test(t, "after_first_load_force_godebug_no_system_certs", func(t *testing.T) {
273+
if err := os.Setenv("GODEBUG", "x509usefallbackroots=1"); err != nil {
274+
t.Fatal(err)
275+
}
276+
277+
SystemCertPool() // load system certs, before setting fallbacks
278+
279+
fallback := newFallbackCertPool(t)
280+
SetFallbackRoots(fallback)
281+
got, err := SystemCertPool()
282+
if err != nil {
283+
t.Fatal(err)
284+
}
285+
if !got.Equal(fallback) {
286+
t.Fatal("SystemCertPool returned a non-fallback CertPool")
287+
}
288+
})
289+
}

src/crypto/x509/root_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,10 @@ func TestFallback(t *testing.T) {
7979

8080
for _, tc := range tests {
8181
t.Run(tc.name, func(t *testing.T) {
82+
useFallbackRoots = false
8283
fallbacksSet = false
8384
systemRoots = tc.systemRoots
85+
8486
if systemRoots != nil {
8587
systemRoots.systemPool = tc.systemPool
8688
}

0 commit comments

Comments
 (0)