detached c541a90

conversation_linux.go RawHistoryFolder

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}

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