detached c541a90

pam_linux_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close