jcs's openbsd hax
openbsd

Test that various calls can be interrupted in a non-threaded process, then dlopen() libpthread and do that again in a second thread, and then verify that they're all correctly acting as cancellation points.

guenther 42d9ae1f 55033df5

+707 -3
+4 -3
regress/lib/libpthread/Makefile
··· 1 - # $OpenBSD: Makefile,v 1.46 2015/09/14 08:02:59 guenther Exp $ 1 + # $OpenBSD: Makefile,v 1.47 2016/05/10 03:59:55 guenther Exp $ 2 2 3 3 # disabled because it requires a buggy behavior that uthread had: 4 4 # dup2_race ··· 12 12 # blocked_fifo 13 13 14 14 SUBDIR+= barrier blocked_shutdown \ 15 - cancel cancel2 cancel_wait close close_race closefrom cwd earlysig \ 16 - errno execve fork \ 15 + cancel cancel2 cancel_wait close close_race closefrom cwd \ 16 + dlopen \ 17 + earlysig errno execve fork \ 17 18 group netdb pcap poll preemption preemption_float \ 18 19 pthread_atfork pthread_cond_timedwait pthread_create \ 19 20 pthread_join pthread_kill pthread_mutex \
+8
regress/lib/libpthread/dlopen/Makefile
··· 1 + # $OpenBSD: Makefile,v 1.1 2016/05/10 03:59:55 guenther Exp $ 2 + 3 + PROG= dlopen 4 + 5 + .include <bsd.regress.mk> 6 + 7 + # Don't link against libpthread 8 + LDADD=
+695
regress/lib/libpthread/dlopen/dlopen.c
··· 1 + /* $OpenBSD: dlopen.c,v 1.1 2016/05/10 03:59:55 guenther Exp $ */ 2 + /* 3 + * Copyright (c) 2016 Philip Guenther <guenther@openbsd.org> 4 + * 5 + * Permission to use, copy, modify, and distribute this software for any 6 + * purpose with or without fee is hereby granted, provided that the above 7 + * copyright notice and this permission notice appear in all copies. 8 + * 9 + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 + */ 17 + 18 + /* 19 + * Test that various calls can be interrupted in a non-threaded process, 20 + * then dlopen() libpthread and do that again in a second thread, 21 + * and then verify that they're all correctly acting as cancellation points. 22 + */ 23 + 24 + #include <sys/types.h> 25 + #include <sys/ipc.h> 26 + #include <sys/sem.h> 27 + #include <sys/socket.h> 28 + #include <sys/stat.h> 29 + #include <sys/time.h> 30 + #include <sys/wait.h> 31 + 32 + #include <arpa/inet.h> 33 + #include <netinet/in.h> 34 + #include <netinet/tcp.h> 35 + 36 + #include <dlfcn.h> 37 + #include <err.h> 38 + #include <errno.h> 39 + #include <fcntl.h> 40 + #include <poll.h> 41 + #include <pthread.h> 42 + #include <signal.h> 43 + #include <stdio.h> 44 + #include <stdlib.h> 45 + #include <string.h> 46 + #include <time.h> 47 + #include <unistd.h> 48 + 49 + /* path of fifo we remove/create/open/remove */ 50 + #define FIFO_PATH "fifo" 51 + 52 + /* path of lock file remove/create/lock/remove */ 53 + #define LOCK_PATH "lock" 54 + 55 + #define TEST_ACCEPT 0x001 56 + #define TEST_CONNECT 0x002 57 + #define TEST_FCNTL 0x004 58 + #define TEST_FLOCK 0x008 59 + #define TEST_NANOSLEEP 0x010 60 + #define TEST_OPEN_FIFO 0x020 61 + #define TEST_POLL 0x040 62 + #define TEST_SIGSUSPEND 0x080 63 + #define TEST_SEMOP 0x100 64 + 65 + #define TEST_ALL 0x1ff 66 + 67 + struct test_spec 68 + { 69 + int flag; 70 + const char *name; 71 + void (*init)(void); 72 + void *(*run)(void *); 73 + void (*fini)(void); 74 + }; 75 + 76 + 77 + /* 78 + * Functions looked up in libpthread 79 + */ 80 + int (*p_attr_init)(pthread_attr_t *); 81 + int (*p_attr_setdetachstate)(pthread_attr_t *, int); 82 + int (*p_cancel)(pthread_t); 83 + int (*p_cond_destroy)(pthread_cond_t *); 84 + int (*p_cond_timedwait)(pthread_cond_t *, pthread_mutex_t *, const struct timespec *); 85 + int (*p_create)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *); 86 + int (*p_join)(pthread_t, void **); 87 + int (*p_mutex_destroy)(pthread_mutex_t *); 88 + int (*p_mutex_lock)(pthread_mutex_t *); 89 + int (*p_mutex_unlock)(pthread_mutex_t *); 90 + pthread_t (*p_self)(void); 91 + 92 + struct funcs 93 + { 94 + const char *name; 95 + void *callback; 96 + } functions[] = 97 + { 98 + #define FUNC(f) { "pthread_"#f, &p_##f } 99 + FUNC(attr_init), 100 + FUNC(attr_setdetachstate), 101 + FUNC(cancel), 102 + FUNC(cond_destroy), 103 + FUNC(cond_timedwait), 104 + FUNC(create), 105 + FUNC(join), 106 + FUNC(mutex_destroy), 107 + FUNC(mutex_lock), 108 + FUNC(mutex_unlock), 109 + FUNC(self), 110 + { NULL, NULL } 111 + #undef FUNC 112 + }; 113 + 114 + /* 115 + * Shared cleanup 116 + */ 117 + void 118 + finish(const char *msg, const int *retval, const struct timespec *tsp) 119 + { 120 + struct timespec after; 121 + const char *fill = "\t\t\t"; 122 + 123 + clock_gettime(CLOCK_REALTIME, &after); 124 + after.tv_sec -= tsp->tv_sec; 125 + after.tv_nsec -= tsp->tv_nsec; 126 + if (after.tv_nsec < 0) { 127 + after.tv_sec--; 128 + after.tv_nsec += 1000000000L; 129 + } 130 + 131 + fill += (strlen(msg) - 1) / 8; 132 + if (retval[0] >= 0) 133 + printf("%s: fail%s\ttime = %ld.%09lu\nr = %d\n", 134 + msg, fill, (long)after.tv_sec, after.tv_nsec, retval[0]); 135 + else if (retval[1] != EINTR) 136 + printf("%s: fail%s\ttime = %ld.%09lu\nr = %d\terrno = %d: %s\n", 137 + msg, fill, (long)after.tv_sec, after.tv_nsec, 138 + retval[0], retval[1], strerror(retval[1])); 139 + else 140 + printf("%s: pass%s\ttime = %ld.%09lu\n", 141 + msg, fill, (long)after.tv_sec, after.tv_nsec); 142 + } 143 + 144 + /* noop signal handler */ 145 + void 146 + sigusr1(int sig) 147 + { 148 + } 149 + 150 + /* 151 + * Interrupt via alarm() 152 + */ 153 + void 154 + sigalrm(int sig) 155 + { 156 + write(1, "* ", 2); 157 + } 158 + 159 + void 160 + set_sigalrm(int restart) 161 + { 162 + struct sigaction sa; 163 + 164 + sa.sa_handler = &sigalrm; 165 + sa.sa_flags = restart ? SA_RESTART : 0; 166 + sigemptyset(&sa.sa_mask); 167 + sigaction(SIGALRM, &sa, NULL); 168 + } 169 + 170 + void 171 + run_sig(const struct test_spec *test) 172 + { 173 + struct timespec before; 174 + int retval[2]; 175 + 176 + if (test->init != NULL) 177 + test->init(); 178 + if (clock_gettime(CLOCK_REALTIME, &before)) 179 + err(1, "clock_gettime"); 180 + alarm(1); 181 + test->run(retval); 182 + finish(test->name, retval, &before); 183 + if (test->fini != NULL) 184 + test->fini(); 185 + } 186 + 187 + 188 + /* 189 + * Interrupt via cancellation 190 + */ 191 + 192 + 193 + void 194 + run_cancel(const struct test_spec *test) 195 + { 196 + struct timespec before, target_time; 197 + pthread_t tester; 198 + pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; 199 + pthread_cond_t c = PTHREAD_COND_INITIALIZER; 200 + int retval[2]; 201 + int r; 202 + 203 + if (test->init != NULL) 204 + test->init(); 205 + 206 + if ((r = p_mutex_lock(&m))) 207 + errc(1, r, "pthread_mutex_lock"); 208 + 209 + if (clock_gettime(CLOCK_REALTIME, &before)) 210 + err(1, "clock_gettime"); 211 + 212 + target_time.tv_sec = before.tv_sec + 1; 213 + target_time.tv_nsec = before.tv_nsec; 214 + 215 + retval[0] = -2; 216 + if ((r = p_create(&tester, NULL, test->run, retval))) 217 + errc(1, r, "pthread_create"); 218 + 219 + /* overkill: could have done it with pthread_mutex_timedlock */ 220 + do 221 + r = p_cond_timedwait(&c, &m, &target_time); 222 + while (r == 0); 223 + if (r != ETIMEDOUT) 224 + errc(1, r, "pthread_cond_timedwait"); 225 + write(1, "* ", 2); 226 + if (retval[0] == -2) { 227 + retval[0] = -1; 228 + retval[1] = EINTR; 229 + } 230 + if ((r = p_cancel(tester))) 231 + errc(1, r, "pthread_cancel"); 232 + if ((r = p_mutex_unlock(&m))) 233 + errc(1, r, "pthread_mutex_unlock"); 234 + if ((r = p_mutex_destroy(&m))) 235 + errc(1, r, "pthread_mutex_destroy"); 236 + if ((r = p_cond_destroy(&c))) 237 + errc(1, r, "pthread_cond_destroy"); 238 + 239 + finish(test->name, retval, &before); 240 + if (test->fini != NULL) 241 + test->fini(); 242 + } 243 + 244 + void (*run)(const struct test_spec *_test) = run_sig; 245 + 246 + 247 + /* 248 + * The operations that are exercised in the tests 249 + */ 250 + 251 + /* 252 + * POLL 253 + */ 254 + void * 255 + poll_run(void *arg) 256 + { 257 + struct pollfd pfd; 258 + int *retval = arg; 259 + 260 + pfd.fd = 0; 261 + pfd.events = POLLIN; 262 + retval[0] = poll(&pfd, 1, 3 * 1000); 263 + retval[1] = errno; 264 + return NULL; 265 + } 266 + 267 + /* 268 + * NANOSLEEP 269 + */ 270 + void * 271 + nanosleep_run(void *arg) 272 + { 273 + struct timespec ts; 274 + int *retval = arg; 275 + 276 + ts.tv_sec = 2; 277 + ts.tv_nsec = 0; 278 + retval[0] = nanosleep(&ts, &ts); 279 + retval[1] = errno; 280 + return NULL; 281 + } 282 + 283 + /* 284 + * FCNTL 285 + */ 286 + struct flock fcntl_fl = { 287 + .l_start = 0, 288 + .l_len = 0, 289 + .l_type = F_WRLCK, 290 + .l_whence = SEEK_SET, 291 + }; 292 + static int fcntl_fd = -1; 293 + static pid_t fcntl_pid = 0; 294 + void 295 + fcntl_init(void) 296 + { 297 + int fds[2]; 298 + char buf[1]; 299 + 300 + if (unlink(LOCK_PATH) && errno != ENOENT) 301 + err(1, "unlink %s", LOCK_PATH); 302 + if (pipe(fds)) 303 + err(1, "pipe"); 304 + fcntl_fd = open(LOCK_PATH, O_RDWR | O_CREAT, 0666); 305 + fcntl_pid = fork(); 306 + if (fcntl_pid == 0) { 307 + fcntl(fcntl_fd, F_SETLKW, &fcntl_fl); 308 + close(fds[0]); 309 + close(fds[1]); 310 + sleep(1000); 311 + _exit(0); 312 + } 313 + close(fds[1]); 314 + read(fds[0], buf, 1); 315 + close(fds[0]); 316 + } 317 + void * 318 + fcntl_run(void *arg) 319 + { 320 + int *retval = arg; 321 + 322 + retval[0] = fcntl(fcntl_fd, F_SETLKW, &fcntl_fl); 323 + retval[1] = errno; 324 + return NULL; 325 + } 326 + void 327 + fcntl_fini(void) 328 + { 329 + if (fcntl_fd >= 0) { 330 + close(fcntl_fd); 331 + fcntl_fd = -1; 332 + } 333 + if (fcntl_pid > 0) { 334 + kill(fcntl_pid, SIGINT); 335 + waitpid(fcntl_pid, NULL, 0); 336 + fcntl_pid = 0; 337 + } 338 + if (unlink(LOCK_PATH)) 339 + err(1, "unlink %s", LOCK_PATH); 340 + } 341 + 342 + /* 343 + * FLOCK 344 + */ 345 + static int flock_fd = -1; 346 + static pid_t flock_pid = 0; 347 + void 348 + flock_init(void) 349 + { 350 + int fds[2]; 351 + char buf[1]; 352 + 353 + if (unlink(LOCK_PATH) && errno != ENOENT) 354 + err(1, "unlink %s", LOCK_PATH); 355 + if (pipe(fds)) 356 + err(1, "pipe"); 357 + flock_pid = fork(); 358 + flock_fd = open(LOCK_PATH, O_RDWR | O_CREAT, 0666); 359 + if (flock_pid == 0) { 360 + flock(flock_fd, LOCK_EX); 361 + close(fds[0]); 362 + close(fds[1]); 363 + sleep(1000); 364 + _exit(0); 365 + } 366 + close(fds[1]); 367 + read(fds[0], buf, 1); 368 + close(fds[0]); 369 + } 370 + void * 371 + flock_run(void *arg) 372 + { 373 + int *retval = arg; 374 + 375 + retval[0] = flock(flock_fd, LOCK_EX); 376 + retval[1] = errno; 377 + return NULL; 378 + } 379 + void 380 + flock_fini(void) 381 + { 382 + if (flock_fd >= 0) { 383 + close(flock_fd); 384 + flock_fd = -1; 385 + } 386 + if (flock_pid > 0) { 387 + kill(flock_pid, SIGINT); 388 + waitpid(flock_pid, NULL, 0); 389 + flock_pid = 0; 390 + } 391 + if (unlink(LOCK_PATH) && errno != ENOENT) 392 + err(1, "unlink %s", LOCK_PATH); 393 + } 394 + 395 + /* 396 + * SIGSUSPEND 397 + */ 398 + void * 399 + sigsuspend_run(void *arg) 400 + { 401 + sigset_t set; 402 + int *retval = arg; 403 + 404 + sigemptyset(&set); 405 + retval[0] = sigsuspend(&set); 406 + retval[1] = errno; 407 + return NULL; 408 + } 409 + 410 + /* 411 + * CONNECT 412 + */ 413 + static int connect_fd = -1; 414 + void 415 + connect_init(void) 416 + { 417 + int on = 1; 418 + 419 + connect_fd = socket(AF_INET, SOCK_STREAM, 0); 420 + setsockopt(connect_fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); 421 + setsockopt(connect_fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); 422 + } 423 + void * 424 + connect_run(void *arg) 425 + { 426 + struct sockaddr_in sin; 427 + int *retval = arg; 428 + 429 + sin.sin_family = AF_INET; 430 + inet_pton(AF_INET, "223.255.255.255", &sin.sin_addr); 431 + sin.sin_port = 25; 432 + retval[0] = connect(connect_fd, (struct sockaddr *)&sin, sizeof(sin)); 433 + retval[1] = errno; 434 + return NULL; 435 + } 436 + void 437 + connect_fini(void) 438 + { 439 + if (connect_fd >= 0) { 440 + close(connect_fd); 441 + connect_fd = -1; 442 + } 443 + } 444 + 445 + /* 446 + * ACCEPT 447 + */ 448 + static int accept_fd = -1; 449 + void 450 + accept_init(void) 451 + { 452 + accept_fd = socket(AF_INET, SOCK_STREAM, 0); 453 + listen(accept_fd, 2); 454 + } 455 + void * 456 + accept_run(void *arg) 457 + { 458 + struct sockaddr_in sin; 459 + socklen_t sl; 460 + int *retval = arg; 461 + 462 + sl = sizeof(sin); 463 + retval[0] = accept(accept_fd, (struct sockaddr *)&sin, &sl); 464 + retval[1] = errno; 465 + return NULL; 466 + } 467 + void 468 + accept_fini(void) 469 + { 470 + if (accept_fd >= 0) { 471 + close(accept_fd); 472 + accept_fd = -1; 473 + } 474 + } 475 + 476 + /* 477 + * OPEN FIFO 478 + */ 479 + void 480 + open_fifo_init(void) 481 + { 482 + /* let's get a fresh fifo */ 483 + if (unlink(FIFO_PATH) && errno != ENOENT) 484 + err(1, "unlink %s", FIFO_PATH); 485 + if (mkfifo(FIFO_PATH, 0600)) 486 + err(1, "mkfifo %s", FIFO_PATH); 487 + } 488 + void * 489 + open_fifo_run(void *arg) 490 + { 491 + int *retval = arg; 492 + 493 + retval[0] = open(FIFO_PATH, O_RDONLY); 494 + retval[1] = errno; 495 + return NULL; 496 + } 497 + void 498 + open_fifo_fini(void) 499 + { 500 + if (unlink(FIFO_PATH) && errno != ENOENT) 501 + err(1, "unlink %s", FIFO_PATH); 502 + } 503 + 504 + /* 505 + * SEMOP 506 + */ 507 + static int semid = -1; 508 + void 509 + semop_init(void) 510 + { 511 + union { 512 + int val; 513 + struct semid_ds *buf; 514 + unsigned short *array; 515 + } semarg; 516 + unsigned short val; 517 + 518 + semid = semget(IPC_PRIVATE, 1, 0600); 519 + semarg.array = &val; 520 + val = 0; 521 + semctl(semid, 0, SETALL, semarg); 522 + } 523 + void * 524 + semop_run(void *arg) 525 + { 526 + struct sembuf op; 527 + int *retval = arg; 528 + 529 + op.sem_num = 0; 530 + op.sem_op = -1; 531 + op.sem_flg = 0; 532 + retval[0] = semop(semid, &op, 1); 533 + retval[1] = errno; 534 + return NULL; 535 + } 536 + void 537 + semop_fini(void) 538 + { 539 + if (semid >= 0) { 540 + semctl(semid, 0, IPC_RMID, NULL); 541 + semid = -1; 542 + } 543 + } 544 + 545 + #define TESTSPEC_FULL(flag, name, prefix) \ 546 + { flag, name, prefix##_init, prefix##_run, prefix##_fini } 547 + #define TESTSPEC(flag, name, prefix) \ 548 + { flag, name, NULL, prefix##_run, NULL } 549 + struct test_spec test_specs[] = { 550 + TESTSPEC_FULL(TEST_ACCEPT, "accept", accept), 551 + TESTSPEC_FULL(TEST_CONNECT, "connect", connect), 552 + TESTSPEC_FULL(TEST_FCNTL, "fcntl(F_SETLKW)", fcntl), 553 + TESTSPEC_FULL(TEST_SEMOP, "semop", semop), 554 + TESTSPEC_FULL(TEST_FLOCK, "flock", flock), 555 + TESTSPEC_FULL(TEST_OPEN_FIFO, "open_fifo", open_fifo), 556 + 557 + TESTSPEC(TEST_NANOSLEEP, "nanosleep", nanosleep), 558 + TESTSPEC(TEST_POLL, "poll", poll), 559 + TESTSPEC(TEST_SIGSUSPEND, "sigsuspend", sigsuspend), 560 + { 0 } 561 + }; 562 + 563 + 564 + void * 565 + run_tests(void *arg) 566 + { 567 + int tests = *(int *)arg; 568 + int flag; 569 + struct test_spec *test; 570 + sigset_t mask; 571 + 572 + /* make sure SIGALRM is unblocked for the tests */ 573 + sigemptyset(&mask); 574 + sigaddset(&mask, SIGALRM); 575 + sigprocmask(SIG_UNBLOCK, &mask, NULL); 576 + 577 + while (tests > 0) { 578 + flag = tests & ~(tests >> 1); 579 + tests &= ~flag; 580 + for (test = test_specs; test->flag; test++) 581 + if (test->flag == flag) { 582 + run(test); 583 + break; 584 + } 585 + } 586 + 587 + return arg; 588 + } 589 + 590 + int 591 + main(int argc, char **argv) 592 + { 593 + int ch, tests; 594 + sigset_t mask; 595 + int r; 596 + void *handle; 597 + struct funcs *f; 598 + pthread_t t; 599 + void *ret; 600 + 601 + set_sigalrm(0); 602 + 603 + tests = 0; 604 + while ((ch = getopt(argc, argv, "AacFfinoprSs")) != -1) 605 + switch (ch) { 606 + case 'A': 607 + tests |= TEST_ALL; 608 + break; 609 + case 'a': 610 + tests |= TEST_ACCEPT; 611 + break; 612 + case 'c': 613 + tests |= TEST_CONNECT; 614 + break; 615 + case 'F': 616 + tests |= TEST_FCNTL; 617 + break; 618 + case 'f': 619 + tests |= TEST_FLOCK; 620 + break; 621 + case 'i': 622 + set_sigalrm(0); 623 + break; 624 + case 'n': 625 + tests |= TEST_NANOSLEEP; 626 + break; 627 + case 'o': 628 + tests |= TEST_OPEN_FIFO; 629 + break; 630 + case 'p': 631 + tests |= TEST_POLL; 632 + break; 633 + case 'r': 634 + set_sigalrm(1); 635 + break; 636 + case 's': 637 + tests |= TEST_SIGSUSPEND; 638 + break; 639 + case 'S': 640 + tests |= TEST_SEMOP; 641 + break; 642 + } 643 + if (tests == 0) 644 + tests = TEST_ALL; 645 + 646 + /* make sure SIGTERM is unblocked */ 647 + sigemptyset(&mask); 648 + sigaddset(&mask, SIGTERM); 649 + sigprocmask(SIG_UNBLOCK, &mask, NULL); 650 + 651 + /* 652 + * Run them in the original thread 653 + */ 654 + printf("single threaded\n"); 655 + run_tests(&tests); 656 + 657 + 658 + /* 659 + * Open libpthread, create a thread and run them in *that* 660 + */ 661 + if ((handle = dlopen("libpthread.so", RTLD_LAZY)) == NULL) 662 + errx(1, "dlopen: %s", dlerror()); 663 + 664 + /* look up all the functions. The cast here isn't strictly portable */ 665 + for (f = functions; f->name != NULL; f++) { 666 + if ((*(void **)f->callback = dlsym(handle, f->name)) == NULL) 667 + errx(1, "dlsym %s: %s", f->name, dlerror()); 668 + } 669 + 670 + /* block SIGALRM in the original thread */ 671 + sigemptyset(&mask); 672 + sigaddset(&mask, SIGALRM); 673 + sigprocmask(SIG_BLOCK, &mask, NULL); 674 + 675 + printf("in thread after dlopen(pthread)\n"); 676 + if ((r = p_create(&t, NULL, run_tests, &tests))) 677 + errc(1, r, "pthread_create"); 678 + if ((r = p_join(t, &ret))) 679 + errc(1, r, "pthread_join"); 680 + if (ret != &tests) 681 + errx(1, "bad return by thread: %p != %p", ret, (void *)&tests); 682 + 683 + 684 + /* 685 + * Run the tests again, this time using cancellation 686 + */ 687 + printf("using cancellation\n"); 688 + run = run_cancel; 689 + run_tests(&tests); 690 + 691 + if (dlclose(handle)) 692 + errx(1, "dlclose: %s", dlerror()); 693 + 694 + return 0; 695 + }