1//go:build linux && (amd64 || arm64)
2
3package pam
4
5import (
6 "errors"
7 "testing"
8 "time"
9 "unsafe"
10
11 "github.com/bnema/purego"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14)
15
16func nativeMessages(t *testing.T, memory nativeMemory, styles ...int32) (unsafe.Pointer, unsafe.Pointer) {
17 t.Helper()
18 pointers, err := memory.alloc(uintptr(len(styles)) * 8)
19 require.NoError(t, err)
20 t.Cleanup(func() { memory.free(pointers) })
21 for i, style := range styles {
22 text, err := nativeCString(memory, "synthetic prompt")
23 require.NoError(t, err)
24 t.Cleanup(func() { memory.free(text) })
25 message, err := memory.alloc(16)
26 require.NoError(t, err)
27 t.Cleanup(func() { memory.free(message) })
28 *(*nativeMessage)(message) = nativeMessage{Style: style, Text: text}
29 unsafe.Slice((*unsafe.Pointer)(pointers), len(styles))[i] = message
30 }
31 output, err := memory.alloc(8)
32 require.NoError(t, err)
33 t.Cleanup(func() { memory.free(output) })
34 return pointers, output
35}
36
37func invokeConversation(t *testing.T, cookie uintptr, count int32, messages, output unsafe.Pointer) Status {
38 t.Helper()
39 address, err := callbackAddress()
40 require.NoError(t, err)
41 result, _, _ := purego.SyscallN(address, uintptr(count), uintptr(messages), uintptr(output), cookie)
42 return Status(int32(result))
43}
44
45func callbackState(t *testing.T, handler Conversation, memory nativeMemory) *conversationState {
46 t.Helper()
47 state, err := registerConversation(handler, memory)
48 require.NoError(t, err)
49 t.Cleanup(state.retire)
50 return state
51}
52
53func TestConversationCABIAndResponseOwnership(t *testing.T) {
54 l, _, _ := testLibrary(t)
55 handler := NewMockConversation(t)
56 s := callbackState(t, handler, l.memory)
57 messages, output := nativeMessages(t, l.memory, PromptEchoOff, PromptEchoOn, ErrorMsg, TextInfo)
58 var answers, prompts [][]byte
59 handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(message Message, answer []byte) (int, error) {
60 require.Equal(t, "synthetic prompt", string(message.Text))
61 require.Equal(t, make([]byte, MaxRespSize), answer)
62 answers = append(answers, answer)
63 prompts = append(prompts, message.Text)
64 if message.Style == PromptEchoOff || message.Style == PromptEchoOn {
65 copy(answer, "synthetic answer")
66 return 16, nil
67 }
68 return 0, nil
69 }).Times(4)
70 require.Equal(t, Success, invokeConversation(t, s.token, 4, messages, output))
71 for _, answer := range answers {
72 require.Equal(t, make([]byte, len(answer)), answer)
73 }
74 for _, prompt := range prompts {
75 require.Equal(t, make([]byte, len(prompt)), prompt)
76 }
77 responses := *(*unsafe.Pointer)(output)
78 require.NotNil(t, responses)
79 array := unsafe.Slice((*nativeResponse)(responses), 4)
80 for i := range array {
81 require.Zero(t, array[i].ReturnCode)
82 if i < 2 {
83 require.Equal(t, []byte("synthetic answer\x00"), unsafe.Slice((*byte)(array[i].Text), 17))
84 l.memory.free(array[i].Text) // simulate PAM ownership release
85 } else {
86 require.Nil(t, array[i].Text)
87 }
88 }
89 l.memory.free(responses)
90}
91
92func TestHandlerFailureAndBoundsWipeBuffers(t *testing.T) {
93 for _, scenario := range []string{"error", "panic", "negative", "oversize", "embedded-nul", "info-answer"} {
94 t.Run(scenario, func(t *testing.T) {
95 l, _, _ := testLibrary(t)
96 handler := NewMockConversation(t)
97 s := callbackState(t, handler, l.memory)
98 style := PromptEchoOff
99 if scenario == "info-answer" {
100 style = TextInfo
101 }
102 messages, output := nativeMessages(t, l.memory, style)
103 var capturedAnswer, capturedPrompt []byte
104 handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(message Message, answer []byte) (int, error) {
105 capturedAnswer, capturedPrompt = answer, message.Text
106 copy(answer, "synthetic")
107 switch scenario {
108 case "error":
109 return 9, errors.New("sensitive payload must not escape")
110 case "panic":
111 panic("sensitive panic must not escape")
112 case "negative":
113 return -1, nil
114 case "oversize":
115 return MaxRespSize + 1, nil
116 case "embedded-nul":
117 return 10, nil
118 default:
119 return 9, nil
120 }
121 }).Once()
122 require.Equal(t, ConvErr, invokeConversation(t, s.token, 1, messages, output))
123 require.Nil(t, *(*unsafe.Pointer)(output))
124 require.Equal(t, make([]byte, len(capturedAnswer)), capturedAnswer)
125 require.Equal(t, make([]byte, len(capturedPrompt)), capturedPrompt)
126 })
127 }
128}
129
130func TestNativeInputValidation(t *testing.T) {
131 for _, scenario := range []string{"zero", "too-many", "negative", "null-array", "null-output", "null-message", "null-text", "binary", "unterminated", "unaligned-array", "unaligned-output", "unaligned-message"} {
132 t.Run(scenario, func(t *testing.T) {
133 l, _, _ := testLibrary(t)
134 handler := NewMockConversation(t) // no Respond allowed
135 s := callbackState(t, handler, l.memory)
136 messages, output := nativeMessages(t, l.memory, PromptEchoOff)
137 message := (*nativeMessage)(*(*unsafe.Pointer)(messages))
138 count := int32(1)
139 switch scenario {
140 case "zero":
141 count = 0
142 case "too-many":
143 count = MaxNumMsg + 1
144 case "negative":
145 count = -1
146 case "null-array":
147 messages = nil
148 case "null-output":
149 output = nil
150 case "null-message":
151 *(*unsafe.Pointer)(messages) = nil
152 case "null-text":
153 message.Text = nil
154 case "binary":
155 message.Style = 7
156 case "unterminated":
157 p, err := l.memory.alloc(MaxMsgSize)
158 require.NoError(t, err)
159 t.Cleanup(func() { l.memory.free(p) })
160 for i := range unsafe.Slice((*byte)(p), MaxMsgSize) {
161 unsafe.Slice((*byte)(p), MaxMsgSize)[i] = 'x'
162 }
163 message.Text = p
164 case "unaligned-array":
165 messages = unsafe.Add(messages, 1)
166 case "unaligned-output":
167 output = unsafe.Add(output, 1)
168 case "unaligned-message":
169 *(*unsafe.Pointer)(messages) = unsafe.Add(unsafe.Pointer(message), 1)
170 }
171 require.Equal(t, ConvErr, invokeConversation(t, s.token, count, messages, output))
172 })
173 }
174 t.Run("unknown-cookie-does-not-touch-pointers", func(t *testing.T) {
175 l, _, _ := testLibrary(t)
176 s := callbackState(t, NewMockConversation(t), l.memory)
177 s.retire()
178 address, err := callbackAddress()
179 require.NoError(t, err)
180 // Deliberately invalid addresses never converted to Go pointers.
181 result, _, _ := purego.SyscallN(address, 1, 1, 1, s.token)
182 require.Equal(t, ConvErr, Status(int32(result)))
183 })
184}
185
186func TestAllocationFailuresCleanPartialNativeResponses(t *testing.T) {
187 for failAt := 0; failAt < 3; failAt++ {
188 t.Run(string(rune('0'+failAt)), func(t *testing.T) {
189 l, _, _ := testLibrary(t)
190 memory := newMocknativeMemory(t)
191 handler := NewMockConversation(t)
192 s := callbackState(t, handler, memory)
193 messages, output := nativeMessages(t, l.memory, PromptEchoOff, PromptEchoOn)
194 index := 0
195 allocated := make(map[unsafe.Pointer]uintptr)
196 memory.EXPECT().alloc(mock.Anything).RunAndReturn(func(size uintptr) (unsafe.Pointer, error) {
197 current := index
198 index++
199 if current == failAt {
200 return nil, ErrAllocation
201 }
202 p, err := l.memory.alloc(size)
203 allocated[p] = size
204 return p, err
205 }).Times(failAt + 1)
206 if failAt > 0 {
207 memory.EXPECT().free(mock.Anything).Run(func(p unsafe.Pointer) {
208 size, ok := allocated[p]
209 require.True(t, ok)
210 if size == 4 {
211 require.Equal(t, make([]byte, 4), unsafe.Slice((*byte)(p), 4))
212 }
213 delete(allocated, p)
214 l.memory.free(p)
215 }).Times(failAt)
216 }
217 var answers [][]byte
218 if failAt > 0 {
219 handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(_ Message, answer []byte) (int, error) {
220 answers = append(answers, answer)
221 copy(answer, "abc")
222 return 3, nil
223 }).Times(failAt)
224 }
225 require.Equal(t, BufErr, invokeConversation(t, s.token, 2, messages, output))
226 require.Nil(t, *(*unsafe.Pointer)(output))
227 require.Empty(t, allocated)
228 for _, answer := range answers {
229 require.Equal(t, make([]byte, len(answer)), answer)
230 }
231 })
232 }
233}
234
235func TestEndRetiresAfterNativeCleanupAndWaitsForCallbacks(t *testing.T) {
236 l, calls, handle := testLibrary(t)
237 handler := NewMockConversation(t)
238 messages, output := nativeMessages(t, l.memory, TextInfo)
239 var conv nativeCookieConversation
240 calls.EXPECT().Start(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_, _, c, out unsafe.Pointer) Status {
241 conv = *(*nativeCookieConversation)(c)
242 *(*unsafe.Pointer)(out) = handle
243 return Success
244 }).Once()
245 tx, err := l.StartConversation("unit-service", "unit-user", handler)
246 require.NoError(t, err)
247 entered, release := make(chan struct{}), make(chan struct{})
248 handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(_ Message, _ []byte) (int, error) {
249 close(entered)
250 <-release
251 return 0, nil
252 }).Once()
253 callbackDone := make(chan Status, 1)
254 go func() { callbackDone <- invokeConversation(t, conv.Cookie, 1, messages, output) }()
255 select {
256 case <-entered:
257 case <-time.After(3 * time.Second):
258 t.Fatal("callback did not enter")
259 }
260 cleanupReturned := make(chan struct{})
261 calls.EXPECT().End(handle, Success).RunAndReturn(func(_ unsafe.Pointer, _ Status) Status {
262 // Admission must still be open during pam_end cleanup.
263 s := acquireConversation(conv.Cookie)
264 require.NotNil(t, s)
265 s.leave()
266 close(cleanupReturned)
267 return Success
268 }).Once()
269 endDone := make(chan error, 1)
270 go func() { endDone <- tx.End() }()
271 select {
272 case <-cleanupReturned:
273 case <-time.After(3 * time.Second):
274 t.Fatal("native End did not return")
275 }
276 select {
277 case <-endDone:
278 t.Fatal("End returned before callback finished")
279 default:
280 }
281 close(release)
282 select {
283 case status := <-callbackDone:
284 require.Equal(t, Success, status)
285 case <-time.After(3 * time.Second):
286 t.Fatal("callback did not finish")
287 }
288 select {
289 case err := <-endDone:
290 require.NoError(t, err)
291 case <-time.After(3 * time.Second):
292 t.Fatal("End did not finish")
293 }
294 require.Zero(t, l.active)
295 require.Empty(t, tx.storage)
296 require.Equal(t, ConvErr, invokeConversation(t, conv.Cookie, 1, nil, nil))
297 l.memory.free(*(*unsafe.Pointer)(output))
298 address, err := callbackAddress()
299 require.NoError(t, err)
300 require.Equal(t, conv.Function, address)
301}