1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 /* 3 * Copyright 2004 Sun Microsystems, Inc. All rights reserved. 4 * Use is subject to license terms. 5 */ 6 7 #include <sys/stat.h> 8 #include <sys/types.h> 9 #include <unistd.h> 10 #include <fcntl.h> 11 #include <sys/mman.h> 12 #include <k5-int.h> 13 #include <stdlib.h> 14 #include <limits.h> 15 #include <syslog.h> 16 #include "kdb5.h" 17 #include "kdb_log.h" 18 #include "kdb5int.h" 19 20 #ifndef MAP_FAILED 21 #define MAP_FAILED ((void *)-1) 22 #endif 23 24 /* This module includes all the necessary functions that create and modify the 25 * Kerberos principal update and header logs. */ 26 27 #define getpagesize() sysconf(_SC_PAGESIZE) 28 29 static int pagesize = 0; 30 31 #define INIT_ULOG(ctx) \ 32 log_ctx = ctx->kdblog_context; \ 33 assert(log_ctx != NULL); \ 34 ulog = log_ctx->ulog; \ 35 assert(ulog != NULL) 36 37 /* Initialize context->kdblog_context if it does not yet exist, and return it. 38 * Return NULL on allocation failure. */ 39 static kdb_log_context * 40 create_log_context(krb5_context context) 41 { 42 kdb_log_context *log_ctx; 43 44 if (context->kdblog_context != NULL) 45 return context->kdblog_context; 46 log_ctx = calloc(1, sizeof(*log_ctx)); 47 if (log_ctx == NULL) 48 return NULL; 49 log_ctx->ulogfd = -1; 50 context->kdblog_context = log_ctx; 51 return log_ctx; 52 } 53 54 static inline krb5_boolean 55 time_equal(const kdbe_time_t *a, const kdbe_time_t *b) 56 { 57 return a->seconds == b->seconds && a->useconds == b->useconds; 58 } 59 60 static void 61 time_current(kdbe_time_t *out) 62 { 63 struct timeval timestamp; 64 65 (void)gettimeofday(×tamp, NULL); 66 out->seconds = timestamp.tv_sec; 67 out->useconds = timestamp.tv_usec; 68 } 69 70 /* Sync update entry to disk. */ 71 static void 72 sync_update(kdb_hlog_t *ulog, kdb_ent_header_t *upd) 73 { 74 unsigned long start, end, size; 75 76 if (!pagesize) 77 pagesize = getpagesize(); 78 79 start = (unsigned long)upd & ~(pagesize - 1); 80 81 end = ((unsigned long)upd + ulog->kdb_block + (pagesize - 1)) & 82 ~(pagesize - 1); 83 84 size = end - start; 85 if (msync((caddr_t)start, size, MS_SYNC)) { 86 /* Couldn't sync to disk, let's panic. */ 87 syslog(LOG_ERR, _("could not sync ulog update to disk")); 88 abort(); 89 } 90 } 91 92 /* Sync memory to disk for the update log header. */ 93 static void 94 sync_header(kdb_hlog_t *ulog) 95 { 96 if (!pagesize) 97 pagesize = getpagesize(); 98 99 if (msync((caddr_t)ulog, pagesize, MS_SYNC)) { 100 /* Couldn't sync to disk, let's panic. */ 101 syslog(LOG_ERR, _("could not sync ulog header to disk")); 102 abort(); 103 } 104 } 105 106 /* Return true if the ulog entry for sno matches sno and timestamp. */ 107 static krb5_boolean 108 check_sno(kdb_log_context *log_ctx, kdb_sno_t sno, 109 const kdbe_time_t *timestamp) 110 { 111 unsigned int indx = (sno - 1) % log_ctx->ulogentries; 112 kdb_ent_header_t *ent = INDEX(log_ctx->ulog, indx); 113 114 return ent->kdb_entry_sno == sno && time_equal(&ent->kdb_time, timestamp); 115 } 116 117 /* 118 * Check last against our ulog and determine whether it is up to date 119 * (UPDATE_NIL), so far out of date that a full dump is required 120 * (UPDATE_FULL_RESYNC_NEEDED), or okay to update with ulog entries 121 * (UPDATE_OK). 122 */ 123 static update_status_t 124 get_sno_status(kdb_log_context *log_ctx, const kdb_last_t *last) 125 { 126 kdb_hlog_t *ulog = log_ctx->ulog; 127 128 /* If last matches the ulog's last serial number and time exactly, it are 129 * up to date even if the ulog is empty. */ 130 if (last->last_sno == ulog->kdb_last_sno && 131 time_equal(&last->last_time, &ulog->kdb_last_time)) 132 return UPDATE_NIL; 133 134 /* If our ulog is empty or does not contain last_sno, a full resync is 135 * required. */ 136 if (ulog->kdb_num == 0 || last->last_sno > ulog->kdb_last_sno || 137 last->last_sno < ulog->kdb_first_sno) 138 return UPDATE_FULL_RESYNC_NEEDED; 139 140 /* If the timestamp in our ulog entry does not match last, then sno was 141 * reused and a full resync is required. */ 142 if (!check_sno(log_ctx, last->last_sno, &last->last_time)) 143 return UPDATE_FULL_RESYNC_NEEDED; 144 145 /* last is not fully up to date, but can be updated using our ulog. */ 146 return UPDATE_OK; 147 } 148 149 /* Extend update log file. */ 150 static krb5_error_code 151 extend_file_to(int fd, unsigned int new_size) 152 { 153 off_t current_offset; 154 static const char zero[512]; 155 ssize_t wrote_size; 156 size_t write_size; 157 158 current_offset = lseek(fd, 0, SEEK_END); 159 if (current_offset < 0) 160 return errno; 161 if (new_size > INT_MAX) 162 return EINVAL; 163 while (current_offset < (off_t)new_size) { 164 write_size = new_size - current_offset; 165 if (write_size > 512) 166 write_size = 512; 167 wrote_size = write(fd, zero, write_size); 168 if (wrote_size < 0) 169 return errno; 170 if (wrote_size == 0) 171 return EINVAL; 172 current_offset += wrote_size; 173 write_size = new_size - current_offset; 174 } 175 return 0; 176 } 177 178 /* 179 * Resize the array elements. We reinitialize the update log rather than 180 * unrolling the the log and copying it over to a temporary log for obvious 181 * performance reasons. Replicas will subsequently do a full resync, but the 182 * need for resizing should be very small. 183 */ 184 static krb5_error_code 185 resize(kdb_hlog_t *ulog, uint32_t ulogentries, int ulogfd, 186 unsigned int recsize) 187 { 188 unsigned int new_block, new_size; 189 190 if (ulog == NULL) 191 return KRB5_LOG_ERROR; 192 193 new_size = sizeof(kdb_hlog_t); 194 new_block = (recsize / ULOG_BLOCK) + 1; 195 new_block *= ULOG_BLOCK; 196 new_size += ulogentries * new_block; 197 198 if (new_size > MAXLOGLEN) 199 return KRB5_LOG_ERROR; 200 201 /* Reinit log with new block size. */ 202 memset(ulog, 0, sizeof(*ulog)); 203 ulog->kdb_hmagic = KDB_ULOG_HDR_MAGIC; 204 ulog->db_version_num = KDB_VERSION; 205 ulog->kdb_state = KDB_STABLE; 206 ulog->kdb_block = new_block; 207 sync_header(ulog); 208 209 /* Expand log considering new block size. */ 210 return extend_file_to(ulogfd, new_size); 211 } 212 213 /* Set the ulog to contain only a dummy entry with the given serial number and 214 * timestamp. */ 215 static void 216 set_dummy(kdb_log_context *log_ctx, kdb_sno_t sno, const kdbe_time_t *kdb_time) 217 { 218 kdb_hlog_t *ulog = log_ctx->ulog; 219 kdb_ent_header_t *ent = INDEX(ulog, (sno - 1) % log_ctx->ulogentries); 220 221 memset(ent, 0, sizeof(*ent)); 222 ent->kdb_umagic = KDB_ULOG_MAGIC; 223 ent->kdb_entry_sno = sno; 224 ent->kdb_time = *kdb_time; 225 sync_update(ulog, ent); 226 227 ulog->kdb_num = 1; 228 ulog->kdb_first_sno = ulog->kdb_last_sno = sno; 229 ulog->kdb_first_time = ulog->kdb_last_time = *kdb_time; 230 } 231 232 /* Reinitialize the ulog header, starting from sno 1 with the current time. */ 233 static void 234 reset_ulog(kdb_log_context *log_ctx) 235 { 236 kdbe_time_t kdb_time; 237 kdb_hlog_t *ulog = log_ctx->ulog; 238 239 memset(ulog, 0, sizeof(*ulog)); 240 ulog->kdb_hmagic = KDB_ULOG_HDR_MAGIC; 241 ulog->db_version_num = KDB_VERSION; 242 ulog->kdb_block = ULOG_BLOCK; 243 244 /* Create a dummy entry to remember the timestamp for downstreams. */ 245 time_current(&kdb_time); 246 set_dummy(log_ctx, 1, &kdb_time); 247 ulog->kdb_state = KDB_STABLE; 248 sync_header(ulog); 249 } 250 251 /* 252 * If any database operations will be invoked while the ulog lock is held, the 253 * caller must explicitly lock the database before locking the ulog, or 254 * deadlock may result. 255 */ 256 static krb5_error_code 257 lock_ulog(krb5_context context, int mode) 258 { 259 kdb_log_context *log_ctx = NULL; 260 kdb_hlog_t *ulog = NULL; 261 262 INIT_ULOG(context); 263 return krb5_lock_file(context, log_ctx->ulogfd, mode); 264 } 265 266 static void 267 unlock_ulog(krb5_context context) 268 { 269 (void)lock_ulog(context, KRB5_LOCKMODE_UNLOCK); 270 } 271 272 /* 273 * Add an update to the log. The update's kdb_entry_sno and kdb_time fields 274 * must already be set. The layout of the update log looks like: 275 * 276 * header log -> [ update header -> xdr(kdb_incr_update_t) ], ... 277 */ 278 static krb5_error_code 279 store_update(kdb_log_context *log_ctx, kdb_incr_update_t *upd) 280 { 281 XDR xdrs; 282 kdb_ent_header_t *indx_log; 283 unsigned int i, recsize; 284 unsigned long upd_size; 285 krb5_error_code retval; 286 kdb_hlog_t *ulog = log_ctx->ulog; 287 uint32_t ulogentries = log_ctx->ulogentries; 288 289 upd_size = xdr_sizeof((xdrproc_t)xdr_kdb_incr_update_t, upd); 290 291 recsize = sizeof(kdb_ent_header_t) + upd_size; 292 293 if (recsize > ulog->kdb_block) { 294 retval = resize(ulog, ulogentries, log_ctx->ulogfd, recsize); 295 if (retval) 296 return retval; 297 } 298 299 ulog->kdb_state = KDB_UNSTABLE; 300 301 i = (upd->kdb_entry_sno - 1) % ulogentries; 302 indx_log = INDEX(ulog, i); 303 304 memset(indx_log, 0, ulog->kdb_block); 305 indx_log->kdb_umagic = KDB_ULOG_MAGIC; 306 indx_log->kdb_entry_size = upd_size; 307 indx_log->kdb_entry_sno = upd->kdb_entry_sno; 308 indx_log->kdb_time = upd->kdb_time; 309 indx_log->kdb_commit = FALSE; 310 311 xdrmem_create(&xdrs, (char *)indx_log->entry_data, 312 indx_log->kdb_entry_size, XDR_ENCODE); 313 if (!xdr_kdb_incr_update_t(&xdrs, upd)) 314 return KRB5_LOG_CONV; 315 316 indx_log->kdb_commit = TRUE; 317 sync_update(ulog, indx_log); 318 319 /* Modify the ulog header to reflect the new update. */ 320 ulog->kdb_last_sno = upd->kdb_entry_sno; 321 ulog->kdb_last_time = upd->kdb_time; 322 if (ulog->kdb_num == 0) { 323 /* We should only see this in old ulogs. */ 324 ulog->kdb_num = 1; 325 ulog->kdb_first_sno = upd->kdb_entry_sno; 326 ulog->kdb_first_time = upd->kdb_time; 327 } else if (ulog->kdb_num < ulogentries) { 328 ulog->kdb_num++; 329 } else { 330 /* We are circling; set kdb_first_sno and time to the next update. */ 331 i = upd->kdb_entry_sno % ulogentries; 332 indx_log = INDEX(ulog, i); 333 ulog->kdb_first_sno = indx_log->kdb_entry_sno; 334 ulog->kdb_first_time = indx_log->kdb_time; 335 } 336 337 ulog->kdb_state = KDB_STABLE; 338 sync_header(ulog); 339 return 0; 340 } 341 342 /* Add an entry to the update log. */ 343 krb5_error_code 344 ulog_add_update(krb5_context context, kdb_incr_update_t *upd) 345 { 346 krb5_error_code ret; 347 kdb_log_context *log_ctx; 348 kdb_hlog_t *ulog; 349 350 INIT_ULOG(context); 351 ret = lock_ulog(context, KRB5_LOCKMODE_EXCLUSIVE); 352 if (ret) 353 return ret; 354 355 /* If we have reached the last possible serial number, reinitialize the 356 * ulog and start over. Replicas will do a full resync. */ 357 if (ulog->kdb_last_sno == (kdb_sno_t)-1) 358 reset_ulog(log_ctx); 359 360 upd->kdb_entry_sno = ulog->kdb_last_sno + 1; 361 time_current(&upd->kdb_time); 362 ret = store_update(log_ctx, upd); 363 unlock_ulog(context); 364 return ret; 365 } 366 367 /* Used by the replica to update its hash db from the incr update log. */ 368 krb5_error_code 369 ulog_replay(krb5_context context, kdb_incr_result_t *incr_ret, char **db_args) 370 { 371 krb5_db_entry *entry = NULL; 372 kdb_incr_update_t *upd = NULL, *fupd; 373 int i, no_of_updates; 374 krb5_error_code retval; 375 krb5_principal dbprinc; 376 char *dbprincstr; 377 kdb_log_context *log_ctx; 378 kdb_hlog_t *ulog = NULL; 379 380 INIT_ULOG(context); 381 382 retval = krb5_db_open(context, db_args, 383 KRB5_KDB_OPEN_RW | KRB5_KDB_SRV_TYPE_ADMIN); 384 if (retval) 385 return retval; 386 387 no_of_updates = incr_ret->updates.kdb_ulog_t_len; 388 upd = incr_ret->updates.kdb_ulog_t_val; 389 fupd = upd; 390 391 for (i = 0; i < no_of_updates; i++) { 392 if (!upd->kdb_commit) 393 continue; 394 395 /* Replay this update in the database. */ 396 if (upd->kdb_deleted) { 397 dbprincstr = k5memdup0(upd->kdb_princ_name.utf8str_t_val, 398 upd->kdb_princ_name.utf8str_t_len, &retval); 399 if (dbprincstr == NULL) 400 goto cleanup; 401 402 retval = krb5_parse_name(context, dbprincstr, &dbprinc); 403 free(dbprincstr); 404 if (retval) 405 goto cleanup; 406 407 retval = krb5int_delete_principal_no_log(context, dbprinc); 408 krb5_free_principal(context, dbprinc); 409 if (retval == KRB5_KDB_NOENTRY) 410 retval = 0; 411 if (retval) 412 goto cleanup; 413 } else { 414 retval = ulog_conv_2dbentry(context, &entry, upd); 415 if (retval) 416 goto cleanup; 417 418 retval = krb5int_put_principal_no_log(context, entry); 419 krb5_db_free_principal(context, entry); 420 if (retval) 421 goto cleanup; 422 } 423 424 retval = lock_ulog(context, KRB5_LOCKMODE_EXCLUSIVE); 425 if (retval) 426 goto cleanup; 427 428 /* If (unexpectedly) this update does not follow the last one we 429 * stored, discard any previous ulog state. */ 430 if (ulog->kdb_num != 0 && upd->kdb_entry_sno != ulog->kdb_last_sno + 1) 431 reset_ulog(log_ctx); 432 433 /* Store this update in the ulog for any downstream KDCs. */ 434 retval = store_update(log_ctx, upd); 435 unlock_ulog(context); 436 if (retval) 437 goto cleanup; 438 439 upd++; 440 } 441 442 cleanup: 443 if (retval) 444 (void)ulog_init_header(context); 445 if (fupd) 446 ulog_free_entries(fupd, no_of_updates); 447 return retval; 448 } 449 450 /* Reinitialize the log header. */ 451 krb5_error_code 452 ulog_init_header(krb5_context context) 453 { 454 krb5_error_code ret; 455 kdb_log_context *log_ctx; 456 kdb_hlog_t *ulog; 457 458 INIT_ULOG(context); 459 ret = lock_ulog(context, KRB5_LOCKMODE_EXCLUSIVE); 460 if (ret) 461 return ret; 462 reset_ulog(log_ctx); 463 unlock_ulog(context); 464 return 0; 465 } 466 467 /* Map the log file to memory for performance and simplicity. */ 468 krb5_error_code 469 ulog_map(krb5_context context, const char *logname, uint32_t ulogentries) 470 { 471 struct stat st; 472 krb5_error_code retval; 473 uint32_t filesize; 474 kdb_log_context *log_ctx; 475 kdb_hlog_t *ulog = NULL; 476 krb5_boolean locked = FALSE; 477 478 log_ctx = create_log_context(context); 479 if (log_ctx == NULL) 480 return ENOMEM; 481 482 if (stat(logname, &st) == -1) { 483 log_ctx->ulogfd = open(logname, O_RDWR | O_CREAT, 0600); 484 if (log_ctx->ulogfd == -1) { 485 retval = errno; 486 goto cleanup; 487 } 488 489 filesize = sizeof(kdb_hlog_t) + ulogentries * ULOG_BLOCK; 490 retval = extend_file_to(log_ctx->ulogfd, filesize); 491 if (retval) 492 goto cleanup; 493 } else { 494 log_ctx->ulogfd = open(logname, O_RDWR, 0600); 495 if (log_ctx->ulogfd == -1) { 496 retval = errno; 497 goto cleanup; 498 } 499 } 500 501 ulog = mmap(0, MAXLOGLEN, PROT_READ | PROT_WRITE, MAP_SHARED, 502 log_ctx->ulogfd, 0); 503 if (ulog == MAP_FAILED) { 504 retval = errno; 505 goto cleanup; 506 } 507 log_ctx->ulog = ulog; 508 log_ctx->ulogentries = ulogentries; 509 510 retval = lock_ulog(context, KRB5_LOCKMODE_EXCLUSIVE); 511 if (retval) 512 goto cleanup; 513 locked = TRUE; 514 515 if (ulog->kdb_hmagic != KDB_ULOG_HDR_MAGIC) { 516 if (ulog->kdb_hmagic != 0) { 517 retval = KRB5_LOG_CORRUPT; 518 goto cleanup; 519 } 520 reset_ulog(log_ctx); 521 } 522 523 /* Reinit ulog if ulogentries changed such that we have too many entries or 524 * our first or last entry was written to the wrong location. */ 525 if (ulog->kdb_num != 0 && 526 (ulog->kdb_num > ulogentries || 527 !check_sno(log_ctx, ulog->kdb_first_sno, &ulog->kdb_first_time) || 528 !check_sno(log_ctx, ulog->kdb_last_sno, &ulog->kdb_last_time))) 529 reset_ulog(log_ctx); 530 531 if (ulog->kdb_num != ulogentries) { 532 /* Expand the ulog file if it isn't big enough. */ 533 filesize = sizeof(kdb_hlog_t) + ulogentries * ulog->kdb_block; 534 retval = extend_file_to(log_ctx->ulogfd, filesize); 535 if (retval) 536 goto cleanup; 537 } 538 539 cleanup: 540 if (locked) 541 unlock_ulog(context); 542 if (retval) 543 ulog_fini(context); 544 return retval; 545 } 546 547 /* Get the last set of updates seen, (last+1) to n is returned. */ 548 krb5_error_code 549 ulog_get_entries(krb5_context context, const kdb_last_t *last, 550 kdb_incr_result_t *ulog_handle) 551 { 552 XDR xdrs; 553 kdb_ent_header_t *indx_log; 554 kdb_incr_update_t *upd; 555 unsigned int indx, count; 556 uint32_t sno; 557 krb5_error_code retval; 558 kdb_log_context *log_ctx; 559 kdb_hlog_t *ulog = NULL; 560 uint32_t ulogentries; 561 562 INIT_ULOG(context); 563 ulogentries = log_ctx->ulogentries; 564 565 retval = lock_ulog(context, KRB5_LOCKMODE_SHARED); 566 if (retval) 567 return retval; 568 569 /* If another process terminated mid-update, reset the ulog and force full 570 * resyncs. */ 571 if (ulog->kdb_state != KDB_STABLE) 572 reset_ulog(log_ctx); 573 574 ulog_handle->ret = get_sno_status(log_ctx, last); 575 if (ulog_handle->ret != UPDATE_OK) 576 goto cleanup; 577 578 sno = last->last_sno; 579 count = ulog->kdb_last_sno - sno; 580 upd = calloc(count, sizeof(kdb_incr_update_t)); 581 if (upd == NULL) { 582 ulog_handle->ret = UPDATE_ERROR; 583 retval = ENOMEM; 584 goto cleanup; 585 } 586 ulog_handle->updates.kdb_ulog_t_val = upd; 587 588 for (; sno < ulog->kdb_last_sno; sno++) { 589 indx = sno % ulogentries; 590 indx_log = INDEX(ulog, indx); 591 592 memset(upd, 0, sizeof(kdb_incr_update_t)); 593 xdrmem_create(&xdrs, (char *)indx_log->entry_data, 594 indx_log->kdb_entry_size, XDR_DECODE); 595 if (!xdr_kdb_incr_update_t(&xdrs, upd)) { 596 ulog_handle->ret = UPDATE_ERROR; 597 retval = KRB5_LOG_CONV; 598 goto cleanup; 599 } 600 601 /* Mark commitment since we didn't want to decode and encode the incr 602 * update record the first time. */ 603 upd->kdb_commit = indx_log->kdb_commit; 604 upd++; 605 } 606 607 ulog_handle->updates.kdb_ulog_t_len = count; 608 609 ulog_handle->lastentry.last_sno = ulog->kdb_last_sno; 610 ulog_handle->lastentry.last_time.seconds = ulog->kdb_last_time.seconds; 611 ulog_handle->lastentry.last_time.useconds = ulog->kdb_last_time.useconds; 612 ulog_handle->ret = UPDATE_OK; 613 614 cleanup: 615 unlock_ulog(context); 616 return retval; 617 } 618 619 krb5_error_code 620 ulog_set_role(krb5_context ctx, iprop_role role) 621 { 622 if (create_log_context(ctx) == NULL) 623 return ENOMEM; 624 ctx->kdblog_context->iproprole = role; 625 return 0; 626 } 627 628 update_status_t 629 ulog_get_sno_status(krb5_context context, const kdb_last_t *last) 630 { 631 update_status_t status; 632 633 if (lock_ulog(context, KRB5_LOCKMODE_SHARED) != 0) 634 return UPDATE_ERROR; 635 status = get_sno_status(context->kdblog_context, last); 636 unlock_ulog(context); 637 return status; 638 } 639 640 krb5_error_code 641 ulog_get_last(krb5_context context, kdb_last_t *last_out) 642 { 643 krb5_error_code ret; 644 kdb_log_context *log_ctx; 645 kdb_hlog_t *ulog; 646 647 INIT_ULOG(context); 648 ret = lock_ulog(context, KRB5_LOCKMODE_SHARED); 649 if (ret) 650 return ret; 651 last_out->last_sno = log_ctx->ulog->kdb_last_sno; 652 last_out->last_time = log_ctx->ulog->kdb_last_time; 653 unlock_ulog(context); 654 return 0; 655 } 656 657 krb5_error_code 658 ulog_set_last(krb5_context context, const kdb_last_t *last) 659 { 660 krb5_error_code ret; 661 kdb_log_context *log_ctx; 662 kdb_hlog_t *ulog; 663 664 INIT_ULOG(context); 665 ret = lock_ulog(context, KRB5_LOCKMODE_EXCLUSIVE); 666 if (ret) 667 return ret; 668 669 set_dummy(log_ctx, last->last_sno, &last->last_time); 670 sync_header(ulog); 671 unlock_ulog(context); 672 return 0; 673 } 674 675 void 676 ulog_fini(krb5_context context) 677 { 678 kdb_log_context *log_ctx = context->kdblog_context; 679 680 if (log_ctx == NULL) 681 return; 682 if (log_ctx->ulog != NULL) 683 munmap(log_ctx->ulog, MAXLOGLEN); 684 if (log_ctx->ulogfd != -1) 685 close(log_ctx->ulogfd); 686 free(log_ctx); 687 context->kdblog_context = NULL; 688 } 689