1use std::cell::{Cell, RefCell};
4use std::ops::{Deref, DerefMut};
5
6use linux_api::errno::Errno;
7use linux_api::fcntl::DescriptorFlags;
8use linux_api::mman::{MapFlags, ProtFlags};
9use linux_api::posix_types::Pid;
10use linux_api::signal::stack_t;
11use shadow_shim_helper_rs::HostId;
12use shadow_shim_helper_rs::explicit_drop::ExplicitDrop;
13use shadow_shim_helper_rs::rootedcell::rc::RootedRc;
14use shadow_shim_helper_rs::rootedcell::refcell::RootedRefCell;
15use shadow_shim_helper_rs::shim_shmem::{HostShmemProtected, ThreadShmem};
16use shadow_shim_helper_rs::syscall_types::{ForeignPtr, SyscallReg};
17use shadow_shim_helper_rs::util::SendPointer;
18use shadow_shmem::allocator::{ShMemBlock, shmalloc};
19
20use super::context::ProcessContext;
21use super::descriptor::descriptor_table::{DescriptorHandle, DescriptorTable};
22use super::host::Host;
23use super::managed_thread::{self, ManagedThread};
24use super::process::{Process, ProcessId};
25use crate::cshadow as c;
26use crate::host::syscall::condition::{SyscallConditionRef, SyscallConditionRefMut};
27use crate::host::syscall::handler::SyscallHandler;
28use crate::utility::callback_queue::CallbackQueue;
29use crate::utility::{IsSend, ObjectCounter, syscall};
30
31#[derive(Debug)]
33#[must_use]
34pub enum ResumeResult {
35 Blocked,
37 ExitedThread(i32),
39 ExitedProcess,
41}
42
43pub struct Thread {
46 id: ThreadId,
47 host_id: HostId,
48 process_id: ProcessId,
49 tid_address: Cell<ForeignPtr<libc::pid_t>>,
52 shim_shared_memory: ShMemBlock<'static, ThreadShmem>,
53 syscallhandler: RootedRefCell<SyscallHandler>,
54 desc_table: Option<RootedRc<RootedRefCell<DescriptorTable>>>,
61 cond: Cell<SendPointer<c::SysCallCondition>>,
64 mthread: RefCell<ManagedThread>,
66 _counter: ObjectCounter,
67}
68
69impl IsSend for Thread {}
70
71impl Thread {
72 pub fn mthread(&self) -> impl Deref<Target = ManagedThread> + '_ {
74 self.mthread.borrow()
75 }
76
77 pub fn update_for_exec(&mut self, host: &Host, mthread: ManagedThread, new_tid: ThreadId) {
81 self.mthread.replace(mthread).handle_process_exit();
82 self.tid_address.set(ForeignPtr::null());
83
84 {
86 let host_shmem_prot = host.shim_shmem_lock_borrow().unwrap();
93
94 let mut thread_shmem =
95 ThreadShmem::clone(&self.shim_shared_memory, &host_shmem_prot.root);
96
97 thread_shmem.tid = new_tid.into();
99
100 unsafe {
102 *thread_shmem
103 .protected
104 .borrow_mut(&host_shmem_prot.root)
105 .sigaltstack_mut() = stack_t::new(
106 std::ptr::null_mut(),
107 linux_api::signal::SigAltStackFlags::SS_DISABLE,
108 0,
109 )
110 };
111
112 self.shim_shared_memory = shmalloc(thread_shmem);
113 }
114
115 self.syscallhandler = RootedRefCell::new(
116 host.root(),
117 SyscallHandler::new(
118 host.id(),
119 self.process_id,
120 new_tid,
121 host.params.use_syscall_counters,
122 ),
123 );
124
125 {
127 let desc_table_rc = self.desc_table.take().unwrap();
129 let mut desc_table = DescriptorTable::clone(&desc_table_rc.borrow(host.root()));
130 desc_table_rc.explicit_drop_recursive(host.root(), host);
131
132 let to_close: Vec<DescriptorHandle> = desc_table
134 .iter()
135 .filter_map(|(handle, descriptor)| {
136 if descriptor.flags().contains(DescriptorFlags::FD_CLOEXEC) {
137 Some(*handle)
138 } else {
139 None
140 }
141 })
142 .collect();
143
144 CallbackQueue::queue_and_run_with_legacy(|q| {
145 for handle in to_close {
146 log::trace!("Unregistering FD_CLOEXEC descriptor {handle:?}");
147 if let Some(Err(e)) = desc_table
148 .deregister_descriptor(handle)
149 .unwrap()
150 .close(host, q)
151 {
152 log::debug!("Error closing {handle:?}: {e:?}");
153 };
154 }
155 });
156
157 self.desc_table = Some(RootedRc::new(
158 host.root(),
159 RootedRefCell::new(host.root(), desc_table),
160 ));
161 }
162
163 if let Some(c) = unsafe { self.cond.get_mut().ptr().as_mut() } {
164 unsafe { c::syscallcondition_cancel(c) };
165 unsafe { c::syscallcondition_unref(c) };
166 }
167 self.cond = Cell::new(unsafe { SendPointer::new(std::ptr::null_mut()) });
168
169 self.id = new_tid;
170 }
171
172 fn native_syscall_raw(
174 &self,
175 ctx: &ProcessContext,
176 n: i64,
177 args: &[SyscallReg],
178 ) -> libc::c_long {
179 self.mthread
180 .borrow()
181 .native_syscall(&ctx.with_thread(self), n, args)
182 .into()
183 }
184
185 fn native_syscall(
187 &self,
188 ctx: &ProcessContext,
189 n: i64,
190 args: &[SyscallReg],
191 ) -> Result<SyscallReg, Errno> {
192 syscall::raw_return_value_to_result(self.native_syscall_raw(ctx, n, args))
193 }
194
195 pub fn process_id(&self) -> ProcessId {
196 self.process_id
197 }
198
199 pub fn host_id(&self) -> HostId {
200 self.host_id
201 }
202
203 pub fn native_pid(&self) -> Pid {
204 self.mthread.borrow().native_pid()
205 }
206
207 pub fn native_tid(&self) -> Pid {
208 self.mthread.borrow().native_tid()
209 }
210
211 pub fn id(&self) -> ThreadId {
212 self.id
213 }
214
215 pub fn is_leader(&self) -> bool {
218 self.id == self.process_id.into()
219 }
220
221 pub fn syscall_condition(&self) -> Option<SyscallConditionRef> {
222 let c = self.cond.get().ptr();
227 if c.is_null() {
228 None
229 } else {
230 Some(unsafe { SyscallConditionRef::borrow_from_c(c) })
231 }
232 }
233
234 pub fn syscall_condition_mut(&self) -> Option<SyscallConditionRefMut> {
235 let c = self.cond.get().ptr();
238 if c.is_null() {
239 None
240 } else {
241 Some(unsafe { SyscallConditionRefMut::borrow_from_c(c) })
242 }
243 }
244
245 pub fn cleanup_syscall_condition(&self) {
246 if let Some(c) = unsafe {
247 self.cond
248 .replace(SendPointer::new(std::ptr::null_mut()))
249 .ptr()
250 .as_mut()
251 } {
252 unsafe { c::syscallcondition_cancel(c) };
253 unsafe { c::syscallcondition_unref(c) };
254 }
255 }
256
257 pub fn descriptor_table(&self) -> &RootedRc<RootedRefCell<DescriptorTable>> {
258 self.desc_table.as_ref().unwrap()
259 }
260
261 #[track_caller]
262 pub fn descriptor_table_borrow<'a>(
263 &'a self,
264 host: &'a Host,
265 ) -> impl Deref<Target = DescriptorTable> + 'a {
266 self.desc_table.as_ref().unwrap().borrow(host.root())
267 }
268
269 #[track_caller]
270 pub fn descriptor_table_borrow_mut<'a>(
271 &'a self,
272 host: &'a Host,
273 ) -> impl DerefMut<Target = DescriptorTable> + 'a {
274 self.desc_table.as_ref().unwrap().borrow_mut(host.root())
275 }
276
277 pub fn native_munmap(
279 &self,
280 ctx: &ProcessContext,
281 ptr: ForeignPtr<u8>,
282 size: usize,
283 ) -> Result<(), Errno> {
284 self.native_syscall(ctx, libc::SYS_munmap, &[ptr.into(), size.into()])?;
285 Ok(())
286 }
287
288 pub fn native_mmap(
290 &self,
291 ctx: &ProcessContext,
292 addr: ForeignPtr<u8>,
293 len: usize,
294 prot: ProtFlags,
295 flags: MapFlags,
296 fd: i32,
297 offset: i64,
298 ) -> Result<ForeignPtr<u8>, Errno> {
299 Ok(self
300 .native_syscall(
301 ctx,
302 libc::SYS_mmap,
303 &[
304 SyscallReg::from(addr),
305 SyscallReg::from(len),
306 SyscallReg::from(prot.bits()),
307 SyscallReg::from(flags.bits()),
308 SyscallReg::from(fd),
309 SyscallReg::from(offset),
310 ],
311 )?
312 .into())
313 }
314
315 pub fn native_mremap(
317 &self,
318 ctx: &ProcessContext,
319 old_addr: ForeignPtr<u8>,
320 old_len: usize,
321 new_len: usize,
322 flags: i32,
323 new_addr: ForeignPtr<u8>,
324 ) -> Result<ForeignPtr<u8>, Errno> {
325 Ok(self
326 .native_syscall(
327 ctx,
328 libc::SYS_mremap,
329 &[
330 SyscallReg::from(old_addr),
331 SyscallReg::from(old_len),
332 SyscallReg::from(new_len),
333 SyscallReg::from(flags),
334 SyscallReg::from(new_addr),
335 ],
336 )?
337 .into())
338 }
339
340 pub fn native_mprotect(
342 &self,
343 ctx: &ProcessContext,
344 addr: ForeignPtr<u8>,
345 len: usize,
346 prot: ProtFlags,
347 ) -> Result<(), Errno> {
348 self.native_syscall(
349 ctx,
350 libc::SYS_mprotect,
351 &[
352 SyscallReg::from(addr),
353 SyscallReg::from(len),
354 SyscallReg::from(prot.bits()),
355 ],
356 )?;
357 Ok(())
358 }
359
360 pub fn native_open(
362 &self,
363 ctx: &ProcessContext,
364 pathname: ForeignPtr<u8>,
365 flags: i32,
366 mode: i32,
367 ) -> Result<i32, Errno> {
368 let res = self.native_syscall(
369 ctx,
370 libc::SYS_open,
371 &[
372 SyscallReg::from(pathname),
373 SyscallReg::from(flags),
374 SyscallReg::from(mode),
375 ],
376 );
377 Ok(i32::from(res?))
378 }
379
380 pub fn native_close(&self, ctx: &ProcessContext, fd: i32) -> Result<(), Errno> {
382 self.native_syscall(ctx, libc::SYS_close, &[SyscallReg::from(fd)])?;
383 Ok(())
384 }
385
386 pub fn native_brk(
388 &self,
389 ctx: &ProcessContext,
390 addr: ForeignPtr<u8>,
391 ) -> Result<ForeignPtr<u8>, Errno> {
392 let res = self.native_syscall(ctx, libc::SYS_brk, &[SyscallReg::from(addr)])?;
393 Ok(ForeignPtr::from(res))
394 }
395
396 pub fn native_chdir(
398 &self,
399 ctx: &ProcessContext,
400 pathname: ForeignPtr<std::ffi::c_char>,
401 ) -> Result<i32, Errno> {
402 let res = self.native_syscall(ctx, libc::SYS_chdir, &[SyscallReg::from(pathname)]);
403 Ok(i32::from(res?))
404 }
405
406 pub fn malloc_foreign_ptr(
409 &self,
410 ctx: &ProcessContext,
411 size: usize,
412 ) -> Result<ForeignPtr<u8>, Errno> {
413 self.native_mmap(
415 ctx,
416 ForeignPtr::null(),
417 size,
418 ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
419 MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS,
420 -1,
421 0,
422 )
423 }
424
425 pub fn free_foreign_ptr(
427 &self,
428 ctx: &ProcessContext,
429 ptr: ForeignPtr<u8>,
430 size: usize,
431 ) -> Result<(), Errno> {
432 self.native_munmap(ctx, ptr, size)?;
433 Ok(())
434 }
435
436 pub fn wrap_mthread(
439 host: &Host,
440 mthread: ManagedThread,
441 desc_table: RootedRc<RootedRefCell<DescriptorTable>>,
442 pid: ProcessId,
443 tid: ThreadId,
444 ) -> Result<Thread, Errno> {
445 let child = Self {
446 mthread: RefCell::new(mthread),
447 syscallhandler: RootedRefCell::new(
448 host.root(),
449 SyscallHandler::new(host.id(), pid, tid, host.params.use_syscall_counters),
450 ),
451 cond: Cell::new(unsafe { SendPointer::new(std::ptr::null_mut()) }),
452 id: tid,
453 host_id: host.id(),
454 process_id: pid,
455 tid_address: Cell::new(ForeignPtr::null()),
456 shim_shared_memory: shmalloc(ThreadShmem::new(
457 &host.shim_shmem_lock_borrow().unwrap(),
458 tid.into(),
459 )),
460 desc_table: Some(desc_table),
461 _counter: ObjectCounter::new("Thread"),
462 };
463 Ok(child)
464 }
465
466 pub fn shmem(&self) -> &ShMemBlock<ThreadShmem> {
468 &self.shim_shared_memory
469 }
470
471 pub fn resume(&self, ctx: &ProcessContext) -> ResumeResult {
472 if let Some(c) = unsafe { self.cond.get().ptr().as_mut() } {
475 unsafe { c::syscallcondition_cancel(c) };
476 }
477
478 let mut syscall_handler = self.syscallhandler.borrow_mut(ctx.host.root());
479
480 let res = self
481 .mthread
482 .borrow()
483 .resume(&ctx.with_thread(self), &mut syscall_handler);
484
485 if let Some(c) = unsafe {
487 self.cond
488 .replace(SendPointer::new(std::ptr::null_mut()))
489 .ptr()
490 .as_mut()
491 } {
492 unsafe { c::syscallcondition_unref(c) };
493 }
494
495 match res {
496 managed_thread::ResumeResult::Blocked(cond) => {
497 let cond = cond.into_inner();
499 self.cond.set(unsafe { SendPointer::new(cond) });
500 if let Some(cond) = unsafe { cond.as_mut() } {
501 unsafe { c::syscallcondition_waitNonblock(cond, ctx.host, ctx.process, self) }
502 }
503 ResumeResult::Blocked
504 }
505 managed_thread::ResumeResult::ExitedThread(c) => ResumeResult::ExitedThread(c),
506 managed_thread::ResumeResult::ExitedProcess => ResumeResult::ExitedProcess,
507 }
508 }
509
510 pub fn handle_process_exit(&self) {
511 self.cleanup_syscall_condition();
512 self.mthread.borrow().handle_process_exit();
513 }
514
515 pub fn return_code(&self) -> Option<i32> {
516 self.mthread.borrow().return_code()
517 }
518
519 pub fn is_running(&self) -> bool {
520 self.mthread.borrow().is_running()
521 }
522
523 pub fn get_tid_address(&self) -> ForeignPtr<libc::pid_t> {
524 self.tid_address.get()
525 }
526
527 pub fn set_tid_address(&self, ptr: ForeignPtr<libc::pid_t>) {
530 self.tid_address.set(ptr)
531 }
532
533 pub fn unblocked_signal_pending(
534 &self,
535 process: &Process,
536 host_shmem: &HostShmemProtected,
537 ) -> bool {
538 debug_assert_eq!(process.id(), self.process_id);
539
540 let thread_shmem_protected = self.shmem().protected.borrow(&host_shmem.root);
541
542 let unblocked_signals = !thread_shmem_protected.blocked_signals;
543 let pending_signals = self
544 .shmem()
545 .protected
546 .borrow(&host_shmem.root)
547 .pending_signals
548 | process
549 .shmem()
550 .protected
551 .borrow(&host_shmem.root)
552 .pending_signals;
553
554 !(pending_signals & unblocked_signals).is_empty()
555 }
556}
557
558impl Drop for Thread {
559 fn drop(&mut self) {
560 if let Some(c) = unsafe { self.cond.get_mut().ptr().as_mut() } {
561 unsafe { c::syscallcondition_cancel(c) };
562 unsafe { c::syscallcondition_unref(c) };
563 }
564 }
565}
566
567impl ExplicitDrop for Thread {
568 type ExplicitDropParam = Host;
569 type ExplicitDropResult = ();
570
571 fn explicit_drop(mut self, host: &Host) {
572 if let Some(table) = self.desc_table.take() {
573 table.explicit_drop_recursive(host.root(), host);
574 }
575 }
576}
577
578#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)]
579pub struct ThreadId(u32);
580
581impl TryFrom<libc::pid_t> for ThreadId {
582 type Error = <u32 as TryFrom<libc::pid_t>>::Error;
583
584 fn try_from(value: libc::pid_t) -> Result<Self, Self::Error> {
585 Ok(Self(u32::try_from(value)?))
586 }
587}
588
589impl From<ProcessId> for ThreadId {
590 fn from(value: ProcessId) -> Self {
591 ThreadId(value.into())
593 }
594}
595
596impl From<ThreadId> for libc::pid_t {
597 fn from(val: ThreadId) -> Self {
598 val.0.try_into().unwrap()
599 }
600}
601
602impl std::fmt::Display for ThreadId {
603 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
604 write!(f, "{}", self.0)
605 }
606}
607
608mod export {
609 use shadow_shim_helper_rs::shim_shmem::export::{ShimShmemHostLock, ShimShmemThread};
610 use shadow_shim_helper_rs::syscall_types::UntypedForeignPtr;
611
612 use super::*;
613 use crate::core::worker::Worker;
614 use crate::host::descriptor::socket::Socket;
615 use crate::host::descriptor::socket::inet::InetSocket;
616 use crate::host::descriptor::{CompatFile, Descriptor, File};
617
618 #[unsafe(no_mangle)]
633 unsafe extern "C-unwind" fn thread_nativeSyscall(
634 thread: *const Thread,
635 n: libc::c_long,
636 arg1: SyscallReg,
637 arg2: SyscallReg,
638 arg3: SyscallReg,
639 arg4: SyscallReg,
640 arg5: SyscallReg,
641 arg6: SyscallReg,
642 ) -> libc::c_long {
643 let thread = unsafe { thread.as_ref().unwrap() };
644 Worker::with_active_host(|host| {
645 Worker::with_active_process(|process| {
646 thread.native_syscall_raw(
647 &ProcessContext::new(host, process),
648 n,
649 &[arg1, arg2, arg3, arg4, arg5, arg6],
650 )
651 })
652 .unwrap()
653 })
654 .unwrap()
655 }
656
657 #[unsafe(no_mangle)]
658 pub unsafe extern "C-unwind" fn thread_getID(thread: *const Thread) -> libc::pid_t {
659 let thread = unsafe { thread.as_ref().unwrap() };
660 thread.id().into()
661 }
662
663 #[unsafe(no_mangle)]
665 pub unsafe extern "C-unwind" fn thread_getTidAddress(
666 thread: *const Thread,
667 ) -> UntypedForeignPtr {
668 let thread = unsafe { thread.as_ref().unwrap() };
669 thread.get_tid_address().cast::<()>()
670 }
671
672 #[unsafe(no_mangle)]
675 pub unsafe extern "C-unwind" fn thread_sharedMem(
676 thread: *const Thread,
677 ) -> *const ShimShmemThread {
678 let thread = unsafe { thread.as_ref().unwrap() };
679 &*thread.shim_shared_memory
680 }
681
682 #[unsafe(no_mangle)]
683 pub unsafe extern "C-unwind" fn thread_getProcess(thread: *const Thread) -> *const Process {
684 let thread = unsafe { thread.as_ref().unwrap() };
685 Worker::with_active_host(|host| {
686 let process = host.process_borrow(thread.process_id).unwrap();
687 let p: &Process = &process.borrow(host.root());
688 std::ptr::from_ref(p)
689 })
690 .unwrap()
691 }
692
693 #[unsafe(no_mangle)]
694 pub unsafe extern "C-unwind" fn thread_getHost(thread: *const Thread) -> *const Host {
695 let thread = unsafe { thread.as_ref().unwrap() };
696 Worker::with_active_host(|host| {
697 assert_eq!(host.id(), thread.host_id());
698 std::ptr::from_ref(host)
699 })
700 .unwrap()
701 }
702
703 #[unsafe(no_mangle)]
704 pub unsafe extern "C-unwind" fn thread_clearSysCallCondition(thread: *const Thread) {
705 let thread = unsafe { thread.as_ref().unwrap() };
706 thread.cleanup_syscall_condition();
707 }
708
709 #[unsafe(no_mangle)]
712 pub unsafe extern "C-unwind" fn thread_unblockedSignalPending(
713 thread: *const Thread,
714 host_lock: *const ShimShmemHostLock,
715 ) -> bool {
716 let thread = unsafe { thread.as_ref().unwrap() };
717 let host_lock = unsafe { host_lock.as_ref().unwrap() };
718
719 Worker::with_active_host(|host| {
720 let process = host.process_borrow(thread.process_id()).unwrap();
721 let process = process.borrow(host.root());
722 thread.unblocked_signal_pending(&process, host_lock)
723 })
724 .unwrap()
725 }
726
727 #[unsafe(no_mangle)]
730 pub extern "C-unwind" fn thread_registerDescriptor(
731 thread: *const Thread,
732 desc: *mut Descriptor,
733 ) -> libc::c_int {
734 let thread = unsafe { thread.as_ref().unwrap() };
735 let desc = Descriptor::from_raw(desc).unwrap();
736
737 Worker::with_active_host(|host| {
738 thread
739 .descriptor_table_borrow_mut(host)
740 .register_descriptor(*desc)
741 .unwrap()
742 .into()
743 })
744 .unwrap()
745 }
746
747 #[unsafe(no_mangle)]
749 pub extern "C-unwind" fn thread_getRegisteredDescriptor(
750 thread: *const Thread,
751 handle: libc::c_int,
752 ) -> *const Descriptor {
753 let thread = unsafe { thread.as_ref().unwrap() };
754
755 let handle = match handle.try_into() {
756 Ok(i) => i,
757 Err(_) => {
758 log::debug!("Attempted to get a descriptor with handle {}", handle);
759 return std::ptr::null();
760 }
761 };
762
763 Worker::with_active_host(
764 |host| match thread.descriptor_table_borrow(host).get(handle) {
765 Some(d) => std::ptr::from_ref(d),
766 None => std::ptr::null(),
767 },
768 )
769 .unwrap()
770 }
771
772 #[unsafe(no_mangle)]
774 pub extern "C-unwind" fn thread_getRegisteredDescriptorMut(
775 thread: *const Thread,
776 handle: libc::c_int,
777 ) -> *mut Descriptor {
778 let thread = unsafe { thread.as_ref().unwrap() };
779
780 let handle = match handle.try_into() {
781 Ok(i) => i,
782 Err(_) => {
783 log::debug!("Attempted to get a descriptor with handle {}", handle);
784 return std::ptr::null_mut();
785 }
786 };
787
788 Worker::with_active_host(|host| {
789 match thread.descriptor_table_borrow_mut(host).get_mut(handle) {
790 Some(d) => d as *mut Descriptor,
791 None => std::ptr::null_mut(),
792 }
793 })
794 .unwrap()
795 }
796
797 #[unsafe(no_mangle)]
799 pub unsafe extern "C-unwind" fn thread_getRegisteredLegacyFile(
800 thread: *const Thread,
801 handle: libc::c_int,
802 ) -> *mut c::LegacyFile {
803 let thread = unsafe { thread.as_ref().unwrap() };
804
805 let handle = match handle.try_into() {
806 Ok(i) => i,
807 Err(_) => {
808 log::debug!("Attempted to get a descriptor with handle {}", handle);
809 return std::ptr::null_mut();
810 }
811 };
812
813 Worker::with_active_host(|host| {
814 match thread.descriptor_table_borrow(host).get(handle).map(|x| x.file()) {
815 Some(CompatFile::Legacy(file)) => file.ptr(),
816 Some(CompatFile::New(file)) => {
817 if let File::Socket(Socket::Inet(InetSocket::LegacyTcp(tcp))) = file.inner_file() {
819 tcp.borrow().as_legacy_file()
820 } else {
821 log::warn!(
822 "A descriptor exists for fd={}, but it is not a legacy file. Returning NULL.",
823 handle
824 );
825 std::ptr::null_mut()
826 }
827 }
828 None => std::ptr::null_mut(),
829 }
830 }).unwrap()
831 }
832}