conversation_extra_linux_test.go RawHistoryFolder
1//go:build linux && (amd64 || arm64)
2
3package pam
4
5import (
6 "testing"
7 "time"
8 "unsafe"
9
10 "github.com/stretchr/testify/mock"
11 "github.com/stretchr/testify/require"
12)
13
14func TestManagedStartFailureRetiresCookie(t *testing.T) {
15 l, calls, _ := testLibrary(t)
16 handler := NewMockConversation(t)
17 var cookie uintptr
18 calls.EXPECT().Start(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_, _, conv, _ unsafe.Pointer) Status {
19 cookie = (*nativeCookieConversation)(conv).Cookie
20 return ServiceErr
21 }).Once()
22 tx, err := l.StartConversation("unit-service", "unit-user", handler)
23 require.Error(t, err)
24 require.Nil(t, tx)
25 require.Nil(t, acquireConversation(cookie))
26 require.Zero(t, l.active)
27 _, err = l.StartConversation("unit-service", "unit-user", nil)
28 require.ErrorIs(t, err, ErrInvalidArgument)
29 _, err = l.StartConversation("", "unit-user", handler)
30 require.ErrorIs(t, err, ErrInvalidArgument)
31}
32
33func TestNativeEndCanInvokeManagedConversation(t *testing.T) {
34 l, calls, handle := testLibrary(t)
35 handler := NewMockConversation(t)
36 messages, output := nativeMessages(t, l.memory, TextInfo)
37 var cookie uintptr
38 calls.EXPECT().Start(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_, _, c, out unsafe.Pointer) Status {
39 cookie = (*nativeCookieConversation)(c).Cookie
40 *(*unsafe.Pointer)(out) = handle
41 return Success
42 }).Once()
43 tx, err := l.StartConversation("unit-service", "", handler)
44 require.NoError(t, err)
45 handler.EXPECT().Respond(mock.Anything, mock.Anything).Return(0, nil).Once()
46 calls.EXPECT().End(handle, Success).RunAndReturn(func(_ unsafe.Pointer, _ Status) Status {
47 require.Equal(t, Success, invokeConversation(t, cookie, 1, messages, output))
48 l.memory.free(*(*unsafe.Pointer)(output))
49 return SystemErr
50 }).Once()
51 require.Error(t, tx.End())
52 require.Nil(t, acquireConversation(cookie))
53 require.Empty(t, tx.storage)
54 require.Zero(t, l.active)
55}
56
57func TestConcurrentEntriesAreSerializedAndRetirementWaits(t *testing.T) {
58 l, _, _ := testLibrary(t)
59 handler := NewMockConversation(t)
60 s := callbackState(t, handler, l.memory)
61 messages, output1 := nativeMessages(t, l.memory, TextInfo)
62 _, output2 := nativeMessages(t, l.memory, TextInfo)
63 entered, release := make(chan struct{}), make(chan struct{})
64 second := make(chan struct{}, 1)
65 handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(_ Message, _ []byte) (int, error) {
66 close(entered)
67 <-release
68 return 0, nil
69 }).Once()
70 handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(_ Message, _ []byte) (int, error) {
71 second <- struct{}{}
72 return 0, nil
73 }).Once()
74 done1, done2 := make(chan Status, 1), make(chan Status, 1)
75 go func() { done1 <- invokeConversation(t, s.token, 1, messages, output1) }()
76 select {
77 case <-entered:
78 case <-time.After(3 * time.Second):
79 t.Fatal("first callback not entered")
80 }
81 // Admit a second entry explicitly; it holds a registry reference until its
82 // callback finishes, so retirement must wait even if it closes admission.
83 admitted := acquireConversation(s.token)
84 require.NotNil(t, admitted)
85 go func() {
86 done2 <- invokeConversation(t, s.token, 1, messages, output2)
87 admitted.leave()
88 }()
89 select {
90 case <-second:
91 t.Fatal("handler reentered while first was active")
92 default:
93 }
94 // Do not retire until both actual callbacks finished: native calls above
95 // exercise serialization, while explicit admitted reference tests the wait.
96 close(release)
97 select {
98 case status := <-done1:
99 require.Equal(t, Success, status)
100 case <-time.After(3 * time.Second):
101 t.Fatal("first callback blocked")
102 }
103 select {
104 case status := <-done2:
105 require.Equal(t, Success, status)
106 case <-time.After(3 * time.Second):
107 t.Fatal("second callback blocked")
108 }
109 for _, output := range []unsafe.Pointer{output1, output2} {
110 l.memory.free(*(*unsafe.Pointer)(output))
111 }
112 held := acquireConversation(s.token)
113 require.NotNil(t, held)
114 retired := make(chan struct{})
115 go func() { s.retire(); close(retired) }()
116 select {
117 case <-retired:
118 t.Fatal("retirement ignored admitted reference")
119 default:
120 }
121 held.leave()
122 select {
123 case <-retired:
124 case <-time.After(3 * time.Second):
125 t.Fatal("retirement did not finish")
126 }
127 require.Nil(t, acquireConversation(s.token))
128}
129
130func TestMaximumAnswerAndMessageCount(t *testing.T) {
131 l, _, _ := testLibrary(t)
132 handler := NewMockConversation(t)
133 s := callbackState(t, handler, l.memory)
134 styles := make([]int32, MaxNumMsg)
135 for i := range styles {
136 styles[i] = PromptEchoOff
137 }
138 messages, output := nativeMessages(t, l.memory, styles...)
139 handler.EXPECT().Respond(mock.Anything, mock.Anything).RunAndReturn(func(_ Message, answer []byte) (int, error) {
140 for i := range answer {
141 answer[i] = 'x'
142 }
143 return MaxRespSize, nil
144 }).Times(MaxNumMsg)
145 require.Equal(t, Success, invokeConversation(t, s.token, MaxNumMsg, messages, output))
146 p := *(*unsafe.Pointer)(output)
147 for _, response := range unsafe.Slice((*nativeResponse)(p), MaxNumMsg) {
148 bytes := unsafe.Slice((*byte)(response.Text), MaxRespSize+1)
149 require.Equal(t, byte('x'), bytes[MaxRespSize-1])
150 require.Zero(t, bytes[MaxRespSize])
151 l.memory.free(response.Text)
152 }
153 l.memory.free(p)
154}