conversation_review_linux_test.go RawHistoryFolder
1//go:build linux && (amd64 || arm64)
2
3package pam
4
5import (
6 "testing"
7 "unsafe"
8
9 "github.com/bnema/purego"
10 "github.com/stretchr/testify/mock"
11 "github.com/stretchr/testify/require"
12)
13
14func TestRawCallbackRejectsUnknownAndRetiredArbitraryBits(t *testing.T) {
15 l, _, _ := testLibrary(t)
16 s := callbackState(t, NewMockConversation(t), l.memory)
17 s.retire()
18 address, err := callbackAddress()
19 require.NoError(t, err)
20 for _, cookie := range []uintptr{0, s.token, ^uintptr(0)} {
21 for _, count := range []uintptr{1, 0, ^uintptr(0), uintptr(1)<<32 | 1} {
22 for _, bits := range []uintptr{0, 1, 8, ^uintptr(0), uintptr(1) << 63} {
23 // All values stay integers throughout unknown-cookie admission.
24 // Both aligned and noncanonical addresses must be ignored.
25 result, _, _ := purego.SyscallN(address, count, bits, ^bits, cookie)
26 require.Equal(t, uintptr(ConvErr), result)
27 }
28 }
29 }
30}
31
32func TestCookieExhaustionNeverWrapsOrReuses(t *testing.T) {
33 l, _, _ := testLibrary(t)
34 handler := NewMockConversation(t)
35 conversations.Lock()
36 require.Empty(t, conversations.states)
37 previous := conversations.next
38 conversations.next = ^uintptr(0) - 1
39 conversations.Unlock()
40 // Test-only counter isolation; production never resets the counter.
41 t.Cleanup(func() {
42 conversations.Lock()
43 defer conversations.Unlock()
44 require.Empty(t, conversations.states)
45 conversations.next = previous
46 })
47 last, err := registerConversation(handler, l.memory)
48 require.NoError(t, err)
49 require.Equal(t, ^uintptr(0), last.token)
50 last.retire()
51 for range 2 {
52 s, err := registerConversation(handler, l.memory)
53 require.Error(t, err)
54 require.Nil(t, s)
55 conversations.Lock()
56 require.Equal(t, ^uintptr(0), conversations.next)
57 require.Empty(t, conversations.states)
58 conversations.Unlock()
59 }
60}
61
62func TestAllocatorPanicsStillWipeAndReleasePartialResponses(t *testing.T) {
63 for _, panicFree := range []bool{false, true} {
64 t.Run(map[bool]string{false: "alloc-panic", true: "alloc-and-free-panic"}[panicFree], func(t *testing.T) {
65 l, _, _ := testLibrary(t)
66 memory := newMocknativeMemory(t)
67 handler := NewMockConversation(t)
68 s := callbackState(t, handler, memory)
69 messages, output := nativeMessages(t, l.memory, PromptEchoOff, PromptEchoOn)
70 allocated := make(map[unsafe.Pointer]uintptr)
71 index := 0
72 memory.EXPECT().alloc(mock.Anything).RunAndReturn(func(size uintptr) (unsafe.Pointer, error) {
73 index++
74 if index == 3 {
75 panic("allocator payload must not escape")
76 }
77 p, err := l.memory.alloc(size)
78 allocated[p] = size
79 return p, err
80 }).Times(3)
81 memory.EXPECT().free(mock.Anything).Run(func(p unsafe.Pointer) {
82 size, ok := allocated[p]
83 require.True(t, ok)
84 if size == 4 {
85 require.Equal(t, make([]byte, 4), unsafe.Slice((*byte)(p), 4))
86 }
87 delete(allocated, p)
88 l.memory.free(p)
89 if panicFree && size == 4 {
90 panic("free payload must not escape")
91 }
92 }).Times(2)
93 var answers, prompts [][]byte
94 handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(message Message, answer []byte) (int, error) {
95 answers = append(answers, answer)
96 prompts = append(prompts, message.Text)
97 copy(answer, "abc")
98 return 3, nil
99 }).Times(2)
100 require.Equal(t, ConvErr, invokeConversation(t, s.token, 2, messages, output))
101 require.Nil(t, *(*unsafe.Pointer)(output))
102 require.Empty(t, allocated)
103 for _, bytes := range append(answers, prompts...) {
104 require.Equal(t, make([]byte, len(bytes)), bytes)
105 }
106 // Reference release also survives allocator and cleanup panics.
107 conversations.Lock()
108 require.Zero(t, s.refs)
109 conversations.Unlock()
110 s.retire()
111 })
112 }
113}