• Home
  • History
  • Annotate
  • Line#
  • Navigate
  • Raw
  • Download
  • only in /asuswrt-rt-n18u-9.0.0.4.380.2695/release/src-rt/router/samba-3.5.8/nsswitch/libwbclient/
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