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}