1//go:build linux && (amd64 || arm64)
2
3package pam
4
5import (
6 "errors"
7 "sync"
8 "unsafe"
9
10 "github.com/bnema/purego"
11)
12
13// Message is a copied Linux-PAM message. Text may be sensitive and is wiped
14// after Respond returns. Do not retain it; do not log prompts or answers.
15type Message struct {
16 Style int32
17 Text []byte
18}
19
20// Conversation supplies responses without retaining native or Go buffers.
21// Respond receives a zeroed MaxRespSize-byte answer buffer. Write the answer
22// there and return its length (0..MaxRespSize), without a NUL. For informational
23// messages return zero. The library wipes Text and answer even on error/panic.
24// Do not retain buffers or access them after return, or call Library/Transaction
25// methods from Respond. Respond must return: no timeout cancels a native call.
26// Calls for one conversation are serialized, including foreign-thread entry.
27// Any private secret copies made by the handler remain its responsibility.
28type Conversation interface {
29 Respond(message Message, answer []byte) (int, error)
30}
31
32// nativeMemory is the allocator seam for callback failure tests.
33type nativeMemory interface {
34 alloc(size uintptr) (unsafe.Pointer, error)
35 free(pointer unsafe.Pointer)
36}
37
38type conversationState struct {
39 token uintptr
40 handler Conversation
41 memory nativeMemory
42 serial sync.Mutex
43 refs int // guarded by conversations.Mutex
44}
45
46var conversations = struct {
47 sync.Mutex
48 next uintptr
49 states map[uintptr]*conversationState
50}{states: make(map[uintptr]*conversationState)}
51var conversationChanged = sync.NewCond(&conversations.Mutex)
52var sharedCallback struct {
53 sync.Once
54 address uintptr
55 err error
56}
57
58// The shared callback is never unreferenced: late calls can safely reject an
59// unknown cookie without reading any appdata, message or output memory.
60func callbackAddress() (uintptr, error) {
61 sharedCallback.Do(func() {
62 defer func() {
63 if recover() != nil {
64 sharedCallback.err = errors.New("pam: callback registration failed")
65 }
66 }()
67 sharedCallback.address = purego.NewCallbackInts(conversationCallback)
68 })
69 return sharedCallback.address, sharedCallback.err
70}
71
72func registerConversation(handler Conversation, memory nativeMemory) (*conversationState, error) {
73 conversations.Lock()
74 defer conversations.Unlock()
75 if conversations.next == ^uintptr(0) {
76 return nil, errors.New("pam: conversation cookie space exhausted")
77 }
78 conversations.next++ // never reuse a cookie, including after failed Start
79 s := &conversationState{token: conversations.next, handler: handler, memory: memory}
80 conversations.states[s.token] = s
81 return s, nil
82}
83
84func acquireConversation(token uintptr) *conversationState {
85 conversations.Lock()
86 defer conversations.Unlock()
87 s := conversations.states[token]
88 if s != nil {
89 s.refs++
90 }
91 return s
92}
93
94func (s *conversationState) leave() {
95 conversations.Lock()
96 s.refs--
97 conversationChanged.Broadcast()
98 conversations.Unlock()
99}
100
101func (s *conversationState) retire() {
102 conversations.Lock()
103 delete(conversations.states, s.token) // close admission before waiting
104 for s.refs != 0 {
105 conversationChanged.Wait()
106 }
107 conversations.Unlock()
108}
109
110// StartConversation adds the managed Go bridge; the external NativeConversation
111// API is unchanged. appdata is a numeric opaque void* cookie, NOT a pointer to
112// memory, and neither PAM nor the bridge may dereference it.
113func (l *Library) StartConversation(service, user string, handler Conversation) (*Transaction, error) {
114 if l == nil || l.closed || l.calls == nil || l.memory == nil {
115 return nil, ErrClosed
116 }
117 if handler == nil {
118 return nil, ErrInvalidArgument
119 }
120 address, err := callbackAddress()
121 if err != nil {
122 return nil, err
123 }
124 s, err := registerConversation(handler, l.memory)
125 if err != nil {
126 return nil, err
127 }
128 t, err := l.start(service, user, NativeConversation{Function: address}, s)
129 if err != nil {
130 s.retire()
131 }
132 return t, err
133}
134
135// Raw integer-class entry keeps all native address bits out of Go pointer
136// values until cookie admission. C int count uses only the low 32 bits.
137func conversationCallback(args *purego.CallbackArgs) (result uintptr) {
138 result = uintptr(ConvErr)
139 defer func() {
140 if recover() != nil {
141 result = uintptr(ConvErr)
142 }
143 }()
144 count, cookie := int32(args.Int(0)), args.Int(3)
145 s := acquireConversation(cookie)
146 if s == nil {
147 return result
148 }
149 defer s.leave()
150 messages, output := args.Int(1), args.Int(2)
151 if count < 1 || count > MaxNumMsg || messages == 0 || output == 0 ||
152 messages%unsafe.Alignof(uintptr(0)) != 0 || output%unsafe.Alignof(uintptr(0)) != 0 {
153 return result
154 }
155 return uintptr(uint32(s.respondNative(count, admittedNativeAddress(messages), admittedNativeAddress(output))))
156}
157
158// admittedNativeAddress reinterprets native address bits only after admission
159// and basic validation. This is not an address-validity guarantee; the admitted
160// native caller must supply readable/writable memory throughout the call.
161func admittedNativeAddress(bits uintptr) unsafe.Pointer {
162 return *(*unsafe.Pointer)(unsafe.Pointer(&bits))
163}
164
165func (s *conversationState) respondNative(count int32, messages, output unsafe.Pointer) (status Status) {
166 status = ConvErr
167 s.serial.Lock()
168 defer s.serial.Unlock()
169 var responses unsafe.Pointer
170 var lengths []int
171 transferred := false
172 defer func() {
173 if recover() != nil {
174 status = ConvErr
175 } // never expose panic payload
176 if responses != nil && !transferred {
177 s.cleanupResponses(responses, lengths)
178 }
179 }()
180 if count < 1 || count > MaxNumMsg || messages == nil || output == nil {
181 return status
182 }
183 // Trusted native caller must supply readable arrays and writable output;
184 // NULL checks and bounded reads cannot validate arbitrary native addresses.
185 if uintptr(messages)%unsafe.Alignof(uintptr(0)) != 0 || uintptr(output)%unsafe.Alignof(uintptr(0)) != 0 {
186 return status
187 }
188 *(*unsafe.Pointer)(output) = nil
189 lengths = make([]int, int(count))
190 var err error
191 responses, err = s.memory.alloc(uintptr(count) * unsafe.Sizeof(nativeResponse{}))
192 if err != nil {
193 return BufErr
194 }
195 array := unsafe.Slice((*nativeResponse)(responses), int(count))
196 pointers := unsafe.Slice((*unsafe.Pointer)(messages), int(count))
197 for i, pointer := range pointers {
198 if pointer == nil || uintptr(pointer)%unsafe.Alignof(nativeMessage{}) != 0 {
199 return ConvErr
200 }
201 message := *(*nativeMessage)(pointer)
202 if message.Style != PromptEchoOff && message.Style != PromptEchoOn && message.Style != ErrorMsg && message.Style != TextInfo {
203 return ConvErr
204 }
205 text, ok := copyMessage(message.Text)
206 if !ok {
207 return ConvErr
208 }
209 answer, n, err := respond(s.handler, Message{Style: message.Style, Text: text})
210 if err != nil {
211 return ConvErr
212 }
213 // respond's caller owns wiping the returned answer after native copy.
214 result := func() Status {
215 defer clear(answer)
216 if message.Style == ErrorMsg || message.Style == TextInfo {
217 if n != 0 {
218 return ConvErr
219 }
220 return Success
221 }
222 p, err := s.memory.alloc(uintptr(n + 1))
223 if err != nil {
224 return BufErr
225 }
226 copy(unsafe.Slice((*byte)(p), n+1), answer[:n])
227 array[i].Text = p
228 lengths[i] = n
229 return Success
230 }()
231 if result != Success {
232 return result
233 }
234 }
235 *(*unsafe.Pointer)(output) = responses
236 transferred = true // PAM/module now owns array and response strings via free
237 return Success
238}
239
240// Schedule every free before executing any, so a panicking allocator seam's
241// free cannot prevent release of the other known native allocations. The raw
242// entry catches cleanup panics without exposing their payload.
243func (s *conversationState) cleanupResponses(responses unsafe.Pointer, lengths []int) {
244 defer s.memory.free(responses)
245 for i, response := range unsafe.Slice((*nativeResponse)(responses), len(lengths)) {
246 if response.Text != nil {
247 defer func(pointer unsafe.Pointer, size int) {
248 defer s.memory.free(pointer)
249 clear(unsafe.Slice((*byte)(pointer), size))
250 }(response.Text, lengths[i]+1)
251 }
252 }
253}
254
255func copyMessage(pointer unsafe.Pointer) ([]byte, bool) {
256 if pointer == nil {
257 return nil, false
258 }
259 // Read one byte at a time; do not form/read a whole maximum-length slice
260 // beyond a short string. Caller still must provide readable native memory.
261 for n := 0; n < MaxMsgSize; n++ {
262 if *(*byte)(unsafe.Add(pointer, n)) == 0 {
263 return append([]byte(nil), unsafe.Slice((*byte)(pointer), n)...), true
264 }
265 }
266 return nil, false
267}
268
269func respond(handler Conversation, message Message) (answer []byte, n int, err error) {
270 answer = make([]byte, MaxRespSize)
271 defer clear(message.Text)
272 defer func() {
273 if recover() != nil {
274 err = errors.New("pam: conversation handler failed")
275 }
276 if err != nil {
277 clear(answer)
278 }
279 }()
280 n, err = handler.Respond(message, answer)
281 if err == nil {
282 if n < 0 || n > len(answer) {
283 err = ErrInvalidArgument
284 } else {
285 for _, b := range answer[:n] {
286 if b == 0 {
287 err = ErrInvalidArgument
288 break
289 }
290 }
291 }
292 }
293 return
294}