1/* 2 Unix SMB/CIFS implementation. 3 Infrastructure for async winbind requests 4 Copyright (C) Volker Lendecke 2008 5 6 ** NOTE! The following LGPL license applies to the wbclient 7 ** library. This does NOT imply that all of Samba is released 8 ** under the LGPL 9 10 This library is free software; you can redistribute it and/or 11 modify it under the terms of the GNU Lesser General Public 12 License as published by the Free Software Foundation; either 13 version 3 of the License, or (at your option) any later version. 14 15 This library is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 Library General Public License for more details. 19 20 You should have received a copy of the GNU Lesser General Public License 21 along with this program. If not, see <http://www.gnu.org/licenses/>. 22*/ 23 24#include "replace.h" 25#include "system/filesys.h" 26#include "system/network.h" 27#include <talloc.h> 28#include <tevent.h> 29#include "lib/async_req/async_sock.h" 30#include "nsswitch/winbind_struct_protocol.h" 31#include "nsswitch/libwbclient/wbclient.h" 32#include "nsswitch/libwbclient/wbc_async.h" 33 34wbcErr map_wbc_err_from_errno(int error) 35{ 36 switch(error) { 37 case EPERM: 38 case EACCES: 39 return WBC_ERR_AUTH_ERROR; 40 case ENOMEM: 41 return WBC_ERR_NO_MEMORY; 42 case EIO: 43 default: 44 return WBC_ERR_UNKNOWN_FAILURE; 45 } 46} 47 48bool tevent_req_is_wbcerr(struct tevent_req *req, wbcErr *pwbc_err) 49{ 50 enum tevent_req_state state; 51 uint64_t error; 52 if (!tevent_req_is_error(req, &state, &error)) { 53 *pwbc_err = WBC_ERR_SUCCESS; 54 return false; 55 } 56 57 switch (state) { 58 case TEVENT_REQ_USER_ERROR: 59 *pwbc_err = error; 60 break; 61 case TEVENT_REQ_TIMED_OUT: 62 *pwbc_err = WBC_ERR_UNKNOWN_FAILURE; 63 break; 64 case TEVENT_REQ_NO_MEMORY: 65 *pwbc_err = WBC_ERR_NO_MEMORY; 66 break; 67 default: 68 *pwbc_err = WBC_ERR_UNKNOWN_FAILURE; 69 break; 70 } 71 return true; 72} 73 74wbcErr tevent_req_simple_recv_wbcerr(struct tevent_req *req) 75{ 76 wbcErr wbc_err; 77 78 if (tevent_req_is_wbcerr(req, &wbc_err)) { 79 return wbc_err; 80 } 81 82 return WBC_ERR_SUCCESS; 83} 84 85struct wbc_debug_ops { 86 void (*debug)(void *context, enum wbcDebugLevel level, 87 const char *fmt, va_list ap) PRINTF_ATTRIBUTE(3,0); 88 void *context; 89}; 90 91struct wb_context { 92 struct tevent_queue *queue; 93 int fd; 94 bool is_priv; 95 const char *dir; 96 struct wbc_debug_ops debug_ops; 97}; 98 99static int make_nonstd_fd(int fd) 100{ 101 int i; 102 int sys_errno = 0; 103 int fds[3]; 104 int num_fds = 0; 105 106 if (fd == -1) { 107 return -1; 108 } 109 while (fd < 3) { 110 fds[num_fds++] = fd; 111 fd = dup(fd); 112 if (fd == -1) { 113 sys_errno = errno; 114 break; 115 } 116 } 117 for (i=0; i<num_fds; i++) { 118 close(fds[i]); 119 } 120 if (fd == -1) { 121 errno = sys_errno; 122 } 123 return fd; 124} 125 126/**************************************************************************** 127 Set a fd into blocking/nonblocking mode. Uses POSIX O_NONBLOCK if available, 128 else 129 if SYSV use O_NDELAY 130 if BSD use FNDELAY 131 Set close on exec also. 132****************************************************************************/ 133 134static int make_safe_fd(int fd) 135{ 136 int result, flags; 137 int new_fd = make_nonstd_fd(fd); 138 139 if (new_fd == -1) { 140 goto fail; 141 } 142 143 /* Socket should be nonblocking. */ 144 145#ifdef O_NONBLOCK 146#define FLAG_TO_SET O_NONBLOCK 147#else 148#ifdef SYSV 149#define FLAG_TO_SET O_NDELAY 150#else /* BSD */ 151#define FLAG_TO_SET FNDELAY 152#endif 153#endif 154 155 if ((flags = fcntl(new_fd, F_GETFL)) == -1) { 156 goto fail; 157 } 158 159 flags |= FLAG_TO_SET; 160 if (fcntl(new_fd, F_SETFL, flags) == -1) { 161 goto fail; 162 } 163 164#undef FLAG_TO_SET 165 166 /* Socket should be closed on exec() */ 167#ifdef FD_CLOEXEC 168 result = flags = fcntl(new_fd, F_GETFD, 0); 169 if (flags >= 0) { 170 flags |= FD_CLOEXEC; 171 result = fcntl( new_fd, F_SETFD, flags ); 172 } 173 if (result < 0) { 174 goto fail; 175 } 176#endif 177 return new_fd; 178 179 fail: 180 if (new_fd != -1) { 181 int sys_errno = errno; 182 close(new_fd); 183 errno = sys_errno; 184 } 185 return -1; 186} 187 188/* Just put a prototype to avoid moving the whole function around */ 189static const char *winbindd_socket_dir(void); 190 191struct wb_context *wb_context_init(TALLOC_CTX *mem_ctx, const char* dir) 192{ 193 struct wb_context *result; 194 195 result = talloc(mem_ctx, struct wb_context); 196 if (result == NULL) { 197 return NULL; 198 } 199 result->queue = tevent_queue_create(result, "wb_trans"); 200 if (result->queue == NULL) { 201 TALLOC_FREE(result); 202 return NULL; 203 } 204 result->fd = -1; 205 result->is_priv = false; 206 207 if (dir != NULL) { 208 result->dir = talloc_strdup(result, dir); 209 } else { 210 result->dir = winbindd_socket_dir(); 211 } 212 if (result->dir == NULL) { 213 TALLOC_FREE(result); 214 return NULL; 215 } 216 return result; 217} 218 219struct wb_connect_state { 220 int dummy; 221}; 222 223static void wbc_connect_connected(struct tevent_req *subreq); 224 225static struct tevent_req *wb_connect_send(TALLOC_CTX *mem_ctx, 226 struct tevent_context *ev, 227 struct wb_context *wb_ctx, 228 const char *dir) 229{ 230 struct tevent_req *result, *subreq; 231 struct wb_connect_state *state; 232 struct sockaddr_un sunaddr; 233 struct stat st; 234 char *path = NULL; 235 wbcErr wbc_err; 236 237 result = tevent_req_create(mem_ctx, &state, struct wb_connect_state); 238 if (result == NULL) { 239 return NULL; 240 } 241 242 if (wb_ctx->fd != -1) { 243 close(wb_ctx->fd); 244 wb_ctx->fd = -1; 245 } 246 247 /* Check permissions on unix socket directory */ 248 249 if (lstat(dir, &st) == -1) { 250 wbc_err = WBC_ERR_WINBIND_NOT_AVAILABLE; 251 goto post_status; 252 } 253 254 if (!S_ISDIR(st.st_mode) || 255 (st.st_uid != 0 && st.st_uid != geteuid())) { 256 wbc_err = WBC_ERR_WINBIND_NOT_AVAILABLE; 257 goto post_status; 258 } 259 260 /* Connect to socket */ 261 262 path = talloc_asprintf(mem_ctx, "%s/%s", dir, 263 WINBINDD_SOCKET_NAME); 264 if (path == NULL) { 265 goto nomem; 266 } 267 268 sunaddr.sun_family = AF_UNIX; 269 strlcpy(sunaddr.sun_path, path, sizeof(sunaddr.sun_path)); 270 TALLOC_FREE(path); 271 272 /* If socket file doesn't exist, don't bother trying to connect 273 with retry. This is an attempt to make the system usable when 274 the winbindd daemon is not running. */ 275 276 if ((lstat(sunaddr.sun_path, &st) == -1) 277 || !S_ISSOCK(st.st_mode) 278 || (st.st_uid != 0 && st.st_uid != geteuid())) { 279 wbc_err = WBC_ERR_WINBIND_NOT_AVAILABLE; 280 goto post_status; 281 } 282 283 wb_ctx->fd = make_safe_fd(socket(AF_UNIX, SOCK_STREAM, 0)); 284 if (wb_ctx->fd == -1) { 285 wbc_err = map_wbc_err_from_errno(errno); 286 goto post_status; 287 } 288 289 subreq = async_connect_send(mem_ctx, ev, wb_ctx->fd, 290 (struct sockaddr *)(void *)&sunaddr, 291 sizeof(sunaddr)); 292 if (subreq == NULL) { 293 goto nomem; 294 } 295 tevent_req_set_callback(subreq, wbc_connect_connected, result); 296 return result; 297 298 post_status: 299 tevent_req_error(result, wbc_err); 300 return tevent_req_post(result, ev); 301 nomem: 302 TALLOC_FREE(result); 303 return NULL; 304} 305 306static void wbc_connect_connected(struct tevent_req *subreq) 307{ 308 struct tevent_req *req = tevent_req_callback_data( 309 subreq, struct tevent_req); 310 int res, err; 311 312 res = async_connect_recv(subreq, &err); 313 TALLOC_FREE(subreq); 314 if (res == -1) { 315 tevent_req_error(req, map_wbc_err_from_errno(err)); 316 return; 317 } 318 tevent_req_done(req); 319} 320 321static wbcErr wb_connect_recv(struct tevent_req *req) 322{ 323 return tevent_req_simple_recv_wbcerr(req); 324} 325 326static const char *winbindd_socket_dir(void) 327{ 328#ifdef SOCKET_WRAPPER 329 const char *env_dir; 330 331 env_dir = getenv(WINBINDD_SOCKET_DIR_ENVVAR); 332 if (env_dir) { 333 return env_dir; 334 } 335#endif 336 337 return WINBINDD_SOCKET_DIR; 338} 339 340struct wb_open_pipe_state { 341 struct wb_context *wb_ctx; 342 struct tevent_context *ev; 343 bool need_priv; 344 struct winbindd_request wb_req; 345}; 346 347static void wb_open_pipe_connect_nonpriv_done(struct tevent_req *subreq); 348static void wb_open_pipe_ping_done(struct tevent_req *subreq); 349static void wb_open_pipe_getpriv_done(struct tevent_req *subreq); 350static void wb_open_pipe_connect_priv_done(struct tevent_req *subreq); 351 352static struct tevent_req *wb_open_pipe_send(TALLOC_CTX *mem_ctx, 353 struct tevent_context *ev, 354 struct wb_context *wb_ctx, 355 bool need_priv) 356{ 357 struct tevent_req *result, *subreq; 358 struct wb_open_pipe_state *state; 359 360 result = tevent_req_create(mem_ctx, &state, struct wb_open_pipe_state); 361 if (result == NULL) { 362 return NULL; 363 } 364 state->wb_ctx = wb_ctx; 365 state->ev = ev; 366 state->need_priv = need_priv; 367 368 if (wb_ctx->fd != -1) { 369 close(wb_ctx->fd); 370 wb_ctx->fd = -1; 371 } 372 373 subreq = wb_connect_send(state, ev, wb_ctx, wb_ctx->dir); 374 if (subreq == NULL) { 375 goto fail; 376 } 377 tevent_req_set_callback(subreq, wb_open_pipe_connect_nonpriv_done, 378 result); 379 return result; 380 381 fail: 382 TALLOC_FREE(result); 383 return NULL; 384} 385 386static void wb_open_pipe_connect_nonpriv_done(struct tevent_req *subreq) 387{ 388 struct tevent_req *req = tevent_req_callback_data( 389 subreq, struct tevent_req); 390 struct wb_open_pipe_state *state = tevent_req_data( 391 req, struct wb_open_pipe_state); 392 wbcErr wbc_err; 393 394 wbc_err = wb_connect_recv(subreq); 395 TALLOC_FREE(subreq); 396 if (!WBC_ERROR_IS_OK(wbc_err)) { 397 state->wb_ctx->is_priv = true; 398 tevent_req_error(req, wbc_err); 399 return; 400 } 401 402 ZERO_STRUCT(state->wb_req); 403 state->wb_req.cmd = WINBINDD_INTERFACE_VERSION; 404 state->wb_req.pid = getpid(); 405 406 subreq = wb_simple_trans_send(state, state->ev, NULL, 407 state->wb_ctx->fd, &state->wb_req); 408 if (tevent_req_nomem(subreq, req)) { 409 return; 410 } 411 tevent_req_set_callback(subreq, wb_open_pipe_ping_done, req); 412} 413 414static void wb_open_pipe_ping_done(struct tevent_req *subreq) 415{ 416 struct tevent_req *req = tevent_req_callback_data( 417 subreq, struct tevent_req); 418 struct wb_open_pipe_state *state = tevent_req_data( 419 req, struct wb_open_pipe_state); 420 struct winbindd_response *wb_resp; 421 int ret, err; 422 423 ret = wb_simple_trans_recv(subreq, state, &wb_resp, &err); 424 TALLOC_FREE(subreq); 425 if (ret == -1) { 426 tevent_req_error(req, map_wbc_err_from_errno(err)); 427 return; 428 } 429 430 if (!state->need_priv) { 431 tevent_req_done(req); 432 return; 433 } 434 435 state->wb_req.cmd = WINBINDD_PRIV_PIPE_DIR; 436 state->wb_req.pid = getpid(); 437 438 subreq = wb_simple_trans_send(state, state->ev, NULL, 439 state->wb_ctx->fd, &state->wb_req); 440 if (tevent_req_nomem(subreq, req)) { 441 return; 442 } 443 tevent_req_set_callback(subreq, wb_open_pipe_getpriv_done, req); 444} 445 446static void wb_open_pipe_getpriv_done(struct tevent_req *subreq) 447{ 448 struct tevent_req *req = tevent_req_callback_data( 449 subreq, struct tevent_req); 450 struct wb_open_pipe_state *state = tevent_req_data( 451 req, struct wb_open_pipe_state); 452 struct winbindd_response *wb_resp = NULL; 453 int ret, err; 454 455 ret = wb_simple_trans_recv(subreq, state, &wb_resp, &err); 456 TALLOC_FREE(subreq); 457 if (ret == -1) { 458 tevent_req_error(req, map_wbc_err_from_errno(err)); 459 return; 460 } 461 462 close(state->wb_ctx->fd); 463 state->wb_ctx->fd = -1; 464 465 subreq = wb_connect_send(state, state->ev, state->wb_ctx, 466 (char *)wb_resp->extra_data.data); 467 TALLOC_FREE(wb_resp); 468 if (tevent_req_nomem(subreq, req)) { 469 return; 470 } 471 tevent_req_set_callback(subreq, wb_open_pipe_connect_priv_done, req); 472} 473 474static void wb_open_pipe_connect_priv_done(struct tevent_req *subreq) 475{ 476 struct tevent_req *req = tevent_req_callback_data( 477 subreq, struct tevent_req); 478 struct wb_open_pipe_state *state = tevent_req_data( 479 req, struct wb_open_pipe_state); 480 wbcErr wbc_err; 481 482 wbc_err = wb_connect_recv(subreq); 483 TALLOC_FREE(subreq); 484 if (!WBC_ERROR_IS_OK(wbc_err)) { 485 tevent_req_error(req, wbc_err); 486 return; 487 } 488 state->wb_ctx->is_priv = true; 489 tevent_req_done(req); 490} 491 492static wbcErr wb_open_pipe_recv(struct tevent_req *req) 493{ 494 return tevent_req_simple_recv_wbcerr(req); 495} 496 497struct wb_trans_state { 498 struct wb_trans_state *prev, *next; 499 struct wb_context *wb_ctx; 500 struct tevent_context *ev; 501 struct winbindd_request *wb_req; 502 struct winbindd_response *wb_resp; 503 bool need_priv; 504}; 505 506static bool closed_fd(int fd) 507{ 508 struct timeval tv; 509 fd_set r_fds; 510 int selret; 511 512 if (fd < 0 || fd >= FD_SETSIZE) { 513 return true; 514 } 515 516 FD_ZERO(&r_fds); 517 FD_SET(fd, &r_fds); 518 ZERO_STRUCT(tv); 519 520 selret = select(fd+1, &r_fds, NULL, NULL, &tv); 521 if (selret == -1) { 522 return true; 523 } 524 if (selret == 0) { 525 return false; 526 } 527 return (FD_ISSET(fd, &r_fds)); 528} 529 530static void wb_trans_trigger(struct tevent_req *req, void *private_data); 531static void wb_trans_connect_done(struct tevent_req *subreq); 532static void wb_trans_done(struct tevent_req *subreq); 533static void wb_trans_retry_wait_done(struct tevent_req *subreq); 534 535struct tevent_req *wb_trans_send(TALLOC_CTX *mem_ctx, 536 struct tevent_context *ev, 537 struct wb_context *wb_ctx, bool need_priv, 538 struct winbindd_request *wb_req) 539{ 540 struct tevent_req *req; 541 struct wb_trans_state *state; 542 543 req = tevent_req_create(mem_ctx, &state, struct wb_trans_state); 544 if (req == NULL) { 545 return NULL; 546 } 547 state->wb_ctx = wb_ctx; 548 state->ev = ev; 549 state->wb_req = wb_req; 550 state->need_priv = need_priv; 551 552 if (!tevent_queue_add(wb_ctx->queue, ev, req, wb_trans_trigger, 553 NULL)) { 554 tevent_req_nomem(NULL, req); 555 return tevent_req_post(req, ev); 556 } 557 return req; 558} 559 560static void wb_trans_trigger(struct tevent_req *req, void *private_data) 561{ 562 struct wb_trans_state *state = tevent_req_data( 563 req, struct wb_trans_state); 564 struct tevent_req *subreq; 565 566 if ((state->wb_ctx->fd != -1) && closed_fd(state->wb_ctx->fd)) { 567 close(state->wb_ctx->fd); 568 state->wb_ctx->fd = -1; 569 } 570 571 if ((state->wb_ctx->fd == -1) 572 || (state->need_priv && !state->wb_ctx->is_priv)) { 573 subreq = wb_open_pipe_send(state, state->ev, state->wb_ctx, 574 state->need_priv); 575 if (tevent_req_nomem(subreq, req)) { 576 return; 577 } 578 tevent_req_set_callback(subreq, wb_trans_connect_done, req); 579 return; 580 } 581 582 state->wb_req->pid = getpid(); 583 584 subreq = wb_simple_trans_send(state, state->ev, NULL, 585 state->wb_ctx->fd, state->wb_req); 586 if (tevent_req_nomem(subreq, req)) { 587 return; 588 } 589 tevent_req_set_callback(subreq, wb_trans_done, req); 590} 591 592static bool wb_trans_retry(struct tevent_req *req, 593 struct wb_trans_state *state, 594 wbcErr wbc_err) 595{ 596 struct tevent_req *subreq; 597 598 if (WBC_ERROR_IS_OK(wbc_err)) { 599 return false; 600 } 601 602 if (wbc_err == WBC_ERR_WINBIND_NOT_AVAILABLE) { 603 /* 604 * Winbind not around or we can't connect to the pipe. Fail 605 * immediately. 606 */ 607 tevent_req_error(req, wbc_err); 608 return true; 609 } 610 611 /* 612 * The transfer as such failed, retry after one second 613 */ 614 615 if (state->wb_ctx->fd != -1) { 616 close(state->wb_ctx->fd); 617 state->wb_ctx->fd = -1; 618 } 619 620 subreq = tevent_wakeup_send(state, state->ev, 621 tevent_timeval_current_ofs(1, 0)); 622 if (tevent_req_nomem(subreq, req)) { 623 return true; 624 } 625 tevent_req_set_callback(subreq, wb_trans_retry_wait_done, req); 626 return true; 627} 628 629static void wb_trans_retry_wait_done(struct tevent_req *subreq) 630{ 631 struct tevent_req *req = tevent_req_callback_data( 632 subreq, struct tevent_req); 633 struct wb_trans_state *state = tevent_req_data( 634 req, struct wb_trans_state); 635 bool ret; 636 637 ret = tevent_wakeup_recv(subreq); 638 TALLOC_FREE(subreq); 639 if (!ret) { 640 tevent_req_error(req, WBC_ERR_UNKNOWN_FAILURE); 641 return; 642 } 643 644 subreq = wb_open_pipe_send(state, state->ev, state->wb_ctx, 645 state->need_priv); 646 if (tevent_req_nomem(subreq, req)) { 647 return; 648 } 649 tevent_req_set_callback(subreq, wb_trans_connect_done, req); 650} 651 652static void wb_trans_connect_done(struct tevent_req *subreq) 653{ 654 struct tevent_req *req = tevent_req_callback_data( 655 subreq, struct tevent_req); 656 struct wb_trans_state *state = tevent_req_data( 657 req, struct wb_trans_state); 658 wbcErr wbc_err; 659 660 wbc_err = wb_open_pipe_recv(subreq); 661 TALLOC_FREE(subreq); 662 663 if (wb_trans_retry(req, state, wbc_err)) { 664 return; 665 } 666 667 subreq = wb_simple_trans_send(state, state->ev, NULL, 668 state->wb_ctx->fd, state->wb_req); 669 if (tevent_req_nomem(subreq, req)) { 670 return; 671 } 672 tevent_req_set_callback(subreq, wb_trans_done, req); 673} 674 675static void wb_trans_done(struct tevent_req *subreq) 676{ 677 struct tevent_req *req = tevent_req_callback_data( 678 subreq, struct tevent_req); 679 struct wb_trans_state *state = tevent_req_data( 680 req, struct wb_trans_state); 681 int ret, err; 682 683 ret = wb_simple_trans_recv(subreq, state, &state->wb_resp, &err); 684 TALLOC_FREE(subreq); 685 if ((ret == -1) 686 && wb_trans_retry(req, state, map_wbc_err_from_errno(err))) { 687 return; 688 } 689 690 tevent_req_done(req); 691} 692 693wbcErr wb_trans_recv(struct tevent_req *req, TALLOC_CTX *mem_ctx, 694 struct winbindd_response **presponse) 695{ 696 struct wb_trans_state *state = tevent_req_data( 697 req, struct wb_trans_state); 698 wbcErr wbc_err; 699 700 if (tevent_req_is_wbcerr(req, &wbc_err)) { 701 return wbc_err; 702 } 703 704 *presponse = talloc_move(mem_ctx, &state->wb_resp); 705 return WBC_ERR_SUCCESS; 706} 707 708/******************************************************************** 709 * Debug wrapper functions, modeled (with lot's of code copied as is) 710 * after the tevent debug wrapper functions 711 ********************************************************************/ 712 713/* 714 this allows the user to choose their own debug function 715*/ 716int wbcSetDebug(struct wb_context *wb_ctx, 717 void (*debug)(void *context, 718 enum wbcDebugLevel level, 719 const char *fmt, 720 va_list ap) PRINTF_ATTRIBUTE(3,0), 721 void *context) 722{ 723 wb_ctx->debug_ops.debug = debug; 724 wb_ctx->debug_ops.context = context; 725 return 0; 726} 727 728/* 729 debug function for wbcSetDebugStderr 730*/ 731static void wbcDebugStderr(void *private_data, 732 enum wbcDebugLevel level, 733 const char *fmt, 734 va_list ap) PRINTF_ATTRIBUTE(3,0); 735static void wbcDebugStderr(void *private_data, 736 enum wbcDebugLevel level, 737 const char *fmt, va_list ap) 738{ 739 if (level <= WBC_DEBUG_WARNING) { 740 vfprintf(stderr, fmt, ap); 741 } 742} 743 744/* 745 convenience function to setup debug messages on stderr 746 messages of level WBC_DEBUG_WARNING and higher are printed 747*/ 748int wbcSetDebugStderr(struct wb_context *wb_ctx) 749{ 750 return wbcSetDebug(wb_ctx, wbcDebugStderr, wb_ctx); 751} 752 753/* 754 * log a message 755 * 756 * The default debug action is to ignore debugging messages. 757 * This is the most appropriate action for a library. 758 * Applications using the library must decide where to 759 * redirect debugging messages 760*/ 761void wbcDebug(struct wb_context *wb_ctx, enum wbcDebugLevel level, 762 const char *fmt, ...) 763{ 764 va_list ap; 765 if (!wb_ctx) { 766 return; 767 } 768 if (wb_ctx->debug_ops.debug == NULL) { 769 return; 770 } 771 va_start(ap, fmt); 772 wb_ctx->debug_ops.debug(wb_ctx->debug_ops.context, level, fmt, ap); 773 va_end(ap); 774} 775