dd653e88bf78b1b30c812d6cf5a2bfb66f6abfa3
[reactos.git] / reactos / ntoskrnl / ps / create.c
1 /* $Id: create.c,v 1.72 2004/03/19 12:45:07 ekohl Exp $
2 *
3 * COPYRIGHT: See COPYING in the top level directory
4 * PROJECT: ReactOS kernel
5 * FILE: ntoskrnl/ps/thread.c
6 * PURPOSE: Thread managment
7 * PROGRAMMER: David Welch (welch@mcmail.com)
8 * REVISION HISTORY:
9 * 23/06/98: Created
10 * 12/10/99: Phillip Susi: Thread priorities, and APC work
11 * 09/08/03: Skywing: ThreadEventPair support (delete)
12 */
13
14 /*
15 * NOTE:
16 *
17 * All of the routines that manipulate the thread queue synchronize on
18 * a single spinlock
19 *
20 */
21
22 /* INCLUDES ****************************************************************/
23
24 #define NTOS_MODE_KERNEL
25 #include <ntos.h>
26 #include <internal/ke.h>
27 #include <internal/ob.h>
28 #include <internal/ps.h>
29 #include <internal/se.h>
30 #include <internal/id.h>
31 #include <internal/dbg.h>
32 #include <internal/ldr.h>
33
34 #define NDEBUG
35 #include <internal/debug.h>
36
37 /* GLOBAL *******************************************************************/
38
39 static ULONG PiNextThreadUniqueId = 0;
40
41 extern KSPIN_LOCK PiThreadListLock;
42 extern ULONG PiNrThreads;
43
44 extern LIST_ENTRY PiThreadListHead;
45
46 #define MAX_THREAD_NOTIFY_ROUTINE_COUNT 8
47
48 static ULONG PiThreadNotifyRoutineCount = 0;
49 static PCREATE_THREAD_NOTIFY_ROUTINE
50 PiThreadNotifyRoutine[MAX_THREAD_NOTIFY_ROUTINE_COUNT];
51
52 /* FUNCTIONS ***************************************************************/
53
54 NTSTATUS STDCALL
55 PsAssignImpersonationToken(PETHREAD Thread,
56 HANDLE TokenHandle)
57 {
58 PACCESS_TOKEN Token;
59 SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
60 NTSTATUS Status;
61
62 if (TokenHandle != NULL)
63 {
64 Status = ObReferenceObjectByHandle(TokenHandle,
65 0,
66 SepTokenObjectType,
67 UserMode,
68 (PVOID*)&Token,
69 NULL);
70 if (!NT_SUCCESS(Status))
71 {
72 return(Status);
73 }
74 ImpersonationLevel = Token->ImpersonationLevel;
75 }
76 else
77 {
78 Token = NULL;
79 ImpersonationLevel = 0;
80 }
81
82 PsImpersonateClient(Thread,
83 Token,
84 0,
85 0,
86 ImpersonationLevel);
87 if (Token != NULL)
88 {
89 ObDereferenceObject(Token);
90 }
91 return(STATUS_SUCCESS);
92 }
93
94
95 /*
96 * @implemented
97 */
98 VOID STDCALL
99 PsRevertToSelf (VOID)
100 {
101 PETHREAD Thread;
102
103 Thread = PsGetCurrentThread ();
104
105 if (Thread->ActiveImpersonationInfo == TRUE)
106 {
107 ObDereferenceObject (Thread->ImpersonationInfo->Token);
108 Thread->ActiveImpersonationInfo = FALSE;
109 }
110 }
111
112
113 /*
114 * @implemented
115 */
116 VOID STDCALL
117 PsImpersonateClient (IN PETHREAD Thread,
118 IN PACCESS_TOKEN Token,
119 IN BOOLEAN CopyOnOpen,
120 IN BOOLEAN EffectiveOnly,
121 IN SECURITY_IMPERSONATION_LEVEL ImpersonationLevel)
122 {
123 if (Token == NULL)
124 {
125 if (Thread->ActiveImpersonationInfo == TRUE)
126 {
127 Thread->ActiveImpersonationInfo = FALSE;
128 if (Thread->ImpersonationInfo->Token != NULL)
129 {
130 ObDereferenceObject (Thread->ImpersonationInfo->Token);
131 }
132 }
133 return;
134 }
135
136 if (Thread->ImpersonationInfo == NULL)
137 {
138 Thread->ImpersonationInfo = ExAllocatePool (NonPagedPool,
139 sizeof(PS_IMPERSONATION_INFO));
140 }
141
142 Thread->ImpersonationInfo->Level = ImpersonationLevel;
143 Thread->ImpersonationInfo->CopyOnOpen = CopyOnOpen;
144 Thread->ImpersonationInfo->EffectiveOnly = EffectiveOnly;
145 Thread->ImpersonationInfo->Token = Token;
146 ObReferenceObjectByPointer (Token,
147 0,
148 SepTokenObjectType,
149 KernelMode);
150 Thread->ActiveImpersonationInfo = TRUE;
151 }
152
153
154 PACCESS_TOKEN
155 PsReferenceEffectiveToken(PETHREAD Thread,
156 PTOKEN_TYPE TokenType,
157 PBOOLEAN EffectiveOnly,
158 PSECURITY_IMPERSONATION_LEVEL Level)
159 {
160 PEPROCESS Process;
161 PACCESS_TOKEN Token;
162
163 if (Thread->ActiveImpersonationInfo == FALSE)
164 {
165 Process = Thread->ThreadsProcess;
166 *TokenType = TokenPrimary;
167 *EffectiveOnly = FALSE;
168 Token = Process->Token;
169 }
170 else
171 {
172 Token = Thread->ImpersonationInfo->Token;
173 *TokenType = TokenImpersonation;
174 *EffectiveOnly = Thread->ImpersonationInfo->EffectiveOnly;
175 *Level = Thread->ImpersonationInfo->Level;
176 }
177 return(Token);
178 }
179
180
181 NTSTATUS STDCALL
182 NtImpersonateThread(IN HANDLE ThreadHandle,
183 IN HANDLE ThreadToImpersonateHandle,
184 IN PSECURITY_QUALITY_OF_SERVICE SecurityQualityOfService)
185 {
186 SECURITY_CLIENT_CONTEXT ClientContext;
187 PETHREAD Thread;
188 PETHREAD ThreadToImpersonate;
189 NTSTATUS Status;
190
191 Status = ObReferenceObjectByHandle (ThreadHandle,
192 0,
193 PsThreadType,
194 UserMode,
195 (PVOID*)&Thread,
196 NULL);
197 if (!NT_SUCCESS (Status))
198 {
199 return Status;
200 }
201
202 Status = ObReferenceObjectByHandle (ThreadToImpersonateHandle,
203 0,
204 PsThreadType,
205 UserMode,
206 (PVOID*)&ThreadToImpersonate,
207 NULL);
208 if (!NT_SUCCESS(Status))
209 {
210 ObDereferenceObject (Thread);
211 return Status;
212 }
213
214 Status = SeCreateClientSecurity (ThreadToImpersonate,
215 SecurityQualityOfService,
216 0,
217 &ClientContext);
218 if (!NT_SUCCESS(Status))
219 {
220 ObDereferenceObject (ThreadToImpersonate);
221 ObDereferenceObject (Thread);
222 return Status;
223 }
224
225 SeImpersonateClient (&ClientContext,
226 Thread);
227 if (ClientContext.Token != NULL)
228 {
229 ObDereferenceObject (ClientContext.Token);
230 }
231
232 ObDereferenceObject (ThreadToImpersonate);
233 ObDereferenceObject (Thread);
234
235 return STATUS_SUCCESS;
236 }
237
238
239 NTSTATUS STDCALL
240 NtOpenThreadToken (IN HANDLE ThreadHandle,
241 IN ACCESS_MASK DesiredAccess,
242 IN BOOLEAN OpenAsSelf,
243 OUT PHANDLE TokenHandle)
244 {
245 PACCESS_TOKEN Token;
246 PETHREAD Thread;
247 NTSTATUS Status;
248
249 Status = ObReferenceObjectByHandle (ThreadHandle,
250 0,
251 PsThreadType,
252 UserMode,
253 (PVOID*)&Thread,
254 NULL);
255 if (!NT_SUCCESS(Status))
256 {
257 return(Status);
258 }
259
260 if (OpenAsSelf)
261 {
262 if (Thread->ActiveImpersonationInfo == FALSE)
263 {
264 ObDereferenceObject (Thread);
265 return STATUS_NO_TOKEN;
266 }
267
268 Token = Thread->ImpersonationInfo->Token;
269 }
270 else
271 {
272 Token = Thread->ThreadsProcess->Token;
273 }
274
275 if (Token == NULL)
276 {
277 ObDereferenceObject (Thread);
278 return STATUS_NO_TOKEN;
279 }
280
281 Status = ObCreateHandle (PsGetCurrentProcess(),
282 Token,
283 DesiredAccess,
284 FALSE,
285 TokenHandle);
286
287 ObDereferenceObject (Thread);
288
289 return Status;
290 }
291
292
293 /*
294 * @implemented
295 */
296 PACCESS_TOKEN STDCALL
297 PsReferenceImpersonationToken(IN PETHREAD Thread,
298 OUT PBOOLEAN CopyOnOpen,
299 OUT PBOOLEAN EffectiveOnly,
300 OUT PSECURITY_IMPERSONATION_LEVEL ImpersonationLevel)
301 {
302 if (Thread->ActiveImpersonationInfo == FALSE)
303 {
304 return NULL;
305 }
306
307 *ImpersonationLevel = Thread->ImpersonationInfo->Level;
308 *CopyOnOpen = Thread->ImpersonationInfo->CopyOnOpen;
309 *EffectiveOnly = Thread->ImpersonationInfo->EffectiveOnly;
310 ObReferenceObjectByPointer (Thread->ImpersonationInfo->Token,
311 TOKEN_ALL_ACCESS,
312 SepTokenObjectType,
313 KernelMode);
314
315 return Thread->ImpersonationInfo->Token;
316 }
317
318
319 VOID
320 PiBeforeBeginThread(CONTEXT c)
321 {
322 KeLowerIrql(PASSIVE_LEVEL);
323 }
324
325
326 VOID STDCALL
327 PiDeleteThread(PVOID ObjectBody)
328 {
329 KIRQL oldIrql;
330 PETHREAD Thread;
331
332 Thread = (PETHREAD)ObjectBody;
333
334 DPRINT("PiDeleteThread(ObjectBody %x)\n",ObjectBody);
335
336 ObDereferenceObject(Thread->ThreadsProcess);
337 Thread->ThreadsProcess = NULL;
338
339 KeAcquireSpinLock(&PiThreadListLock, &oldIrql);
340 PiNrThreads--;
341 RemoveEntryList(&Thread->Tcb.ThreadListEntry);
342 KeReleaseSpinLock(&PiThreadListLock, oldIrql);
343
344 KeReleaseThread(Thread);
345 DPRINT("PiDeleteThread() finished\n");
346 }
347
348
349 NTSTATUS
350 PsInitializeThread(HANDLE ProcessHandle,
351 PETHREAD* ThreadPtr,
352 PHANDLE ThreadHandle,
353 ACCESS_MASK DesiredAccess,
354 POBJECT_ATTRIBUTES ThreadAttributes,
355 BOOLEAN First)
356 {
357 PETHREAD Thread;
358 NTSTATUS Status;
359 KIRQL oldIrql;
360 PEPROCESS Process;
361
362 /*
363 * Reference process
364 */
365 if (ProcessHandle != NULL)
366 {
367 Status = ObReferenceObjectByHandle(ProcessHandle,
368 PROCESS_CREATE_THREAD,
369 PsProcessType,
370 UserMode,
371 (PVOID*)&Process,
372 NULL);
373 if (Status != STATUS_SUCCESS)
374 {
375 DPRINT("Failed at %s:%d\n",__FILE__,__LINE__);
376 return(Status);
377 }
378 DPRINT( "Creating thread in process %x\n", Process );
379 }
380 else
381 {
382 Process = PsInitialSystemProcess;
383 ObReferenceObjectByPointer(Process,
384 PROCESS_CREATE_THREAD,
385 PsProcessType,
386 UserMode);
387 }
388
389 /*
390 * Create and initialize thread
391 */
392 Status = ObCreateObject(UserMode,
393 PsThreadType,
394 ThreadAttributes,
395 UserMode,
396 NULL,
397 sizeof(ETHREAD),
398 0,
399 0,
400 (PVOID*)&Thread);
401 if (!NT_SUCCESS(Status))
402 {
403 return(Status);
404 }
405
406 Status = ObInsertObject ((PVOID)Thread,
407 NULL,
408 DesiredAccess,
409 0,
410 NULL,
411 ThreadHandle);
412 if (!NT_SUCCESS(Status))
413 {
414 ObDereferenceObject (Thread);
415 return Status;
416 }
417
418 DPRINT("Thread = %x\n",Thread);
419
420 PiNrThreads++;
421
422 KeInitializeThread(&Process->Pcb, &Thread->Tcb, First);
423 Thread->ThreadsProcess = Process;
424 /*
425 * FIXME: What lock protects this?
426 */
427 InsertTailList(&Thread->ThreadsProcess->ThreadListHead,
428 &Thread->Tcb.ProcessThreadListEntry);
429 InitializeListHead(&Thread->TerminationPortList);
430 KeInitializeSpinLock(&Thread->ActiveTimerListLock);
431 InitializeListHead(&Thread->IrpList);
432 Thread->Cid.UniqueThread = (HANDLE)InterlockedIncrement(
433 (LONG *)&PiNextThreadUniqueId);
434 Thread->Cid.UniqueProcess = (HANDLE)Thread->ThreadsProcess->UniqueProcessId;
435 Thread->DeadThread = 0;
436 Thread->Win32Thread = 0;
437 DPRINT("Thread->Cid.UniqueThread %d\n",Thread->Cid.UniqueThread);
438
439 *ThreadPtr = Thread;
440
441 KeAcquireSpinLock(&PiThreadListLock, &oldIrql);
442 InsertTailList(&PiThreadListHead, &Thread->Tcb.ThreadListEntry);
443 KeReleaseSpinLock(&PiThreadListLock, oldIrql);
444
445 Thread->Tcb.BasePriority = (CHAR)Thread->ThreadsProcess->Pcb.BasePriority;
446 Thread->Tcb.Priority = Thread->Tcb.BasePriority;
447
448 return(STATUS_SUCCESS);
449 }
450
451
452 static NTSTATUS
453 PsCreateTeb(HANDLE ProcessHandle,
454 PTEB *TebPtr,
455 PETHREAD Thread,
456 PUSER_STACK UserStack)
457 {
458 MEMORY_BASIC_INFORMATION Info;
459 NTSTATUS Status;
460 ULONG ByteCount;
461 ULONG RegionSize;
462 ULONG TebSize;
463 PVOID TebBase;
464 TEB Teb;
465 ULONG ResultLength;
466
467 TebBase = (PVOID)0x7FFDE000;
468 TebSize = PAGE_SIZE;
469
470 while (TRUE)
471 {
472 Status = NtQueryVirtualMemory(ProcessHandle,
473 TebBase,
474 MemoryBasicInformation,
475 &Info,
476 sizeof(Info),
477 &ResultLength);
478 if (!NT_SUCCESS(Status))
479 {
480 CPRINT("NtQueryVirtualMemory (Status %x)\n", Status);
481 KEBUGCHECK(0);
482 }
483 /* FIXME: Race between this and the above check */
484 if (Info.State == MEM_FREE)
485 {
486 /* The TEB must reside in user space */
487 Status = NtAllocateVirtualMemory(ProcessHandle,
488 &TebBase,
489 0,
490 &TebSize,
491 MEM_RESERVE | MEM_COMMIT,
492 PAGE_READWRITE);
493 if (NT_SUCCESS(Status))
494 {
495 break;
496 }
497 }
498
499 TebBase = (char*)TebBase - TebSize;
500 }
501
502 DPRINT ("TebBase %p TebSize %lu\n", TebBase, TebSize);
503
504 RtlZeroMemory(&Teb, sizeof(TEB));
505 /* set all pointers to and from the TEB */
506 Teb.Tib.Self = TebBase;
507 if (Thread->ThreadsProcess)
508 {
509 Teb.Peb = Thread->ThreadsProcess->Peb; /* No PEB yet!! */
510 }
511 DPRINT("Teb.Peb %x\n", Teb.Peb);
512
513 /* store stack information from UserStack */
514 if(UserStack != NULL)
515 {
516 /* fixed-size stack */
517 if(UserStack->FixedStackBase && UserStack->FixedStackLimit)
518 {
519 Teb.Tib.StackBase = UserStack->FixedStackBase;
520 Teb.Tib.StackLimit = UserStack->FixedStackLimit;
521 Teb.DeallocationStack = UserStack->FixedStackLimit;
522 }
523 /* expandable stack */
524 else
525 {
526 Teb.Tib.StackBase = UserStack->ExpandableStackBase;
527 Teb.Tib.StackLimit = UserStack->ExpandableStackLimit;
528 Teb.DeallocationStack = UserStack->ExpandableStackBottom;
529 }
530 }
531
532 /* more initialization */
533 Teb.Cid.UniqueThread = Thread->Cid.UniqueThread;
534 Teb.Cid.UniqueProcess = Thread->Cid.UniqueProcess;
535 Teb.CurrentLocale = PsDefaultThreadLocaleId;
536
537 /* Terminate the exception handler list */
538 Teb.Tib.ExceptionList = (PVOID)-1;
539
540 DPRINT("sizeof(TEB) %x\n", sizeof(TEB));
541
542 /* write TEB data into teb page */
543 Status = NtWriteVirtualMemory(ProcessHandle,
544 TebBase,
545 &Teb,
546 sizeof(TEB),
547 &ByteCount);
548
549 if (!NT_SUCCESS(Status))
550 {
551 /* free TEB */
552 DPRINT1 ("Writing TEB failed!\n");
553
554 RegionSize = 0;
555 NtFreeVirtualMemory(ProcessHandle,
556 TebBase,
557 &RegionSize,
558 MEM_RELEASE);
559
560 return Status;
561 }
562
563 if (TebPtr != NULL)
564 {
565 *TebPtr = (PTEB)TebBase;
566 }
567
568 DPRINT("TEB allocated at %p\n", TebBase);
569
570 return Status;
571 }
572
573
574 VOID STDCALL
575 LdrInitApcRundownRoutine(PKAPC Apc)
576 {
577 ExFreePool(Apc);
578 }
579
580
581 VOID STDCALL
582 LdrInitApcKernelRoutine(PKAPC Apc,
583 PKNORMAL_ROUTINE* NormalRoutine,
584 PVOID* NormalContext,
585 PVOID* SystemArgument1,
586 PVOID* SystemArgument2)
587 {
588 ExFreePool(Apc);
589 }
590
591
592 NTSTATUS STDCALL
593 NtCreateThread(PHANDLE ThreadHandle,
594 ACCESS_MASK DesiredAccess,
595 POBJECT_ATTRIBUTES ObjectAttributes,
596 HANDLE ProcessHandle,
597 PCLIENT_ID Client,
598 PCONTEXT ThreadContext,
599 PUSER_STACK UserStack,
600 BOOLEAN CreateSuspended)
601 {
602 PETHREAD Thread;
603 PTEB TebBase;
604 NTSTATUS Status;
605 PKAPC LdrInitApc;
606
607 DPRINT("NtCreateThread(ThreadHandle %x, PCONTEXT %x)\n",
608 ThreadHandle,ThreadContext);
609
610 Status = PsInitializeThread(ProcessHandle,
611 &Thread,
612 ThreadHandle,
613 DesiredAccess,
614 ObjectAttributes,
615 FALSE);
616 if (!NT_SUCCESS(Status))
617 {
618 return(Status);
619 }
620
621 Status = KiArchInitThreadWithContext(&Thread->Tcb, ThreadContext);
622 if (!NT_SUCCESS(Status))
623 {
624 return(Status);
625 }
626
627 Status = PsCreateTeb(ProcessHandle,
628 &TebBase,
629 Thread,
630 UserStack);
631 if (!NT_SUCCESS(Status))
632 {
633 return(Status);
634 }
635 Thread->Tcb.Teb = TebBase;
636
637 Thread->StartAddress = NULL;
638
639 if (Client != NULL)
640 {
641 *Client = Thread->Cid;
642 }
643
644 /*
645 * Maybe send a message to the process's debugger
646 */
647 DbgkCreateThread((PVOID)ThreadContext->Eip);
648
649 /*
650 * First, force the thread to be non-alertable for user-mode alerts.
651 */
652 Thread->Tcb.Alertable = FALSE;
653
654 /*
655 * If the thread is to be created suspended then queue an APC to
656 * do the suspend before we run any userspace code.
657 */
658 if (CreateSuspended)
659 {
660 PsSuspendThread(Thread, NULL);
661 }
662
663 /*
664 * Queue an APC to the thread that will execute the ntdll startup
665 * routine.
666 */
667 LdrInitApc = ExAllocatePool(NonPagedPool, sizeof(KAPC));
668 KeInitializeApc(LdrInitApc, &Thread->Tcb, OriginalApcEnvironment, LdrInitApcKernelRoutine,
669 LdrInitApcRundownRoutine, LdrpGetSystemDllEntryPoint(),
670 UserMode, NULL);
671 KeInsertQueueApc(LdrInitApc, NULL, NULL, IO_NO_INCREMENT);
672
673 /*
674 * Start the thread running and force it to execute the APC(s) we just
675 * queued before it runs anything else in user-mode.
676 */
677 Thread->Tcb.Alertable = TRUE;
678 Thread->Tcb.Alerted[0] = 1;
679 PsUnblockThread(Thread, NULL);
680
681 return(STATUS_SUCCESS);
682 }
683
684
685 /*
686 * @implemented
687 */
688 NTSTATUS STDCALL
689 PsCreateSystemThread(PHANDLE ThreadHandle,
690 ACCESS_MASK DesiredAccess,
691 POBJECT_ATTRIBUTES ObjectAttributes,
692 HANDLE ProcessHandle,
693 PCLIENT_ID ClientId,
694 PKSTART_ROUTINE StartRoutine,
695 PVOID StartContext)
696 /*
697 * FUNCTION: Creates a thread which executes in kernel mode
698 * ARGUMENTS:
699 * ThreadHandle (OUT) = Caller supplied storage for the returned thread
700 * handle
701 * DesiredAccess = Requested access to the thread
702 * ObjectAttributes = Object attributes (optional)
703 * ProcessHandle = Handle of process thread will run in
704 * NULL to use system process
705 * ClientId (OUT) = Caller supplied storage for the returned client id
706 * of the thread (optional)
707 * StartRoutine = Entry point for the thread
708 * StartContext = Argument supplied to the thread when it begins
709 * execution
710 * RETURNS: Success or failure status
711 */
712 {
713 PETHREAD Thread;
714 NTSTATUS Status;
715
716 DPRINT("PsCreateSystemThread(ThreadHandle %x, ProcessHandle %x)\n",
717 ThreadHandle,ProcessHandle);
718
719 Status = PsInitializeThread(ProcessHandle,
720 &Thread,
721 ThreadHandle,
722 DesiredAccess,
723 ObjectAttributes,
724 FALSE);
725 if (!NT_SUCCESS(Status))
726 {
727 return(Status);
728 }
729
730 Thread->StartAddress = StartRoutine;
731 Status = KiArchInitThread(&Thread->Tcb, StartRoutine, StartContext);
732 if (!NT_SUCCESS(Status))
733 {
734 return(Status);
735 }
736
737 if (ClientId != NULL)
738 {
739 *ClientId=Thread->Cid;
740 }
741
742 PsUnblockThread(Thread, NULL);
743
744 return(STATUS_SUCCESS);
745 }
746
747
748 VOID STDCALL
749 PspRunCreateThreadNotifyRoutines(PETHREAD CurrentThread,
750 BOOLEAN Create)
751 {
752 ULONG i;
753 CLIENT_ID Cid = CurrentThread->Cid;
754
755 for (i = 0; i < PiThreadNotifyRoutineCount; i++)
756 {
757 PiThreadNotifyRoutine[i](Cid.UniqueProcess, Cid.UniqueThread, Create);
758 }
759 }
760
761
762 /*
763 * @implemented
764 */
765 NTSTATUS STDCALL
766 PsSetCreateThreadNotifyRoutine(IN PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine)
767 {
768 if (PiThreadNotifyRoutineCount >= MAX_THREAD_NOTIFY_ROUTINE_COUNT)
769 {
770 return(STATUS_INSUFFICIENT_RESOURCES);
771 }
772
773 PiThreadNotifyRoutine[PiThreadNotifyRoutineCount] = NotifyRoutine;
774 PiThreadNotifyRoutineCount++;
775
776 return(STATUS_SUCCESS);
777 }
778
779 /* EOF */