From 7f7ce0ec2f3e3cfb46314e5ad3ea6b5c49085f1d Mon Sep 17 00:00:00 2001 From: Ralph Boehme Date: Thu, 27 Dec 2018 16:32:46 +0100 Subject: s3:smbd: let SMB_VFS_GETXATTRAT_SEND() do explicit impersonation SMB_VFS_GETXATTRAT_SEND() gets passed a raw event context and the default implementation uses that as well a raw threadpool. Impersonation is done explicitly instead of by the tevent and pthreadpool wrappers. Signed-off-by: Ralph Boehme Reviewed-by: Volker Lendecke Reviewed-by: Stefan Metzmacher --- examples/VFS/skel_opaque.c | 3 +- examples/VFS/skel_transparent.c | 5 +- source3/include/vfs.h | 6 +- source3/include/vfs_macros.h | 8 +- source3/modules/vfs_default.c | 195 +++++++++++++++++++++++++++------- source3/modules/vfs_full_audit.c | 5 +- source3/modules/vfs_not_implemented.c | 3 +- source3/modules/vfs_time_audit.c | 5 +- source3/modules/vfs_xattr_tdb.c | 3 +- source3/smbd/smb2_query_directory.c | 6 +- source3/smbd/vfs.c | 17 +-- 11 files changed, 179 insertions(+), 77 deletions(-) diff --git a/examples/VFS/skel_opaque.c b/examples/VFS/skel_opaque.c index 054de50197e..b3cd83a3e42 100644 --- a/examples/VFS/skel_opaque.c +++ b/examples/VFS/skel_opaque.c @@ -873,14 +873,13 @@ struct skel_getxattrat_state { static struct tevent_req *skel_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, const char *xattr_name, size_t alloc_hint) { - struct tevent_context *ev = smb_vfs_ev_glue_ev_ctx(evg); struct tevent_req *req = NULL; struct skel_getxattrat_state *state = NULL; diff --git a/examples/VFS/skel_transparent.c b/examples/VFS/skel_transparent.c index cff52fa185e..4e978577837 100644 --- a/examples/VFS/skel_transparent.c +++ b/examples/VFS/skel_transparent.c @@ -1094,14 +1094,13 @@ static void skel_getxattrat_done(struct tevent_req *subreq); static struct tevent_req *skel_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, const char *xattr_name, size_t alloc_hint) { - struct tevent_context *ev = smb_vfs_ev_glue_ev_ctx(evg); struct tevent_req *req = NULL; struct skel_getxattrat_state *state = NULL; struct tevent_req *subreq = NULL; @@ -1113,7 +1112,7 @@ static struct tevent_req *skel_getxattrat_send( } subreq = SMB_VFS_NEXT_GETXATTRAT_SEND(state, - evg, + ev, handle, dir_fsp, smb_fname, diff --git a/source3/include/vfs.h b/source3/include/vfs.h index 4f3db694896..83ae6399a4a 100644 --- a/source3/include/vfs.h +++ b/source3/include/vfs.h @@ -969,7 +969,7 @@ struct vfs_fn_pointers { size_t size); struct tevent_req *(*getxattrat_send_fn)( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, @@ -1481,7 +1481,7 @@ ssize_t smb_vfs_call_getxattr(struct vfs_handle_struct *handle, size_t size); struct tevent_req *smb_vfs_call_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, @@ -1910,7 +1910,7 @@ ssize_t vfs_not_implemented_getxattr(vfs_handle_struct *handle, size_t size); struct tevent_req *vfs_not_implemented_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, diff --git a/source3/include/vfs_macros.h b/source3/include/vfs_macros.h index a13680c239e..7a0f14ef08d 100644 --- a/source3/include/vfs_macros.h +++ b/source3/include/vfs_macros.h @@ -515,18 +515,18 @@ #define SMB_VFS_NEXT_GETXATTR(handle,smb_fname,name,value,size) \ smb_vfs_call_getxattr((handle)->next,(smb_fname),(name),(value),(size)) -#define SMB_VFS_GETXATTRAT_SEND(mem_ctx,evg,dir_fsp,smb_fname, \ +#define SMB_VFS_GETXATTRAT_SEND(mem_ctx,ev,dir_fsp,smb_fname, \ xattr_name, alloc_hint) \ - smb_vfs_call_getxattrat_send((mem_ctx),(evg), \ + smb_vfs_call_getxattrat_send((mem_ctx),(ev), \ (dir_fsp)->conn->vfs_handles, \ (dir_fsp),(smb_fname),(xattr_name), \ (alloc_hint)) #define SMB_VFS_GETXATTRAT_RECV(req, aio_state, mem_ctx, xattr_value) \ smb_vfs_call_getxattrat_recv((req),(aio_state),(mem_ctx),(xattr_value)) -#define SMB_VFS_NEXT_GETXATTRAT_SEND(mem_ctx,evg,handle,dir_fsp,smb_fname, \ +#define SMB_VFS_NEXT_GETXATTRAT_SEND(mem_ctx,ev,handle,dir_fsp,smb_fname, \ xattr_name,alloc_hint) \ - smb_vfs_call_getxattrat_send((mem_ctx),(evg), \ + smb_vfs_call_getxattrat_send((mem_ctx),(ev), \ (handle)->next, \ (dir_fsp), (smb_fname),(xattr_name), \ (alloc_hint)) diff --git a/source3/modules/vfs_default.c b/source3/modules/vfs_default.c index b1c4acc482d..f54f87dc5c9 100644 --- a/source3/modules/vfs_default.c +++ b/source3/modules/vfs_default.c @@ -1493,7 +1493,7 @@ struct vfswrap_get_dos_attributes_state { struct vfs_aio_state aio_state; connection_struct *conn; TALLOC_CTX *mem_ctx; - const struct smb_vfs_ev_glue *evg; + struct tevent_context *ev; files_struct *dir_fsp; struct smb_filename *smb_fname; uint32_t dosmode; @@ -1509,7 +1509,7 @@ static struct tevent_req *vfswrap_get_dos_attributes_send( files_struct *dir_fsp, struct smb_filename *smb_fname) { - struct tevent_context *ev = smb_vfs_ev_glue_ev_ctx(evg); + struct tevent_context *ev = dir_fsp->conn->sconn->raw_ev_ctx; struct tevent_req *req = NULL; struct tevent_req *subreq = NULL; struct vfswrap_get_dos_attributes_state *state = NULL; @@ -1523,13 +1523,13 @@ static struct tevent_req *vfswrap_get_dos_attributes_send( *state = (struct vfswrap_get_dos_attributes_state) { .conn = dir_fsp->conn, .mem_ctx = mem_ctx, - .evg = evg, + .ev = ev, .dir_fsp = dir_fsp, .smb_fname = smb_fname, }; subreq = SMB_VFS_GETXATTRAT_SEND(state, - evg, + ev, dir_fsp, smb_fname, SAMBA_XATTR_DOS_ATTRIB, @@ -1562,8 +1562,6 @@ static void vfswrap_get_dos_attributes_getxattr_done(struct tevent_req *subreq) &blob.data); TALLOC_FREE(subreq); if (xattr_size == -1) { - const struct smb_vfs_ev_glue *root_evg = NULL; - status = map_nt_error_from_unix(state->aio_state.error); if (state->as_root) { @@ -1576,14 +1574,15 @@ static void vfswrap_get_dos_attributes_getxattr_done(struct tevent_req *subreq) } state->as_root = true; - root_evg = smb_vfs_ev_glue_get_root_glue(state->evg); + become_root(); subreq = SMB_VFS_GETXATTRAT_SEND(state, - root_evg, + state->ev, state->dir_fsp, state->smb_fname, SAMBA_XATTR_DOS_ATTRIB, sizeof(fstring)); + unbecome_root(); if (tevent_req_nomem(subreq, req)) { return; } @@ -2903,13 +2902,22 @@ static ssize_t vfswrap_getxattr(struct vfs_handle_struct *handle, } struct vfswrap_getxattrat_state { - int dirfd; + struct tevent_context *ev; + files_struct *dir_fsp; + const struct smb_filename *smb_fname; + struct tevent_req *req; + + /* + * The following variables are talloced off "state" which is protected + * by a destructor and thus are guaranteed to be safe to be used in the + * job function in the worker thread. + */ char *name; - size_t xattr_bufsize; const char *xattr_name; - ssize_t xattr_size; uint8_t *xattr_value; + struct security_unix_token *token; + ssize_t xattr_size; struct vfs_aio_state vfs_aio_state; SMBPROFILE_BYTES_ASYNC_STATE(profile_bytes); }; @@ -2920,23 +2928,26 @@ static int vfswrap_getxattrat_state_destructor( return -1; } -static void vfswrap_getxattrat_do(void *private_data); +static void vfswrap_getxattrat_do_sync(struct tevent_req *req); +static void vfswrap_getxattrat_do_async(void *private_data); static void vfswrap_getxattrat_done(struct tevent_req *subreq); static struct tevent_req *vfswrap_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, const char *xattr_name, size_t alloc_hint) { - struct tevent_context *ev = smb_vfs_ev_glue_ev_ctx(evg); - struct pthreadpool_tevent *tp = smb_vfs_ev_glue_tp_chdir_safe(evg); struct tevent_req *req = NULL; struct tevent_req *subreq = NULL; struct vfswrap_getxattrat_state *state = NULL; + size_t max_threads = 0; + bool have_per_thread_cwd = false; + bool have_per_thread_creds = false; + bool do_async = false; req = tevent_req_create(mem_ctx, &state, struct vfswrap_getxattrat_state); @@ -2944,19 +2955,49 @@ static struct tevent_req *vfswrap_getxattrat_send( return NULL; } *state = (struct vfswrap_getxattrat_state) { - .dirfd = dir_fsp->fh->fd, - .xattr_bufsize = alloc_hint, + .ev = ev, + .dir_fsp = dir_fsp, + .smb_fname = smb_fname, + .req = req, }; + max_threads = pthreadpool_tevent_max_threads(dir_fsp->conn->sconn->pool); + if (max_threads >= 1) { + /* + * We need a non sync threadpool! + */ + have_per_thread_cwd = per_thread_cwd_supported(); + } +#ifdef HAVE_LINUX_THREAD_CREDENTIALS + have_per_thread_creds = true; +#endif + if (have_per_thread_cwd && have_per_thread_creds) { + do_async = true; + } + SMBPROFILE_BYTES_ASYNC_START(syscall_asys_getxattrat, profile_p, state->profile_bytes, 0); - if (state->dirfd == -1) { + if (dir_fsp->fh->fd == -1) { DBG_ERR("Need a valid directory fd\n"); tevent_req_error(req, EINVAL); return tevent_req_post(req, ev); } + if (alloc_hint > 0) { + state->xattr_value = talloc_zero_array(state, + uint8_t, + alloc_hint); + if (tevent_req_nomem(state->xattr_value, req)) { + return tevent_req_post(req, ev); + } + } + + if (!do_async) { + vfswrap_getxattrat_do_sync(req); + return tevent_req_post(req, ev); + } + /* * Now allocate all parameters from a memory context that won't go away * no matter what. These paremeters will get used in threads and we @@ -2974,22 +3015,32 @@ static struct tevent_req *vfswrap_getxattrat_send( return tevent_req_post(req, ev); } - if (state->xattr_bufsize > 0) { - state->xattr_value = talloc_zero_array(state, - uint8_t, - state->xattr_bufsize); - if (tevent_req_nomem(state->xattr_value, req)) { - return tevent_req_post(req, ev); - } + /* + * This is a hot codepath so at first glance one might think we should + * somehow optimize away the token allocation and do a + * talloc_reference() or similar black magic instead. But due to the + * talloc_stackframe pool per SMB2 request this should be a simple copy + * without a malloc in most cases. + */ + if (geteuid() == sec_initial_uid()) { + state->token = root_unix_token(state); + } else { + state->token = copy_unix_token( + state, + dir_fsp->conn->session_info->unix_token); + } + if (tevent_req_nomem(state->token, req)) { + return tevent_req_post(req, ev); } SMBPROFILE_BYTES_ASYNC_SET_IDLE(state->profile_bytes); - subreq = pthreadpool_tevent_job_send(state, - ev, - tp, - vfswrap_getxattrat_do, - state); + subreq = pthreadpool_tevent_job_send( + state, + ev, + dir_fsp->conn->sconn->raw_thread_pool, + vfswrap_getxattrat_do_async, + state); if (tevent_req_nomem(subreq, req)) { return tevent_req_post(req, ev); } @@ -3000,7 +3051,43 @@ static struct tevent_req *vfswrap_getxattrat_send( return req; } -static void vfswrap_getxattrat_do(void *private_data) +static void vfswrap_getxattrat_do_sync(struct tevent_req *req) +{ + struct vfswrap_getxattrat_state *state = talloc_get_type_abort( + req, struct vfswrap_getxattrat_state); + char *path = NULL; + char *tofree = NULL; + char pathbuf[PATH_MAX+1]; + size_t pathlen; + int err; + + pathlen = full_path_tos(state->dir_fsp->fsp_name->base_name, + state->smb_fname->base_name, + pathbuf, + sizeof(pathbuf), + &path, + &tofree); + if (pathlen == -1) { + tevent_req_error(req, ENOMEM); + return; + } + + state->xattr_size = getxattr(path, + state->xattr_name, + state->xattr_value, + talloc_array_length(state->xattr_value)); + err = errno; + TALLOC_FREE(tofree); + if (state->xattr_size == -1) { + tevent_req_error(req, err); + return; + } + + tevent_req_done(req); + return; +} + +static void vfswrap_getxattrat_do_async(void *private_data) { struct vfswrap_getxattrat_state *state = talloc_get_type_abort( private_data, struct vfswrap_getxattrat_state); @@ -3014,14 +3101,22 @@ static void vfswrap_getxattrat_do(void *private_data) /* * Here we simulate a getxattrat() * call using fchdir();getxattr() - * - * We don't need to revert the directory - * change as pthreadpool_tevent wrapper - * handlers that. */ - SMB_ASSERT(pthreadpool_tevent_current_job_per_thread_cwd()); - ret = fchdir(state->dirfd); + per_thread_cwd_activate(); + + /* Become the correct credential on this thread. */ + ret = set_thread_credentials(state->token->uid, + state->token->gid, + (size_t)state->token->ngroups, + state->token->groups); + if (ret != 0) { + state->xattr_size = -1; + state->vfs_aio_state.error = errno; + goto end_profile; + } + + ret = fchdir(state->dir_fsp->fh->fd); if (ret == -1) { state->xattr_size = -1; state->vfs_aio_state.error = errno; @@ -3031,7 +3126,7 @@ static void vfswrap_getxattrat_do(void *private_data) state->xattr_size = getxattr(state->name, state->xattr_name, state->xattr_value, - state->xattr_bufsize); + talloc_array_length(state->xattr_value)); if (state->xattr_size == -1) { state->vfs_aio_state.error = errno; } @@ -3049,12 +3144,34 @@ static void vfswrap_getxattrat_done(struct tevent_req *subreq) struct vfswrap_getxattrat_state *state = tevent_req_data( req, struct vfswrap_getxattrat_state); int ret; + bool ok; + + /* + * Make sure we run as the user again + */ + ok = change_to_user(state->dir_fsp->conn, + state->dir_fsp->vuid); + if (!ok) { + smb_panic("Can't change to user"); + return; + } ret = pthreadpool_tevent_job_recv(subreq); TALLOC_FREE(subreq); SMBPROFILE_BYTES_ASYNC_END(state->profile_bytes); talloc_set_destructor(state, NULL); - if (tevent_req_error(req, ret)) { + if (ret != 0) { + if (ret != EAGAIN) { + tevent_req_error(req, ret); + return; + } + /* + * If we get EAGAIN from pthreadpool_tevent_job_recv() this + * means the lower level pthreadpool failed to create a new + * thread. Fallback to sync processing in that case to allow + * some progress for the client. + */ + vfswrap_getxattrat_do_sync(req); return; } diff --git a/source3/modules/vfs_full_audit.c b/source3/modules/vfs_full_audit.c index bae08102b0e..7dbb6e1e628 100644 --- a/source3/modules/vfs_full_audit.c +++ b/source3/modules/vfs_full_audit.c @@ -2523,14 +2523,13 @@ static void smb_full_audit_getxattrat_done(struct tevent_req *subreq); static struct tevent_req *smb_full_audit_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, const char *xattr_name, size_t alloc_hint) { - struct tevent_context *ev = smb_vfs_ev_glue_ev_ctx(evg); struct tevent_req *req = NULL; struct tevent_req *subreq = NULL; struct smb_full_audit_getxattrat_state *state = NULL; @@ -2555,7 +2554,7 @@ static struct tevent_req *smb_full_audit_getxattrat_send( }; subreq = SMB_VFS_NEXT_GETXATTRAT_SEND(state, - evg, + ev, handle, dir_fsp, smb_fname, diff --git a/source3/modules/vfs_not_implemented.c b/source3/modules/vfs_not_implemented.c index e20b7eb76ed..d642a133c18 100644 --- a/source3/modules/vfs_not_implemented.c +++ b/source3/modules/vfs_not_implemented.c @@ -877,14 +877,13 @@ struct vfs_not_implemented_getxattrat_state { struct tevent_req *vfs_not_implemented_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, const char *xattr_name, size_t alloc_hint) { - struct tevent_context *ev = smb_vfs_ev_glue_ev_ctx(evg); struct tevent_req *req = NULL; struct vfs_not_implemented_getxattrat_state *state = NULL; diff --git a/source3/modules/vfs_time_audit.c b/source3/modules/vfs_time_audit.c index aefea33d305..5dd6032b658 100644 --- a/source3/modules/vfs_time_audit.c +++ b/source3/modules/vfs_time_audit.c @@ -2456,14 +2456,13 @@ static void smb_time_audit_getxattrat_done(struct tevent_req *subreq); static struct tevent_req *smb_time_audit_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, const char *xattr_name, size_t alloc_hint) { - struct tevent_context *ev = smb_vfs_ev_glue_ev_ctx(evg); struct tevent_req *req = NULL; struct tevent_req *subreq = NULL; struct smb_time_audit_getxattrat_state *state = NULL; @@ -2480,7 +2479,7 @@ static struct tevent_req *smb_time_audit_getxattrat_send( }; subreq = SMB_VFS_NEXT_GETXATTRAT_SEND(state, - evg, + ev, handle, dir_fsp, smb_fname, diff --git a/source3/modules/vfs_xattr_tdb.c b/source3/modules/vfs_xattr_tdb.c index 32968ae083f..037c4045632 100644 --- a/source3/modules/vfs_xattr_tdb.c +++ b/source3/modules/vfs_xattr_tdb.c @@ -112,14 +112,13 @@ struct xattr_tdb_getxattrat_state { static struct tevent_req *xattr_tdb_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, const char *xattr_name, size_t alloc_hint) { - struct tevent_context *ev = smb_vfs_ev_glue_ev_ctx(evg); struct tevent_req *req = NULL; struct xattr_tdb_getxattrat_state *state = NULL; struct smb_filename *cwd = NULL; diff --git a/source3/smbd/smb2_query_directory.c b/source3/smbd/smb2_query_directory.c index 68c00241a51..654e21decc8 100644 --- a/source3/smbd/smb2_query_directory.c +++ b/source3/smbd/smb2_query_directory.c @@ -240,7 +240,6 @@ struct smbd_smb2_query_directory_state { bool async_dosmode; bool async_ask_sharemode; int last_entry_off; - struct pthreadpool_tevent *tp_chdir_safe; size_t max_async_dosmode_active; uint32_t async_dosmode_active; bool done; @@ -279,7 +278,6 @@ static struct tevent_req *smbd_smb2_query_directory_send(TALLOC_CTX *mem_ctx, } state->evg = conn->user_vfs_evg; state->ev = ev; - state->tp_chdir_safe = smb_vfs_ev_glue_tp_chdir_safe(state->evg); state->fsp = fsp; state->smb2req = smb2req; state->in_output_buffer_length = in_output_buffer_length; @@ -519,7 +517,7 @@ static struct tevent_req *smbd_smb2_query_directory_send(TALLOC_CTX *mem_ctx, if (state->async_dosmode) { size_t max_threads; - max_threads = pthreadpool_tevent_max_threads(state->tp_chdir_safe); + max_threads = pthreadpool_tevent_max_threads(conn->sconn->raw_thread_pool); state->max_async_dosmode_active = lp_smbd_max_async_dosmode( SNUM(conn)); @@ -664,7 +662,7 @@ static bool smb2_query_directory_next_entry(struct tevent_req *req) state->async_dosmode_active++; outstanding_aio = pthreadpool_tevent_queued_jobs( - state->tp_chdir_safe); + state->fsp->conn->sconn->raw_thread_pool); if (outstanding_aio > state->max_async_dosmode_active) { stop = true; diff --git a/source3/smbd/vfs.c b/source3/smbd/vfs.c index 351cd0a5567..7d46ec9273b 100644 --- a/source3/smbd/vfs.c +++ b/source3/smbd/vfs.c @@ -3539,7 +3539,7 @@ static void smb_vfs_call_getxattrat_done(struct tevent_req *subreq); struct tevent_req *smb_vfs_call_getxattrat_send( TALLOC_CTX *mem_ctx, - const struct smb_vfs_ev_glue *evg, + struct tevent_context *ev, struct vfs_handle_struct *handle, files_struct *dir_fsp, const struct smb_filename *smb_fname, @@ -3549,7 +3549,6 @@ struct tevent_req *smb_vfs_call_getxattrat_send( struct tevent_req *req = NULL; struct smb_vfs_call_getxattrat_state *state = NULL; struct tevent_req *subreq = NULL; - bool ok; req = tevent_req_create(mem_ctx, &state, struct smb_vfs_call_getxattrat_state); @@ -3560,24 +3559,18 @@ struct tevent_req *smb_vfs_call_getxattrat_send( VFS_FIND(getxattrat_send); state->recv_fn = handle->fns->getxattrat_recv_fn; - ok = smb_vfs_ev_glue_push_use(evg, req); - if (!ok) { - tevent_req_error(req, EIO); - return tevent_req_post(req, evg->return_ev); - } - subreq = handle->fns->getxattrat_send_fn(mem_ctx, - evg->next_glue, + ev, handle, dir_fsp, smb_fname, xattr_name, alloc_hint); - smb_vfs_ev_glue_pop_use(evg); - if (tevent_req_nomem(subreq, req)) { - return tevent_req_post(req, evg->return_ev); + return tevent_req_post(req, ev); } + tevent_req_defer_callback(req, ev); + tevent_req_set_callback(subreq, smb_vfs_call_getxattrat_done, req); return req; } -- cgit v1.2.1