main c541a90

pam_linux.go RawHistoryFolder

1//go:build linux && (amd64 || arm64)
2
3// Package pam provides Linux-PAM binding without cgo. It is not an
4// authentication policy, credential provider, or cancellable worker.
5package pam
6
7import (
8	"errors"
9	"fmt"
10	"strings"
11	"unsafe"
12
13	"github.com/bnema/purego"
14)
15
16var (
17	ErrClosed          = errors.New("pam: library or transaction is closed")
18	ErrBusy            = errors.New("pam: active transactions prevent library close")
19	ErrInvalidArgument = errors.New("pam: invalid argument")
20)
21
22// Error preserves the native status without retaining or logging credentials.
23type Error struct {
24	Operation string
25	Status    Status
26}
27
28func (e *Error) Error() string {
29	return fmt.Sprintf("pam: %s failed (status %d)", e.Operation, e.Status)
30}
31func statusError(operation string, status Status) error {
32	if status == Success {
33		return nil
34	}
35	return &Error{Operation: operation, Status: status}
36}
37
38// Library owns loader references. The zero value is closed. Do not copy it.
39// All operations on a Library and its Transactions must be serialized by one caller/worker.
40// Native calls are synchronous, may block, and have no cancellation guarantee.
41type Library struct {
42	calls                 libraryFunctions
43	memory                nativeMemory
44	pamHandle, libcHandle uintptr
45	active                int
46	closed                bool
47}
48
49// Transaction owns a PAM handle and native argument storage until End returns.
50// Do not copy a Transaction. End must be called exactly once after successful
51// Start; it consumes the transaction even if pam_end reports an error.
52type Transaction struct {
53	library *Library
54	handle  unsafe.Pointer
55	storage []unsafe.Pointer
56	last    Status
57	ended   bool
58	bridge  *conversationState
59}
60
61// Start requires an explicit service and native conversation; user may be empty
62// (passed as NULL so PAM may prompt). It never authenticates. No service is
63// installed by this library. NativeConversation is a trusted unsafe boundary.
64func (l *Library) Start(service, user string, conversation NativeConversation) (*Transaction, error) {
65	return l.start(service, user, conversation, nil)
66}
67
68func (l *Library) start(service, user string, conversation NativeConversation, bridge *conversationState) (*Transaction, error) {
69	if l == nil || l.closed || l.calls == nil || l.memory == nil {
70		return nil, ErrClosed
71	}
72	if service == "" || strings.ContainsRune(service, '\x00') || strings.ContainsRune(user, '\x00') || conversation.Function == 0 {
73		return nil, ErrInvalidArgument
74	}
75	t := &Transaction{library: l, last: Abort, bridge: bridge}
76	keep := func(p unsafe.Pointer, err error) (unsafe.Pointer, error) {
77		if err == nil {
78			t.storage = append(t.storage, p)
79		}
80		return p, err
81	}
82	servicePtr, err := keep(nativeCString(l.memory, service))
83	if err != nil {
84		return nil, err
85	}
86	var userPtr unsafe.Pointer
87	if user != "" {
88		userPtr, err = keep(nativeCString(l.memory, user))
89		if err != nil {
90			t.release()
91			return nil, err
92		}
93	}
94	convPtr, err := keep(l.memory.alloc(unsafe.Sizeof(nativeConversation{})))
95	if err != nil {
96		t.release()
97		return nil, err
98	}
99	if bridge == nil {
100		*(*nativeConversation)(convPtr) = nativeConversation(conversation)
101	} else {
102		// Write opaque cookie bits without converting them to a Go pointer.
103		*(*nativeCookieConversation)(convPtr) = nativeCookieConversation{Function: conversation.Function, Cookie: bridge.token}
104	}
105	output, err := keep(l.memory.alloc(unsafe.Sizeof(uintptr(0))))
106	if err != nil {
107		t.release()
108		return nil, err
109	}
110	status := l.calls.Start(servicePtr, userPtr, convPtr, output)
111	if status != Success {
112		// pam_start(3): output is undefined on failure. Never read it or
113		// call pam_end with it; libpam owns its failed-start cleanup.
114		t.release()
115		return nil, statusError("start", status)
116	}
117	t.handle = *(*unsafe.Pointer)(output)
118	if t.handle == nil {
119		t.release()
120		return nil, errors.New("pam: start succeeded with a NULL handle")
121	}
122	t.last = Success
123	l.active++
124	return t, nil
125}
126
127func (t *Transaction) valid() bool {
128	return t != nil && !t.ended && t.library != nil && t.handle != nil
129}
130
131// Authenticate returns an error for every status other than PAM_SUCCESS.
132// Success here is NOT an account authorization decision; call AcctMgmt too.
133func (t *Transaction) Authenticate(flags Flags) error {
134	if !t.valid() {
135		return ErrClosed
136	}
137	if flags & ^(Silent|DisallowNullAuthtok) != 0 {
138		t.last = SystemErr
139		return ErrInvalidArgument
140	}
141	t.last = t.library.calls.Authenticate(t.handle, flags)
142	return statusError("authenticate", t.last)
143}
144
145// AcctMgmt fails closed, including PAM_NEW_AUTHTOK_REQD and PAM_INCOMPLETE.
146func (t *Transaction) AcctMgmt(flags Flags) error {
147	if !t.valid() {
148		return ErrClosed
149	}
150	if flags & ^(Silent|DisallowNullAuthtok) != 0 {
151		t.last = SystemErr
152		return ErrInvalidArgument
153	}
154	t.last = t.library.calls.AcctMgmt(t.handle, flags)
155	return statusError("acct_mgmt", t.last)
156}
157
158// End passes the last operation's status to pam_end and releases native storage
159// only after that call returns. A second End returns ErrClosed, never calls C.
160func (t *Transaction) End() error {
161	if !t.valid() {
162		return ErrClosed
163	}
164	t.ended = true
165	status := t.library.calls.End(t.handle, t.last)
166	t.handle = nil
167	t.release()
168	t.library.active--
169	return statusError("end", status)
170}
171
172func (t *Transaction) release() {
173	if t.bridge != nil {
174		t.bridge.retire()
175		t.bridge = nil
176	}
177	for i := len(t.storage) - 1; i >= 0; i-- {
178		t.library.memory.free(t.storage[i])
179	}
180	t.storage = nil
181}
182
183// Close refuses to unload symbols still needed by live transactions.
184func (l *Library) Close() error {
185	if l == nil || l.closed || l.calls == nil {
186		return ErrClosed
187	}
188	if l.active != 0 {
189		return ErrBusy
190	}
191	l.closed = true
192	var err error
193	if l.pamHandle != 0 {
194		err = errorsWithClose(err, l.pamHandle)
195	}
196	if l.libcHandle != 0 {
197		err = errorsWithClose(err, l.libcHandle)
198	}
199	return err
200}
201
202func errorsWithClose(err error, handle uintptr) error {
203	return errors.Join(err, purego.Dlclose(handle))
204}

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