1//go:build linux && (amd64 || arm64)
2
3package pam
4
5import (
6 "errors"
7 "fmt"
8 "testing"
9 "unsafe"
10
11 "github.com/bnema/purego"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14)
15
16// Real libc allocations plus a generated PAM seam; no real PAM transaction.
17func testLibrary(t *testing.T) (*Library, *mocklibraryFunctions, unsafe.Pointer) {
18 t.Helper()
19 l, err := Open()
20 require.NoError(t, err)
21 t.Cleanup(func() { require.NoError(t, l.Close()) })
22 handle, err := l.memory.alloc(8)
23 require.NoError(t, err)
24 t.Cleanup(func() { l.memory.free(handle) })
25 calls := newMocklibraryFunctions(t)
26 l.calls = calls
27 return l, calls, handle
28}
29
30func expectStart(t *testing.T, calls *mocklibraryFunctions, handle unsafe.Pointer, status Status) {
31 t.Helper()
32 calls.EXPECT().Start(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
33 RunAndReturn(func(service, user, conv, output unsafe.Pointer) Status {
34 require.Equal(t, []byte("unit-service\x00"), unsafe.Slice((*byte)(service), 13))
35 require.Equal(t, []byte("unit-user\x00"), unsafe.Slice((*byte)(user), 10))
36 require.Equal(t, uintptr(1), (*nativeConversation)(conv).Function)
37 require.Nil(t, (*nativeConversation)(conv).Data)
38 require.Nil(t, *(*unsafe.Pointer)(output))
39 *(*unsafe.Pointer)(output) = handle
40 return status
41 }).Once()
42}
43
44func TestLoadSymbolsAndNativeMemory(t *testing.T) {
45 l, err := Open()
46 require.NoError(t, err)
47 defer func() { require.NoError(t, l.Close()) }()
48 for _, name := range []string{"pam_start", "pam_authenticate", "pam_acct_mgmt", "pam_end"} {
49 address, err := purego.Dlsym(l.pamHandle, name)
50 require.NoError(t, err)
51 require.NotZero(t, address)
52 }
53 var missing func()
54 require.Error(t, register(&missing, l.pamHandle, "purego_pam_nonexistent_symbol"))
55 p, err := l.memory.alloc(32)
56 require.NoError(t, err)
57 require.Equal(t, make([]byte, 32), unsafe.Slice((*byte)(p), 32))
58 l.memory.free(p)
59 p, err = nativeCString(l.memory, "synthetic")
60 require.NoError(t, err)
61 require.Equal(t, []byte("synthetic\x00"), unsafe.Slice((*byte)(p), 10))
62 l.memory.free(p)
63 _, err = nativeCString(l.memory, "bad\x00text")
64 require.ErrorIs(t, err, ErrInvalidArgument)
65}
66
67func TestNativeLayouts(t *testing.T) {
68 require.Equal(t, uintptr(4), unsafe.Sizeof(Status(0)))
69 require.Equal(t, uintptr(4), unsafe.Sizeof(Flags(0)))
70 require.Equal(t, uintptr(8), unsafe.Sizeof(uintptr(0)))
71 require.Equal(t, uintptr(16), unsafe.Sizeof(nativeMessage{}))
72 require.Equal(t, uintptr(16), unsafe.Sizeof(nativeResponse{}))
73 require.Equal(t, uintptr(16), unsafe.Sizeof(nativeConversation{}))
74 require.Equal(t, uintptr(8), unsafe.Offsetof(nativeMessage{}.Text))
75 require.Equal(t, uintptr(8), unsafe.Offsetof(nativeResponse{}.ReturnCode))
76 require.Equal(t, uintptr(8), unsafe.Offsetof(nativeConversation{}.Data))
77 require.Equal(t, uintptr(8), unsafe.Alignof(nativeMessage{}))
78 require.Equal(t, uintptr(8), unsafe.Alignof(nativeResponse{}))
79 require.Equal(t, uintptr(8), unsafe.Alignof(nativeConversation{}))
80}
81
82func TestTransactionLifecycle(t *testing.T) {
83 l, calls, handle := testLibrary(t)
84 expectStart(t, calls, handle, Success)
85 tx, err := l.Start("unit-service", "unit-user", NativeConversation{Function: 1})
86 require.NoError(t, err)
87 require.ErrorIs(t, l.Close(), ErrBusy)
88 calls.EXPECT().Authenticate(handle, DisallowNullAuthtok).Return(Success).Once()
89 require.NoError(t, tx.Authenticate(DisallowNullAuthtok))
90 calls.EXPECT().AcctMgmt(handle, Flags(0)).Return(Success).Once()
91 require.NoError(t, tx.AcctMgmt(0))
92 calls.EXPECT().End(handle, Success).RunAndReturn(func(_ unsafe.Pointer, _ Status) Status {
93 // Native arguments are still alive while pam_end executes.
94 require.Len(t, tx.storage, 4)
95 require.Equal(t, []byte("unit-service\x00"), unsafe.Slice((*byte)(tx.storage[0]), 13))
96 return Success
97 }).Once()
98 require.NoError(t, tx.End())
99 require.Empty(t, tx.storage)
100 require.Zero(t, l.active)
101 require.ErrorIs(t, tx.End(), ErrClosed)
102 require.ErrorIs(t, tx.Authenticate(0), ErrClosed)
103 require.ErrorIs(t, tx.AcctMgmt(0), ErrClosed)
104}
105
106func TestEveryNonSuccessFailsClosed(t *testing.T) {
107 for status := Status(1); status <= Incomplete+1; status++ {
108 for _, operation := range []string{"authenticate", "acct_mgmt"} {
109 t.Run(fmt.Sprintf("%s/%d", operation, status), func(t *testing.T) {
110 l, calls, handle := testLibrary(t)
111 expectStart(t, calls, handle, Success)
112 tx, err := l.Start("unit-service", "unit-user", NativeConversation{Function: 1})
113 require.NoError(t, err)
114 if operation == "authenticate" {
115 calls.EXPECT().Authenticate(handle, Flags(0)).Return(status).Once()
116 err = tx.Authenticate(0)
117 } else {
118 calls.EXPECT().AcctMgmt(handle, Flags(0)).Return(status).Once()
119 err = tx.AcctMgmt(0)
120 }
121 var nativeErr *Error
122 require.ErrorAs(t, err, &nativeErr)
123 require.Equal(t, status, nativeErr.Status)
124 require.Equal(t, operation, nativeErr.Operation)
125 calls.EXPECT().End(handle, status).Return(Success).Once()
126 require.NoError(t, tx.End())
127 })
128 }
129 }
130}
131
132func TestFailedStartAndEnd(t *testing.T) {
133 for _, tc := range []struct {
134 name string
135 status Status
136 output bool
137 }{
138 {"failure-with-undefined-output", ServiceErr, true},
139 {"failure-without-handle", BufErr, false},
140 {"success-without-handle", Success, false},
141 } {
142 t.Run(tc.name, func(t *testing.T) {
143 l, calls, handle := testLibrary(t)
144 var output unsafe.Pointer
145 if tc.output {
146 // Output is undefined on failure: no End expectation means
147 // attempting native cleanup with this value fails the test.
148 output = handle
149 }
150 expectStart(t, calls, output, tc.status)
151 tx, err := l.Start("unit-service", "unit-user", NativeConversation{Function: 1})
152 require.Error(t, err)
153 require.Nil(t, tx)
154 require.Zero(t, l.active)
155 if tc.output {
156 var nativeErr *Error
157 require.True(t, errors.As(err, &nativeErr))
158 require.Equal(t, tc.status, nativeErr.Status)
159 }
160 })
161 }
162 t.Run("end-error-consumes-handle", func(t *testing.T) {
163 l, calls, handle := testLibrary(t)
164 expectStart(t, calls, handle, Success)
165 tx, err := l.Start("unit-service", "unit-user", NativeConversation{Function: 1})
166 require.NoError(t, err)
167 calls.EXPECT().End(handle, Success).Return(SystemErr).Once()
168 require.Error(t, tx.End())
169 require.Empty(t, tx.storage)
170 require.Zero(t, l.active)
171 require.ErrorIs(t, tx.End(), ErrClosed)
172 })
173}
174
175func TestArgumentAndClosedValidation(t *testing.T) {
176 l, calls, handle := testLibrary(t)
177 for _, tc := range []struct {
178 service, user string
179 conv NativeConversation
180 }{
181 {"", "user", NativeConversation{Function: 1}},
182 {"bad\x00service", "user", NativeConversation{Function: 1}},
183 {"service", "bad\x00user", NativeConversation{Function: 1}},
184 {"service", "user", NativeConversation{}},
185 } {
186 _, err := l.Start(tc.service, tc.user, tc.conv)
187 require.ErrorIs(t, err, ErrInvalidArgument)
188 }
189 expectStart(t, calls, handle, Success)
190 tx, err := l.Start("unit-service", "unit-user", NativeConversation{Function: 1})
191 require.NoError(t, err)
192 require.ErrorIs(t, tx.Authenticate(Flags(0x4000)), ErrInvalidArgument)
193 require.ErrorIs(t, tx.AcctMgmt(Flags(-1)), ErrInvalidArgument)
194 calls.EXPECT().End(handle, SystemErr).Return(Success).Once()
195 require.NoError(t, tx.End())
196 var zero Library
197 _, err = zero.Start("service", "user", NativeConversation{Function: 1})
198 require.ErrorIs(t, err, ErrClosed)
199 require.ErrorIs(t, zero.Close(), ErrClosed)
200 var absent *Transaction
201 require.ErrorIs(t, absent.End(), ErrClosed)
202}
203
204func TestEmptyUserAndClosedLibrary(t *testing.T) {
205 l, calls, handle := testLibrary(t)
206 calls.EXPECT().Start(mock.Anything, unsafe.Pointer(nil), mock.Anything, mock.Anything).
207 RunAndReturn(func(_, _, _, output unsafe.Pointer) Status { *(*unsafe.Pointer)(output) = handle; return Success }).Once()
208 tx, err := l.Start("unit-service", "", NativeConversation{Function: 1})
209 require.NoError(t, err)
210 calls.EXPECT().End(handle, Success).Return(Success).Once()
211 require.NoError(t, tx.End())
212 // Close is checked separately so fixture cleanup still frees through libc.
213 other, err := Open()
214 require.NoError(t, err)
215 require.NoError(t, other.Close())
216 require.ErrorIs(t, other.Close(), ErrClosed)
217 _, err = other.Start("service", "user", NativeConversation{Function: 1})
218 require.ErrorIs(t, err, ErrClosed)
219}