1
2/*
3 * Licensed Materials - Property of IBM
4 *
5 * trousers - An open source TCG Software Stack
6 *
7 * (C) Copyright International Business Machines Corp. 2004-2006
8 *
9 */
10
11#include <stdio.h>
12#include <string.h>
13#include <stdlib.h>
14#include <limits.h>
15
16#include "trousers/tss.h"
17#include "trousers_types.h"
18#include "tcs_tsp.h"
19#include "tcs_utils.h"
20#include "tcs_int_literals.h"
21#include "capabilities.h"
22#include "tcslog.h"
23#include "tcsd_wrap.h"
24#include "tcsd.h"
25#include "auth_mgr.h"
26#include "req_mgr.h"
27
28
29MUTEX_DECLARE_EXTERN(tcsp_lock);
30
31/* Note: The after taking the auth_mgr_lock in any of the functions below, the
32 * mem_cache_lock cannot be taken without risking a deadlock. So, the auth_mgr
33 * functions must be "self-contained" wrt locking */
34
35/* no locking done in init since its called by only a single thread */
36TSS_RESULT
37auth_mgr_init()
38{
39	memset(&auth_mgr, 0, sizeof(struct _auth_mgr));
40
41	auth_mgr.max_auth_sessions = tpm_metrics.num_auths;
42
43	auth_mgr.overflow = calloc(TSS_DEFAULT_OVERFLOW_AUTHS, sizeof(COND_VAR *));
44	if (auth_mgr.overflow == NULL) {
45		LogError("malloc of %zd bytes failed",
46			 (TSS_DEFAULT_OVERFLOW_AUTHS * sizeof(COND_VAR *)));
47		return TCSERR(TSS_E_OUTOFMEMORY);
48	}
49	auth_mgr.overflow_size = TSS_DEFAULT_OVERFLOW_AUTHS;
50
51	auth_mgr.auth_mapper = calloc(TSS_DEFAULT_AUTH_TABLE_SIZE, sizeof(struct auth_map));
52	if (auth_mgr.auth_mapper == NULL) {
53		LogError("malloc of %zd bytes failed",
54			 (TSS_DEFAULT_AUTH_TABLE_SIZE * sizeof(struct auth_map)));
55		return TCSERR(TSS_E_OUTOFMEMORY);
56	}
57	auth_mgr.auth_mapper_size = TSS_DEFAULT_AUTH_TABLE_SIZE;
58
59	return TSS_SUCCESS;
60}
61
62TSS_RESULT
63auth_mgr_final()
64{
65	UINT32 i;
66
67	/* wake up any sleeping threads, so they can be joined */
68	for (i = 0; i < auth_mgr.overflow_size; i++) {
69		if (auth_mgr.overflow[i] != NULL)
70			COND_SIGNAL(auth_mgr.overflow[i]);
71	}
72
73	free(auth_mgr.overflow);
74	free(auth_mgr.auth_mapper);
75
76	return TSS_SUCCESS;
77}
78
79TSS_RESULT
80auth_mgr_save_ctx(TCS_CONTEXT_HANDLE hContext)
81{
82       TSS_RESULT result = TSS_SUCCESS;
83	UINT32 i;
84
85	for (i = 0; i < auth_mgr.auth_mapper_size; i++) {
86		if (auth_mgr.auth_mapper[i].full == TRUE &&
87		    auth_mgr.auth_mapper[i].swap == NULL &&
88		    auth_mgr.auth_mapper[i].tcs_ctx != hContext) {
89			LogDebug("Calling TPM_SaveAuthContext for TCS CTX %x. Swapping out: TCS %x "
90				 "TPM %x", hContext, auth_mgr.auth_mapper[i].tcs_ctx,
91				 auth_mgr.auth_mapper[i].tpm_handle);
92
93			if ((result = TPM_SaveAuthContext(auth_mgr.auth_mapper[i].tpm_handle,
94							  &auth_mgr.auth_mapper[i].swap_size,
95							  &auth_mgr.auth_mapper[i].swap))) {
96				LogDebug("TPM_SaveAuthContext failed: 0x%x", result);
97				return result;
98			}
99                       break;
100		}
101	}
102
103       return result;
104}
105
106/* if there's a TCS context waiting to get auth, wake it up or swap it in */
107void
108auth_mgr_swap_in()
109{
110	if (auth_mgr.overflow[auth_mgr.of_tail] != NULL) {
111		LogDebug("waking up thread %lddd, auth slot has opened", THREAD_ID);
112		/* wake up the next sleeping thread in order and increment tail */
113		COND_SIGNAL(auth_mgr.overflow[auth_mgr.of_tail]);
114		auth_mgr.overflow[auth_mgr.of_tail] = NULL;
115		auth_mgr.of_tail = (auth_mgr.of_tail + 1) % auth_mgr.overflow_size;
116	} else {
117		/* else nobody needs to be swapped in, so continue */
118		LogDebug("no threads need to be signaled.");
119	}
120}
121
122/* we need to swap out an auth context or add a waiting context to the overflow queue */
123TSS_RESULT
124auth_mgr_swap_out(TCS_CONTEXT_HANDLE hContext)
125{
126	COND_VAR *cond;
127
128	/* If the TPM can do swapping and it succeeds, return, else cond wait below */
129	if (tpm_metrics.authctx_swap && !auth_mgr_save_ctx(hContext))
130		return TSS_SUCCESS;
131
132	if ((cond = ctx_get_cond_var(hContext)) == NULL) {
133		LogError("Auth swap variable not found for TCS context 0x%x", hContext);
134		return TCSERR(TSS_E_INTERNAL_ERROR);
135	}
136
137	/* Test whether we are the last awake thread.  If we are, we can't go to sleep
138	 * since then there'd be no worker thread to wake the others up. This situation
139	 * can arise when we're on a busy system who's TPM doesn't support auth ctx
140	 * swapping.
141	 */
142	if (auth_mgr.sleeping_threads == (tcsd_options.num_threads - 1)) {
143		LogError("auth mgr failing: too many threads already waiting");
144		LogTPMERR(TCPA_E_RESOURCES, __FILE__, __LINE__);
145		return TCPA_E_RESOURCES;
146	}
147
148	if (auth_mgr.overflow[auth_mgr.of_head] == NULL) {
149		auth_mgr.overflow[auth_mgr.of_head] = cond;
150		auth_mgr.of_head = (auth_mgr.of_head + 1) % auth_mgr.overflow_size;
151		/* go to sleep */
152		LogDebug("thread %lddd going to sleep until auth slot opens", THREAD_ID);
153		auth_mgr.sleeping_threads++;
154		COND_WAIT(cond, &tcsp_lock);
155		auth_mgr.sleeping_threads--;
156	} else if (auth_mgr.overflow_size + TSS_DEFAULT_OVERFLOW_AUTHS < UINT_MAX) {
157		COND_VAR **tmp = auth_mgr.overflow;
158
159		LogDebugFn("Table of sleeping threads is full (%hu), growing to %hu entries",
160			   auth_mgr.sleeping_threads,
161			   auth_mgr.overflow_size + TSS_DEFAULT_OVERFLOW_AUTHS);
162
163		auth_mgr.overflow = calloc(auth_mgr.overflow_size + TSS_DEFAULT_OVERFLOW_AUTHS,
164					   sizeof(COND_VAR *));
165		if (auth_mgr.overflow == NULL) {
166			LogDebugFn("malloc of %zd bytes failed",
167				   (auth_mgr.overflow_size + TSS_DEFAULT_OVERFLOW_AUTHS) *
168				   sizeof(COND_VAR *));
169			auth_mgr.overflow = tmp;
170			return TCSERR(TSS_E_OUTOFMEMORY);
171		}
172		auth_mgr.overflow_size += TSS_DEFAULT_OVERFLOW_AUTHS;
173
174		LogDebugFn("Success.");
175		memcpy(auth_mgr.overflow, tmp, auth_mgr.sleeping_threads * sizeof(COND_VAR *));
176		free(tmp);
177
178		/* XXX This could temporarily wake threads up out of order */
179		auth_mgr.of_head = auth_mgr.sleeping_threads;
180		auth_mgr.of_tail = 0;
181		auth_mgr.overflow[auth_mgr.of_head] = cond;
182		auth_mgr.of_head = (auth_mgr.of_head + 1) % auth_mgr.overflow_size;
183		LogDebug("thread %lddd going to sleep until auth slot opens", THREAD_ID);
184		auth_mgr.sleeping_threads++;
185		COND_WAIT(cond, &tcsp_lock);
186		auth_mgr.sleeping_threads--;
187	} else {
188		LogError("Auth table overflow is full (%u entries), some will fail.",
189			 auth_mgr.overflow_size);
190		return TCSERR(TSS_E_INTERNAL_ERROR);
191	}
192
193	return TSS_SUCCESS;
194}
195
196/* close all auth contexts associated with this TCS_CONTEXT_HANDLE */
197TSS_RESULT
198auth_mgr_close_context(TCS_CONTEXT_HANDLE tcs_handle)
199{
200	UINT32 i;
201	TSS_RESULT result;
202
203	for (i = 0; i < auth_mgr.auth_mapper_size; i++) {
204		if (auth_mgr.auth_mapper[i].full == TRUE &&
205		    auth_mgr.auth_mapper[i].tcs_ctx == tcs_handle) {
206			if (auth_mgr.auth_mapper[i].swap) {
207				/* This context is swapped out of the TPM, so we can just free the
208				 * blob */
209				free(auth_mgr.auth_mapper[i].swap);
210				auth_mgr.auth_mapper[i].swap = NULL;
211				auth_mgr.auth_mapper[i].swap_size = 0;
212			} else {
213				result = TCSP_FlushSpecific_Common(auth_mgr.auth_mapper[i].tpm_handle,
214								   TPM_RT_AUTH);
215
216				/* Ok, probably dealing with a 1.1 TPM */
217				if (result == TPM_E_BAD_ORDINAL)
218                                       result = internal_TerminateHandle(
219                                           auth_mgr.auth_mapper[i].tpm_handle);
220
221				if (result == TCPA_E_INVALID_AUTHHANDLE) {
222					LogDebug("Tried to close an invalid auth handle: %x",
223						 auth_mgr.auth_mapper[i].tpm_handle);
224				} else if (result != TCPA_SUCCESS) {
225					LogDebug("TPM_TerminateHandle returned %d", result);
226				}
227			}
228                       /* clear the slot */
229			auth_mgr.open_auth_sessions--;
230			auth_mgr.auth_mapper[i].full = FALSE;
231                       auth_mgr.auth_mapper[i].tpm_handle = 0;
232                       auth_mgr.auth_mapper[i].tcs_ctx = 0;
233			LogDebug("released auth for TCS %x TPM %x", tcs_handle,
234                               auth_mgr.auth_mapper[i].tpm_handle);
235
236			auth_mgr_swap_in();
237		}
238	}
239
240	return TSS_SUCCESS;
241}
242
243void
244auth_mgr_release_auth(TPM_AUTH *auth0, TPM_AUTH *auth1, TCS_CONTEXT_HANDLE tcs_handle)
245{
246	if (auth0)
247		auth_mgr_release_auth_handle(auth0->AuthHandle, tcs_handle,
248					     auth0->fContinueAuthSession);
249
250	if (auth1)
251		auth_mgr_release_auth_handle(auth1->AuthHandle, tcs_handle,
252					     auth1->fContinueAuthSession);
253}
254
255/* unload the auth ctx associated with this auth handle */
256TSS_RESULT
257auth_mgr_release_auth_handle(TCS_AUTHHANDLE tpm_auth_handle, TCS_CONTEXT_HANDLE tcs_handle,
258			     TSS_BOOL cont)
259{
260	UINT32 i;
261	TSS_RESULT result = TSS_SUCCESS;
262
263	for (i = 0; i < auth_mgr.auth_mapper_size; i++) {
264		if (auth_mgr.auth_mapper[i].full == TRUE &&
265		    auth_mgr.auth_mapper[i].tpm_handle == tpm_auth_handle &&
266		    auth_mgr.auth_mapper[i].tcs_ctx == tcs_handle) {
267			if (!cont) {
268                               /*
269                                * This function should not be necessary, but
270                                * if the main operation resulted in an error,
271                                * the TPM may still hold the auth handle
272                                * and it must be freed.  Most of the time
273                                * this call will result in TPM_E_INVALID_AUTHHANDLE
274                                * error which can be ignored.
275                                */
276                               result = TCSP_FlushSpecific_Common(
277                                   auth_mgr.auth_mapper[i].tpm_handle,
278                                   TPM_RT_AUTH);
279
280				/* Ok, probably dealing with a 1.1 TPM */
281				if (result == TPM_E_BAD_ORDINAL)
282                                       result = internal_TerminateHandle(
283                                           auth_mgr.auth_mapper[i].tpm_handle);
284
285				if (result == TCPA_E_INVALID_AUTHHANDLE) {
286					LogDebug("Tried to close an invalid auth handle: %x",
287						 auth_mgr.auth_mapper[i].tpm_handle);
288				} else if (result != TCPA_SUCCESS) {
289					LogDebug("TPM_TerminateHandle returned %d", result);
290				}
291
292                               if (result == TPM_SUCCESS) {
293                                       LogDebug("released auth for TCS %x TPM %x",
294                                                auth_mgr.auth_mapper[i].tcs_ctx, tpm_auth_handle);
295                               }
296                               /*
297                                * Mark it as released, the "cont" flag indicates
298                                * that it is no longer needed.
299                                */
300                               auth_mgr.open_auth_sessions--;
301                               auth_mgr.auth_mapper[i].full = FALSE;
302                               auth_mgr.auth_mapper[i].tpm_handle = 0;
303                               auth_mgr.auth_mapper[i].tcs_ctx = 0;
304                               auth_mgr_swap_in();
305			}
306                       /* If the cont flag is TRUE, we have to keep the handle */
307		}
308	}
309
310	return result;
311}
312
313TSS_RESULT
314auth_mgr_check(TCS_CONTEXT_HANDLE tcsContext, TPM_AUTHHANDLE *tpm_auth_handle)
315{
316	UINT32 i;
317	TSS_RESULT result = TSS_SUCCESS;
318
319	for (i = 0; i < auth_mgr.auth_mapper_size; i++) {
320		if (auth_mgr.auth_mapper[i].full == TRUE &&
321		    auth_mgr.auth_mapper[i].tpm_handle == *tpm_auth_handle &&
322		    auth_mgr.auth_mapper[i].tcs_ctx == tcsContext) {
323			/* We have a record of this session, now swap it into the TPM if need be. */
324			if (auth_mgr.auth_mapper[i].swap) {
325				LogDebugFn("TPM_LoadAuthContext for TCS %x TPM %x", tcsContext,
326					   auth_mgr.auth_mapper[i].tpm_handle);
327
328				result = TPM_LoadAuthContext(auth_mgr.auth_mapper[i].swap_size,
329							     auth_mgr.auth_mapper[i].swap,
330							     tpm_auth_handle);
331				if (result == TSS_SUCCESS) {
332					free(auth_mgr.auth_mapper[i].swap);
333					auth_mgr.auth_mapper[i].swap = NULL;
334					auth_mgr.auth_mapper[i].swap_size = 0;
335
336					LogDebugFn("TPM_LoadAuthContext succeeded. Old TPM: %x, New"
337						   " TPM: %x", auth_mgr.auth_mapper[i].tpm_handle,
338						   *tpm_auth_handle);
339
340					auth_mgr.auth_mapper[i].tpm_handle = *tpm_auth_handle;
341				} else if (result == TPM_E_RESOURCES) {
342					if ((result = auth_mgr_swap_out(tcsContext))) {
343						LogDebugFn("TPM_LoadAuthContext failed with TPM_E_R"
344							   "ESOURCES and swapping out failed, retur"
345							   "ning error");
346						return result;
347					}
348
349					LogDebugFn("Retrying TPM_LoadAuthContext after swap"
350						   " out...");
351					result =
352					      TPM_LoadAuthContext(auth_mgr.auth_mapper[i].swap_size,
353								  auth_mgr.auth_mapper[i].swap,
354								  tpm_auth_handle);
355					free(auth_mgr.auth_mapper[i].swap);
356					auth_mgr.auth_mapper[i].swap = NULL;
357					auth_mgr.auth_mapper[i].swap_size = 0;
358				} else {
359					LogDebug("TPM_LoadAuthContext failed: 0x%x.", result);
360				}
361			}
362
363			return result;
364		}
365	}
366
367	LogDebugFn("Can't find auth for TCS handle %x, should be %x", tcsContext, *tpm_auth_handle);
368	return TCSERR(TSS_E_INTERNAL_ERROR);
369}
370
371TSS_RESULT
372auth_mgr_add(TCS_CONTEXT_HANDLE tcsContext, TCS_AUTHHANDLE tpm_auth_handle)
373{
374	struct auth_map *tmp = auth_mgr.auth_mapper;
375	UINT32 i;
376
377	for (i = 0; i < auth_mgr.auth_mapper_size; i++) {
378		if (auth_mgr.auth_mapper[i].full == FALSE) {
379			auth_mgr.auth_mapper[i].tpm_handle = tpm_auth_handle;
380			auth_mgr.auth_mapper[i].tcs_ctx = tcsContext;
381			auth_mgr.auth_mapper[i].full = TRUE;
382			auth_mgr.open_auth_sessions++;
383			LogDebug("added auth for TCS %x TPM %x", tcsContext, tpm_auth_handle);
384
385			return TSS_SUCCESS;
386		}
387	}
388
389
390	LogDebugFn("Thread %ld growing the auth table to %u entries", THREAD_ID,
391		   auth_mgr.auth_mapper_size + TSS_DEFAULT_AUTH_TABLE_SIZE);
392
393	auth_mgr.auth_mapper = calloc(auth_mgr.auth_mapper_size +
394				      TSS_DEFAULT_AUTH_TABLE_SIZE, sizeof(struct auth_map));
395	if (auth_mgr.auth_mapper == NULL) {
396		auth_mgr.auth_mapper = tmp;
397		return TCSERR(TSS_E_OUTOFMEMORY);
398	} else {
399		memcpy(auth_mgr.auth_mapper, tmp,
400				auth_mgr.auth_mapper_size * sizeof(struct auth_map));
401		auth_mgr.auth_mapper_size += TSS_DEFAULT_AUTH_TABLE_SIZE;
402		LogDebugFn("Success.");
403		return TSS_SUCCESS;
404	}
405}
406
407/* A thread wants a new OIAP or OSAP session with the TPM. Returning TRUE indicates that we should
408 * allow it to open the session, FALSE to indicate that the request should be queued or have
409 * another thread's session swapped out to make room for it.
410 */
411TSS_BOOL
412auth_mgr_req_new(TCS_CONTEXT_HANDLE hContext)
413{
414	UINT32 i, opened = 0;
415
416	for (i = 0; i < auth_mgr.auth_mapper_size; i++) {
417		if (auth_mgr.auth_mapper[i].full == TRUE &&
418		    auth_mgr.auth_mapper[i].tcs_ctx  == hContext) {
419			opened++;
420		}
421	}
422
423	/* If this TSP has already opened its max open auth handles, deny another open */
424	if (opened >= MAX(2, (UINT32)auth_mgr.max_auth_sessions/2)) {
425		LogDebug("Max opened auth handles already opened.");
426		return FALSE;
427	}
428
429	/* if we have one opened already and there's a slot available, ok */
430	if (opened && ((auth_mgr.max_auth_sessions - auth_mgr.open_auth_sessions) >= 1))
431		return TRUE;
432
433	/* we don't already have one open and there are at least 2 slots left */
434	if ((auth_mgr.max_auth_sessions - auth_mgr.open_auth_sessions) >= 2)
435		return TRUE;
436
437	LogDebug("Request for new auth handle denied by TCS. (%d opened sessions)", opened);
438
439	return FALSE;
440}
441
442TSS_RESULT
443auth_mgr_oiap(TCS_CONTEXT_HANDLE hContext,	/* in */
444	      TCS_AUTHHANDLE *authHandle,	/* out */
445	      TCPA_NONCE *nonce0)		/* out */
446{
447	TSS_RESULT result;
448
449	/* are the maximum number of auth sessions open? */
450	if (auth_mgr_req_new(hContext) == FALSE) {
451		if ((result = auth_mgr_swap_out(hContext)))
452			goto done;
453	}
454
455	if ((result = TCSP_OIAP_Internal(hContext, authHandle, nonce0)))
456		goto done;
457
458	/* success, add an entry to the table */
459	result = auth_mgr_add(hContext, *authHandle);
460done:
461	return result;
462}
463
464TSS_RESULT
465auth_mgr_osap(TCS_CONTEXT_HANDLE hContext,	/* in */
466	      TCPA_ENTITY_TYPE entityType,	/* in */
467	      UINT32 entityValue,		/* in */
468	      TCPA_NONCE nonceOddOSAP,		/* in */
469	      TCS_AUTHHANDLE *authHandle,	/* out */
470	      TCPA_NONCE *nonceEven,		/* out */
471	      TCPA_NONCE *nonceEvenOSAP)	/* out */
472{
473	TSS_RESULT result;
474	UINT32 newEntValue = 0;
475
476	/* if ET is not KEYHANDLE or KEY, newEntValue is a don't care */
477	if (entityType == TCPA_ET_KEYHANDLE || entityType == TCPA_ET_KEY) {
478		if (ensureKeyIsLoaded(hContext, entityValue, &newEntValue))
479			return TCSERR(TSS_E_FAIL);
480	} else {
481		newEntValue = entityValue;
482	}
483
484	/* are the maximum number of auth sessions open? */
485	if (auth_mgr_req_new(hContext) == FALSE) {
486		if ((result = auth_mgr_swap_out(hContext)))
487			goto done;
488	}
489
490	if ((result = TCSP_OSAP_Internal(hContext, entityType, newEntValue, nonceOddOSAP,
491					 authHandle, nonceEven, nonceEvenOSAP)))
492		goto done;
493
494	/* success, add an entry to the table */
495	result = auth_mgr_add(hContext, *authHandle);
496done:
497	return result;
498}
499
500/* This function is only called directly from the TSP, we use other internal routines to free
501 * our handles. */
502TSS_RESULT
503TCSP_TerminateHandle_Internal(TCS_CONTEXT_HANDLE hContext,	/* in */
504			      TCS_AUTHHANDLE handle)	/* in */
505{
506	TSS_RESULT result;
507
508	LogDebug("Entering TCSI_TerminateHandle");
509	if ((result = ctx_verify_context(hContext)))
510		return result;
511
512	if ((result = auth_mgr_check(hContext, &handle)))
513		return result;
514
515	result = auth_mgr_release_auth_handle(handle, hContext, TRUE);
516
517	LogResult("Terminate Handle", result);
518	return result;
519}
520
521TSS_RESULT
522TPM_SaveAuthContext(TPM_AUTHHANDLE handle, UINT32 *size, BYTE **blob)
523{
524	UINT64 offset;
525	UINT32 trash, bsize;
526	TSS_RESULT result;
527	BYTE txBlob[TSS_TPM_TXBLOB_SIZE];
528
529	offset = 10;
530	LoadBlob_UINT32(&offset, handle, txBlob);
531	LoadBlob_Header(TPM_TAG_RQU_COMMAND, offset, TPM_ORD_SaveAuthContext, txBlob);
532
533	if ((result = req_mgr_submit_req(txBlob)))
534		return result;
535
536	result = UnloadBlob_Header(txBlob, &trash);
537
538	LogDebug("total packet size received from TPM: %u", trash);
539
540	if (!result) {
541		offset = 10;
542		UnloadBlob_UINT32(&offset, &bsize, txBlob);
543
544		LogDebug("Reported blob size from TPM: %u", bsize);
545
546		*blob = malloc(bsize);
547		if (*blob == NULL) {
548			LogError("malloc of %u bytes failed.", bsize);
549			return TCSERR(TSS_E_OUTOFMEMORY);
550		}
551		UnloadBlob(&offset, bsize, txBlob, *blob);
552		*size = bsize;
553	}
554
555	return result;
556}
557
558TSS_RESULT
559TPM_LoadAuthContext(UINT32 size, BYTE *blob, TPM_AUTHHANDLE *handle)
560{
561	UINT64 offset;
562	UINT32 trash;
563	TSS_RESULT result;
564	BYTE txBlob[TSS_TPM_TXBLOB_SIZE];
565
566	LogDebugFn("Loading %u byte auth blob back into TPM", size);
567
568	offset = 10;
569	LoadBlob_UINT32(&offset, size, txBlob);
570	LoadBlob(&offset, size, txBlob, blob);
571	LoadBlob_Header(TPM_TAG_RQU_COMMAND, offset, TPM_ORD_LoadAuthContext, txBlob);
572
573	if ((result = req_mgr_submit_req(txBlob)))
574		return result;
575
576	result = UnloadBlob_Header(txBlob, &trash);
577
578	if (!result) {
579		offset = 10;
580		UnloadBlob_UINT32(&offset, handle, txBlob);
581	}
582
583	return result;
584}
585