/* SSSD Files provider operations Copyright (C) 2016 Red Hat This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include "config.h" #include "providers/files/files_private.h" #include "db/sysdb.h" #include "util/inotify.h" #include "util/util.h" /* When changing this constant, make sure to also adjust the files integration * test for reallocation branch */ #define FILES_REALLOC_CHUNK 64 #define PWD_MAXSIZE 1024 #define GRP_MAXSIZE 2048 struct files_ctx { struct snotify_ctx *pwd_watch; struct snotify_ctx *grp_watch; struct files_ops_ctx *ops; }; static errno_t enum_files_users(TALLOC_CTX *mem_ctx, struct files_id_ctx *id_ctx, struct passwd ***_users) { errno_t ret, close_ret; struct passwd *pwd_iter = NULL; struct passwd *pwd = NULL; struct passwd **users = NULL; FILE *pwd_handle = NULL; size_t n_users = 0; pwd_handle = fopen(id_ctx->passwd_file, "r"); if (pwd_handle == NULL) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "Cannot open passwd file %s [%d]\n", id_ctx->passwd_file, ret); goto done; } users = talloc_zero_array(mem_ctx, struct passwd *, FILES_REALLOC_CHUNK); if (users == NULL) { ret = ENOMEM; goto done; } while ((pwd_iter = fgetpwent(pwd_handle)) != NULL) { /* FIXME - we might want to support paging of sorts to avoid allocating * all users atop a memory context or only return users that differ from * the local storage as a diff to minimize memory spikes */ DEBUG(SSSDBG_TRACE_LIBS, "User found (%s, %s, %"SPRIuid", %"SPRIgid", %s, %s, %s)\n", pwd_iter->pw_name, pwd_iter->pw_passwd, pwd_iter->pw_uid, pwd_iter->pw_gid, pwd_iter->pw_gecos, pwd_iter->pw_dir, pwd_iter->pw_shell); pwd = talloc_zero(users, struct passwd); if (pwd == NULL) { ret = ENOMEM; goto done; } pwd->pw_uid = pwd_iter->pw_uid; pwd->pw_gid = pwd_iter->pw_gid; pwd->pw_name = talloc_strdup(pwd, pwd_iter->pw_name); if (pwd->pw_name == NULL) { /* We only check pw_name here on purpose to allow broken * records to be optionally rejected when saving them * or fallback values to be used. */ ret = ENOMEM; goto done; } pwd->pw_dir = talloc_strdup(pwd, pwd_iter->pw_dir); pwd->pw_gecos = talloc_strdup(pwd, pwd_iter->pw_gecos); pwd->pw_shell = talloc_strdup(pwd, pwd_iter->pw_shell); pwd->pw_passwd = talloc_strdup(pwd, pwd_iter->pw_passwd); users[n_users] = pwd; n_users++; if (n_users % FILES_REALLOC_CHUNK == 0) { users = talloc_realloc(mem_ctx, users, struct passwd *, talloc_array_length(users) + FILES_REALLOC_CHUNK); if (users == NULL) { ret = ENOMEM; goto done; } } } ret = EOK; users[n_users] = NULL; *_users = users; done: if (ret != EOK) { talloc_free(users); } if (pwd_handle) { close_ret = fclose(pwd_handle); if (close_ret != 0) { close_ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "Cannot close passwd file %s [%d]\n", id_ctx->passwd_file, close_ret); } } return ret; } static errno_t enum_files_groups(TALLOC_CTX *mem_ctx, struct files_id_ctx *id_ctx, struct group ***_groups) { errno_t ret, close_ret; struct group *grp_iter = NULL; struct group *grp = NULL; struct group **groups = NULL; size_t n_groups = 0; FILE *grp_handle = NULL; grp_handle = fopen(id_ctx->group_file, "r"); if (grp_handle == NULL) { ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "Cannot open group file %s [%d]\n", id_ctx->group_file, ret); goto done; } groups = talloc_zero_array(mem_ctx, struct group *, FILES_REALLOC_CHUNK); if (groups == NULL) { ret = ENOMEM; goto done; } while ((grp_iter = fgetgrent(grp_handle)) != NULL) { DEBUG(SSSDBG_TRACE_LIBS, "Group found (%s, %"SPRIgid")\n", grp_iter->gr_name, grp_iter->gr_gid); grp = talloc_zero(groups, struct group); if (grp == NULL) { ret = ENOMEM; goto done; } grp->gr_gid = grp_iter->gr_gid; grp->gr_name = talloc_strdup(grp, grp_iter->gr_name); if (grp->gr_name == NULL) { /* We only check gr_name here on purpose to allow broken * records to be optionally rejected when saving them * or fallback values to be used. */ ret = ENOMEM; goto done; } grp->gr_passwd = talloc_strdup(grp, grp_iter->gr_passwd); if (grp_iter->gr_mem != NULL) { size_t nmem; for (nmem = 0; grp_iter->gr_mem[nmem] != NULL; nmem++); grp->gr_mem = talloc_zero_array(grp, char *, nmem + 1); if (grp->gr_mem == NULL) { ret = ENOMEM; goto done; } for (nmem = 0; grp_iter->gr_mem[nmem] != NULL; nmem++) { grp->gr_mem[nmem] = talloc_strdup(grp, grp_iter->gr_mem[nmem]); if (grp->gr_mem[nmem] == NULL) { ret = ENOMEM; goto done; } } } groups[n_groups] = grp; n_groups++; if (n_groups % FILES_REALLOC_CHUNK == 0) { groups = talloc_realloc(mem_ctx, groups, struct group *, talloc_array_length(groups) + FILES_REALLOC_CHUNK); if (groups == NULL) { ret = ENOMEM; goto done; } } } ret = EOK; groups[n_groups] = NULL; *_groups = groups; done: if (ret != EOK) { talloc_free(groups); } if (grp_handle) { close_ret = fclose(grp_handle); if (close_ret != 0) { close_ret = errno; DEBUG(SSSDBG_CRIT_FAILURE, "Cannot close group file %s [%d]\n", id_ctx->group_file, close_ret); } } return ret; } static errno_t delete_all_users(struct sss_domain_info *dom) { TALLOC_CTX *tmp_ctx; struct ldb_dn *base_dn; errno_t ret; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory!\n"); return ENOMEM; } base_dn = sysdb_user_base_dn(tmp_ctx, dom); if (base_dn == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); ret = ENOMEM; goto done; } ret = sysdb_delete_recursive(dom->sysdb, base_dn, true); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Unable to delete users subtree [%d]: %s\n", ret, sss_strerror(ret)); goto done; } ret = EOK; done: talloc_free(tmp_ctx); return ret; } static errno_t save_file_user(struct files_id_ctx *id_ctx, struct passwd *pw) { errno_t ret; char *fqname; TALLOC_CTX *tmp_ctx = NULL; const char *shell; const char *gecos; struct sysdb_attrs *attrs = NULL; if (strcmp(pw->pw_name, "root") == 0 || pw->pw_uid == 0 || pw->pw_gid == 0) { DEBUG(SSSDBG_TRACE_FUNC, "Skipping %s\n", pw->pw_name); return EOK; } tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } fqname = sss_create_internal_fqname(tmp_ctx, pw->pw_name, id_ctx->domain->name); if (fqname == NULL) { ret = ENOMEM; goto done; } attrs = sysdb_new_attrs(tmp_ctx); if (attrs == NULL) { ret = ENOMEM; goto done; } if (pw->pw_shell && pw->pw_shell[0] != '\0') { shell = pw->pw_shell; } else { shell = NULL; } if (pw->pw_gecos && pw->pw_gecos[0] != '\0') { gecos = pw->pw_gecos; } else { gecos = NULL; } /* FIXME - optimize later */ ret = sysdb_store_user(id_ctx->domain, fqname, pw->pw_passwd, pw->pw_uid, pw->pw_gid, gecos, pw->pw_dir, shell, NULL, attrs, NULL, 0, 0); if (ret != EOK) { goto done; } ret = EOK; done: talloc_free(tmp_ctx); return ret; } static errno_t refresh_override_attrs(struct files_id_ctx *id_ctx, enum sysdb_member_type type) { const char *override_attrs[] = { SYSDB_OVERRIDE_OBJECT_DN, NULL}; struct ldb_dn *base_dn; size_t count; struct ldb_message **msgs; struct ldb_message *msg = NULL; struct ldb_context *ldb_ctx; size_t c; TALLOC_CTX *tmp_ctx; int ret; const char *filter; ldb_ctx = sysdb_ctx_get_ldb(id_ctx->domain->sysdb); if (ldb_ctx == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Missing ldb_context.\n"); return EINVAL; } tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } filter = talloc_asprintf(tmp_ctx, "%s=%s", SYSDB_OBJECTCLASS, type == SYSDB_MEMBER_USER ? SYSDB_OVERRIDE_USER_CLASS : SYSDB_OVERRIDE_GROUP_CLASS ); if (filter == NULL) { DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n"); ret = ENOMEM; goto done; } base_dn = ldb_dn_new(tmp_ctx, ldb_ctx, SYSDB_TMPL_VIEW_BASE); if (base_dn == NULL) { DEBUG(SSSDBG_OP_FAILURE, "ldb_dn_new failed.\n"); ret = ENOMEM; goto done; } ret = sysdb_search_entry(tmp_ctx, id_ctx->domain->sysdb, base_dn, LDB_SCOPE_SUBTREE, filter, override_attrs, &count, &msgs); if (ret != EOK) { if (ret == ENOENT) { DEBUG(SSSDBG_OP_FAILURE, "No overrides, nothing to do.\n"); ret = EOK; } else { DEBUG(SSSDBG_OP_FAILURE, "sysdb_search_entry failed.\n"); } goto done; } for (c = 0; c < count; c++) { talloc_free(msg); msg = ldb_msg_new(tmp_ctx); if (msg == NULL) { ret = ENOMEM; goto done; } msg->dn = ldb_msg_find_attr_as_dn(ldb_ctx, tmp_ctx, msgs[c], SYSDB_OVERRIDE_OBJECT_DN); if (msg->dn == NULL) { DEBUG(SSSDBG_OP_FAILURE, "Failed to get object DN, skipping.\n"); continue; } ret = ldb_msg_add_empty(msg, SYSDB_OVERRIDE_DN, LDB_FLAG_MOD_ADD, NULL); if (ret != LDB_SUCCESS) { DEBUG(SSSDBG_OP_FAILURE, "ldb_msg_add_empty failed.\n"); continue; } ret = ldb_msg_add_string(msg, SYSDB_OVERRIDE_DN, ldb_dn_get_linearized(msgs[c]->dn)); if (ret != LDB_SUCCESS) { DEBUG(SSSDBG_OP_FAILURE, "ldb_msg_add_string failed.\n"); continue; } ret = ldb_modify(ldb_ctx, msg); if (ret != LDB_SUCCESS) { DEBUG(SSSDBG_CRIT_FAILURE, "Failed to store override DN: %s(%d)[%s], skipping.\n", ldb_strerror(ret), ret, ldb_errstring(ldb_ctx)); continue; } } ret = EOK; done: talloc_free(tmp_ctx); return ret; } static errno_t sf_enum_groups(struct files_id_ctx *id_ctx); errno_t sf_enum_users(struct files_id_ctx *id_ctx) { errno_t ret; errno_t tret; TALLOC_CTX *tmp_ctx = NULL; struct passwd **users = NULL; bool in_transaction = false; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } ret = enum_files_users(tmp_ctx, id_ctx, &users); if (ret != EOK) { goto done; } ret = sysdb_transaction_start(id_ctx->domain->sysdb); if (ret != EOK) { goto done; } in_transaction = true; /* remove previous cache contents */ /* FIXME - this is terribly inefficient */ ret = delete_all_users(id_ctx->domain); if (ret != EOK) { goto done; } for (size_t i = 0; users[i]; i++) { ret = save_file_user(id_ctx, users[i]); if (ret != EOK) { DEBUG(SSSDBG_MINOR_FAILURE, "Cannot save user %s: [%d]: %s\n", users[i]->pw_name, ret, sss_strerror(ret)); continue; } } ret = refresh_override_attrs(id_ctx, SYSDB_MEMBER_USER); if (ret != EOK) { DEBUG(SSSDBG_MINOR_FAILURE, "Failed to refresh override attributes, " "override values might not be available.\n"); } ret = sysdb_transaction_commit(id_ctx->domain->sysdb); if (ret != EOK) { goto done; } in_transaction = false; /* Covers the case when someone edits /etc/group, adds a group member and * only then edits passwd and adds the user. The reverse is not needed, * because member/memberof links are established when groups are saved. */ ret = sf_enum_groups(id_ctx); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Cannot refresh groups\n"); goto done; } ret = EOK; done: if (in_transaction) { tret = sysdb_transaction_cancel(id_ctx->domain->sysdb); if (tret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot cancel transaction: %d\n", ret); } } talloc_free(tmp_ctx); return ret; } static const char **get_cached_user_names(TALLOC_CTX *mem_ctx, struct sss_domain_info *dom) { errno_t ret; struct ldb_result *res = NULL; const char **user_names = NULL; unsigned c = 0; ret = sysdb_enumpwent(mem_ctx, dom, &res); if (ret != EOK) { goto done; } user_names = talloc_zero_array(mem_ctx, const char *, res->count + 1); if (user_names == NULL) { goto done; } for (unsigned i = 0; i < res->count; i++) { user_names[c] = ldb_msg_find_attr_as_string(res->msgs[i], SYSDB_NAME, NULL); if (user_names[c] == NULL) { continue; } c++; } done: /* Don't free res and keep it around to avoid duplicating the names */ return user_names; } static errno_t delete_all_groups(struct sss_domain_info *dom) { TALLOC_CTX *tmp_ctx; struct ldb_dn *base_dn; errno_t ret; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory!\n"); return ENOMEM; } base_dn = sysdb_group_base_dn(tmp_ctx, dom); if (base_dn == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); ret = ENOMEM; goto done; } ret = sysdb_delete_recursive(dom->sysdb, base_dn, true); if (ret != EOK) { DEBUG(SSSDBG_OP_FAILURE, "Unable to delete groups subtree [%d]: %s\n", ret, sss_strerror(ret)); goto done; } ret = EOK; done: talloc_free(tmp_ctx); return ret; } static errno_t save_file_group(struct files_id_ctx *id_ctx, struct group *grp, const char **cached_users) { errno_t ret; char *fqname; struct sysdb_attrs *attrs = NULL; TALLOC_CTX *tmp_ctx = NULL; char **fq_gr_files_mem; const char **fq_gr_mem; unsigned mi = 0; if (strcmp(grp->gr_name, "root") == 0 || grp->gr_gid == 0) { DEBUG(SSSDBG_TRACE_FUNC, "Skipping %s\n", grp->gr_name); return EOK; } tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } fqname = sss_create_internal_fqname(tmp_ctx, grp->gr_name, id_ctx->domain->name); if (fqname == NULL) { ret = ENOMEM; goto done; } attrs = sysdb_new_attrs(tmp_ctx); if (attrs == NULL) { ret = ENOMEM; goto done; } if (grp->gr_mem && grp->gr_mem[0]) { fq_gr_files_mem = sss_create_internal_fqname_list( tmp_ctx, (const char *const*) grp->gr_mem, id_ctx->domain->name); if (fq_gr_files_mem == NULL) { ret = ENOMEM; goto done; } fq_gr_mem = talloc_zero_array(tmp_ctx, const char *, talloc_array_length(fq_gr_files_mem)); if (fq_gr_mem == NULL) { ret = ENOMEM; goto done; } for (unsigned i=0; fq_gr_files_mem[i] != NULL; i++) { if (string_in_list(fq_gr_files_mem[i], discard_const(cached_users), true)) { fq_gr_mem[mi] = fq_gr_files_mem[i]; mi++; DEBUG(SSSDBG_TRACE_LIBS, "User %s is cached, will become a member of %s\n", fq_gr_files_mem[i], grp->gr_name); } else { ret = sysdb_attrs_add_string(attrs, SYSDB_GHOST, fq_gr_files_mem[i]); if (ret != EOK) { DEBUG(SSSDBG_MINOR_FAILURE, "Cannot add ghost %s for group %s\n", fq_gr_files_mem[i], fqname); continue; } DEBUG(SSSDBG_TRACE_LIBS, "User %s is not cached, will become a ghost of %s\n", fq_gr_files_mem[i], grp->gr_name); } } if (fq_gr_mem != NULL && fq_gr_mem[0] != NULL) { ret = sysdb_attrs_users_from_str_list( attrs, SYSDB_MEMBER, id_ctx->domain->name, (const char *const *) fq_gr_mem); if (ret) { DEBUG(SSSDBG_OP_FAILURE, "Could not add group members\n"); goto done; } } } ret = sysdb_store_group(id_ctx->domain, fqname, grp->gr_gid, attrs, 0, 0); if (ret) { DEBUG(SSSDBG_OP_FAILURE, "Could not add group to cache\n"); goto done; } ret = EOK; done: talloc_free(tmp_ctx); return ret; } static errno_t sf_enum_groups(struct files_id_ctx *id_ctx) { errno_t ret; errno_t tret; TALLOC_CTX *tmp_ctx = NULL; struct group **groups = NULL; bool in_transaction = false; const char **cached_users = NULL; tmp_ctx = talloc_new(NULL); if (tmp_ctx == NULL) { return ENOMEM; } ret = enum_files_groups(tmp_ctx, id_ctx, &groups); if (ret != EOK) { goto done; } cached_users = get_cached_user_names(tmp_ctx, id_ctx->domain); if (cached_users == NULL) { goto done; } ret = sysdb_transaction_start(id_ctx->domain->sysdb); if (ret != EOK) { goto done; } in_transaction = true; /* remove previous cache contents */ ret = delete_all_groups(id_ctx->domain); if (ret != EOK) { goto done; } for (size_t i = 0; groups[i]; i++) { ret = save_file_group(id_ctx, groups[i], cached_users); if (ret != EOK) { DEBUG(SSSDBG_MINOR_FAILURE, "Cannot save group %s\n", groups[i]->gr_name); continue; } } ret = refresh_override_attrs(id_ctx, SYSDB_MEMBER_GROUP); if (ret != EOK) { DEBUG(SSSDBG_MINOR_FAILURE, "Failed to refresh override attributes, " "override values might not be available.\n"); } ret = sysdb_transaction_commit(id_ctx->domain->sysdb); if (ret != EOK) { goto done; } in_transaction = false; ret = EOK; done: if (in_transaction) { tret = sysdb_transaction_cancel(id_ctx->domain->sysdb); if (tret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot cancel transaction: %d\n", ret); } } talloc_free(tmp_ctx); return ret; } static void sf_cb_done(struct files_id_ctx *id_ctx) { /* Only activate a domain when both callbacks are done */ if (id_ctx->updating_passwd == false && id_ctx->updating_groups == false) { dp_sbus_domain_active(id_ctx->be->provider, id_ctx->domain); } } static int sf_passwd_cb(const char *filename, uint32_t flags, void *pvt) { struct files_id_ctx *id_ctx; errno_t ret; id_ctx = talloc_get_type(pvt, struct files_id_ctx); if (id_ctx == NULL) { return EINVAL; } DEBUG(SSSDBG_TRACE_FUNC, "passwd notification\n"); if (strcmp(filename, id_ctx->passwd_file) != 0) { DEBUG(SSSDBG_CRIT_FAILURE, "Wrong file, expected %s, got %s\n", id_ctx->passwd_file, filename); return EINVAL; } id_ctx->updating_passwd = true; dp_sbus_domain_inconsistent(id_ctx->be->provider, id_ctx->domain); dp_sbus_reset_users_ncache(id_ctx->be->provider, id_ctx->domain); dp_sbus_reset_users_memcache(id_ctx->be->provider); dp_sbus_reset_initgr_memcache(id_ctx->be->provider); ret = sf_enum_users(id_ctx); id_ctx->updating_passwd = false; sf_cb_done(id_ctx); files_account_info_finished(id_ctx, BE_REQ_USER, ret); return ret; } static int sf_group_cb(const char *filename, uint32_t flags, void *pvt) { struct files_id_ctx *id_ctx; errno_t ret; id_ctx = talloc_get_type(pvt, struct files_id_ctx); if (id_ctx == NULL) { return EINVAL; } DEBUG(SSSDBG_TRACE_FUNC, "group notification\n"); if (strcmp(filename, id_ctx->group_file) != 0) { DEBUG(SSSDBG_CRIT_FAILURE, "Wrong file, expected %s, got %s\n", id_ctx->group_file, filename); return EINVAL; } id_ctx->updating_groups = true; dp_sbus_domain_inconsistent(id_ctx->be->provider, id_ctx->domain); dp_sbus_reset_groups_ncache(id_ctx->be->provider, id_ctx->domain); dp_sbus_reset_groups_memcache(id_ctx->be->provider); dp_sbus_reset_initgr_memcache(id_ctx->be->provider); ret = sf_enum_groups(id_ctx); id_ctx->updating_groups = false; sf_cb_done(id_ctx); files_account_info_finished(id_ctx, BE_REQ_GROUP, ret); return ret; } static void startup_enum_files(struct tevent_context *ev, struct tevent_immediate *imm, void *pvt) { struct files_id_ctx *id_ctx = talloc_get_type(pvt, struct files_id_ctx); errno_t ret; talloc_zfree(imm); ret = sf_enum_users(id_ctx); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Enumerating users failed, data might be inconsistent!\n"); } ret = sf_enum_groups(id_ctx); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Enumerating groups failed, data might be inconsistent!\n"); } } static struct snotify_ctx *sf_setup_watch(TALLOC_CTX *mem_ctx, struct tevent_context *ev, const char *filename, snotify_cb_fn fn, struct files_id_ctx *id_ctx) { return snotify_create(mem_ctx, ev, SNOTIFY_WATCH_DIR, filename, NULL, IN_DELETE_SELF | IN_CLOSE_WRITE | IN_MOVE_SELF | \ IN_CREATE | IN_MOVED_TO, fn, id_ctx); } struct files_ctx *sf_init(TALLOC_CTX *mem_ctx, struct tevent_context *ev, const char *passwd_file, const char *group_file, struct files_id_ctx *id_ctx) { struct files_ctx *fctx; struct tevent_immediate *imm; fctx = talloc(mem_ctx, struct files_ctx); if (fctx == NULL) { return NULL; } fctx->pwd_watch = sf_setup_watch(fctx, ev, passwd_file, sf_passwd_cb, id_ctx); fctx->grp_watch = sf_setup_watch(fctx, ev, group_file, sf_group_cb, id_ctx); if (fctx->pwd_watch == NULL || fctx->grp_watch == NULL) { talloc_free(fctx); return NULL; } /* Enumerate users and groups on startup to process any changes when * sssd was down. We schedule a request here to minimize the time * we spend in the init function */ imm = tevent_create_immediate(id_ctx); if (imm == NULL) { DEBUG(SSSDBG_OP_FAILURE, "tevent_create_immediate failed.\n"); talloc_free(fctx); return NULL; } tevent_schedule_immediate(imm, ev, startup_enum_files, id_ctx); return fctx; }