/* Copyright (C) 2015 ABRT team 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "libabrt.h" #include "abrt_problems2_entry.h" #include #include typedef struct { char *p2e_dirname; AbrtP2EntryState p2e_state; } AbrtP2EntryPrivate; struct _AbrtP2Entry { GObject parent_instance; AbrtP2EntryPrivate *pv; }; G_DEFINE_TYPE_WITH_PRIVATE(AbrtP2Entry, abrt_p2_entry, G_TYPE_OBJECT) static void abrt_p2_entry_finalize(GObject *gobject) { AbrtP2EntryPrivate *pv = abrt_p2_entry_get_instance_private(ABRT_P2_ENTRY(gobject)); free(pv->p2e_dirname); } static void abrt_p2_entry_class_init(AbrtP2EntryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = abrt_p2_entry_finalize; } static void abrt_p2_entry_init(AbrtP2Entry *self) { self->pv = abrt_p2_entry_get_instance_private(self); } AbrtP2Entry *abrt_p2_entry_new(char *dirname) { return abrt_p2_entry_new_with_state(dirname, ABRT_P2_ENTRY_STATE_COMPLETE); } AbrtP2Entry *abrt_p2_entry_new_with_state(char *dirname, AbrtP2EntryState state) { AbrtP2Entry *entry = g_object_new(TYPE_ABRT_P2_ENTRY, NULL); entry->pv->p2e_dirname = dirname; entry->pv->p2e_state = state; return entry; } AbrtP2EntryState abrt_p2_entry_state(AbrtP2Entry *entry) { return entry->pv->p2e_state; } void abrt_p2_entry_set_state(AbrtP2Entry *entry, AbrtP2EntryState state) { entry->pv->p2e_state = state; } const char *abrt_p2_entry_problem_id(AbrtP2Entry *entry) { return entry->pv->p2e_dirname; } int abrt_p2_entry_accessible_by_uid(AbrtP2Entry *entry, uid_t uid, struct dump_dir **dd) { struct dump_dir *tmp = dd_opendir(entry->pv->p2e_dirname, DD_OPEN_FD_ONLY | DD_FAIL_QUIETLY_ENOENT | DD_FAIL_QUIETLY_EACCES); if (tmp == NULL) { VERB2 perror_msg("can't open problem directory '%s'", entry->pv->p2e_dirname); return -ENOTDIR; } const int ret = dd_accessible_by_uid(tmp, uid) ? 0 : -EACCES; if (ret == 0 && dd != NULL) *dd = tmp; else dd_close(tmp); return ret; } int abrt_p2_entry_delete(AbrtP2Entry *entry, uid_t caller_uid, GError **error) { struct dump_dir *dd = NULL; int ret = abrt_p2_entry_accessible_by_uid(entry, caller_uid, &dd); if (ret != 0) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED, "You are not authorized to delete the problem"); return ret; } if (entry->pv->p2e_state == ABRT_P2_ENTRY_STATE_DELETED) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Problem entry is already deleted"); dd_close(dd); return -EINVAL; } dd = dd_fdopendir(dd, DD_DONT_WAIT_FOR_LOCK); if (dd == NULL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Cannot lock the problem. Check system logs."); return -EWOULDBLOCK; } ret = dd_delete(dd); if (ret != 0) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Failed to remove problem data. Check system logs."); dd_close(dd); return ret; } abrt_p2_entry_set_state(entry, ABRT_P2_ENTRY_STATE_DELETED); return ret; } GVariant *abrt_p2_entry_problem_data(AbrtP2Entry *node, uid_t caller_uid, GError **error) { struct dump_dir *dd = abrt_p2_entry_open_dump_dir(node, caller_uid, DD_OPEN_READONLY, error); if (dd == NULL) return NULL; problem_data_t *pd = create_problem_data_from_dump_dir(dd); problem_data_add_text_noteditable(pd, CD_DUMPDIR, node->pv->p2e_dirname); GVariantBuilder response_builder; g_variant_builder_init(&response_builder, G_VARIANT_TYPE_ARRAY); GHashTableIter pd_iter; char *element_name; struct problem_item *element_info; g_hash_table_iter_init(&pd_iter, pd); while (g_hash_table_iter_next(&pd_iter, (void**)&element_name, (void**)&element_info)) { unsigned long size = 0; if (problem_item_get_size(element_info, &size) != 0) { log_notice("Can't get stat of : '%s'", element_info->content); continue; } g_variant_builder_add(&response_builder, "{s(its)}", element_name, element_info->flags, size, element_info->content); } problem_data_free(pd); dd_close(dd); return g_variant_new("(a{s(its)})", &response_builder); } struct dump_dir *abrt_p2_entry_open_dump_dir(AbrtP2Entry *entry, uid_t caller_uid, int dd_flags, GError **error) { struct dump_dir *dd = NULL; if (0 != abrt_p2_entry_accessible_by_uid(entry, caller_uid, &dd)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED, "You are not authorized to access the problem"); return NULL; } dd = dd_fdopendir(dd, dd_flags); if (dd == NULL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Failed reopen dump directory"); return NULL; } return dd; } /** * Read elements */ GVariant *abrt_p2_entry_read_elements(AbrtP2Entry *entry, gint32 flags, GVariant *elements, GUnixFDList *fd_list, uid_t caller_uid, long max_size, long max_unix_fds, GError **error) { if ((flags & ABRT_P2_ENTRY_READ_ALL_FD) && (flags & ABRT_P2_ENTRY_READ_ALL_NO_FD)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Invalid arguments 'ALL FD' ~ 'ALL NO FD'"); return NULL; } struct dump_dir *dd = abrt_p2_entry_open_dump_dir(entry, caller_uid, DD_OPEN_READONLY | DD_DONT_WAIT_FOR_LOCK, error); if (dd == NULL) return NULL; GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); size_t loaded_size = 0; gchar *name = NULL; GVariantIter iter; g_variant_iter_init(&iter, elements); /* No need to free 'name' unless breaking out of the loop */ while (g_variant_iter_loop(&iter, "s", &name)) { log_debug("Reading element: %s", name); /* Do ask me why -> see libreport xmalloc_read() */ size_t data_size = (INT_MAX - 4095); int elem_type = 0; char *data = NULL; int fd = -1; const int r = problem_data_load_dump_dir_element(dd, name, &data, &elem_type, &fd); if (r < 0) { if (r == -ENOENT) log_debug("Element does not exist: %s", name); else if (r == -EINVAL) error_msg("Attempt to read prohibited data: '%s'", name); else error_msg("Failed to open %s: %s", name, strerror(-r)); continue; } if ( ((flags & ABRT_P2_ENTRY_READ_ONLY_TEXT) && !(elem_type & CD_FLAG_TXT)) || ((flags & ABRT_P2_ENTRY_READ_ONLY_BIG_TEXT) && !(elem_type & CD_FLAG_BIGTXT)) || ((flags & ABRT_P2_ENTRY_READ_ONLY_BINARY) && !(elem_type & CD_FLAG_BIN)) ) { log_debug("Element is not of the requested type: %s", name); free(data); close(fd); continue; } if ((flags & ABRT_P2_ENTRY_READ_ALL_FD) || !(elem_type & CD_FLAG_TXT)) { log_debug("Rewinding file descriptor %d", fd); free(data); if (lseek(fd, 0, SEEK_SET)) { perror_msg("Failed to rewind file descriptor of %s", name); close(fd); continue; } } if ( (flags & ABRT_P2_ENTRY_READ_ALL_FD) || (!(flags & ABRT_P2_ENTRY_READ_ALL_NO_FD) && !(elem_type & CD_FLAG_TXT))) { if (g_unix_fd_list_get_length(fd_list) == max_unix_fds) { error_msg("Reached limit of UNIX FDs per message: %ld", max_unix_fds); close(fd); continue; } GError *error = NULL; const gint pos = g_unix_fd_list_append(fd_list, fd, &error); close(fd); if (error != NULL) { error_msg("Failed to add file descriptor of %s: %s", name, error->message); g_error_free(error); continue; } log_debug("Adding new Unix FD at position: %d", pos); g_variant_builder_add(&builder, "{sv}", name, g_variant_new("h", pos)); continue; } if (!(elem_type & CD_FLAG_TXT)) { data = xmalloc_read(fd, &data_size); log_debug("Re-loaded entire element: %zu Bytes", data_size); } else data_size = strlen(data); close(fd); if (data_size > DBUS_MAXIMUM_ARRAY_LENGTH) { error_msg("Element '%s' cannot be returned as array due to length limit: %ld", name, (long)DBUS_MAXIMUM_ARRAY_LENGTH); free(data); continue; } if (data_size > max_size || loaded_size > max_size - data_size) { error_msg("With element '%s', reached runtime data size limit: %ld", name, max_size); free(data); continue; } if (loaded_size > ULONG_MAX - data_size) { error_msg("With element '%s', reached static data size limit: %ld", name, max_size); free(data); continue; } loaded_size += data_size; if (elem_type & CD_FLAG_BIN) { log_debug("Adding element binary data"); g_variant_builder_add(&builder, "{sv}", name, g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, data, data_size, sizeof(char))); } else { log_debug("Adding element text data"); g_variant_builder_add(&builder, "{sv}", name, g_variant_new_string(data)); } free(data); } dd_close(dd); GVariant *retval_body[1]; retval_body[0] = g_variant_builder_end(&builder); return g_variant_new_tuple(retval_body, ARRAY_SIZE(retval_body)); } /** * Asynchronous version of Read elements */ typedef struct { gint32 flags; GVariant *elements; GUnixFDList *fd_list; uid_t caller_uid; long max_size; long max_unix_fds; } AbrtP2EntryReadElementsData; #define abrt_p2_entry_read_elements_data_new() \ xmalloc(sizeof(AbrtP2EntryReadElementsData)) static inline void abrt_p2_entry_read_elements_data_free(AbrtP2EntryReadElementsData *data) { free(data); } void abrt_p2_entry_read_elements_async_task(GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { AbrtP2Entry *entry = source_object; AbrtP2EntryReadElementsData *data = task_data; GError *error = NULL; GVariant *response = abrt_p2_entry_read_elements(entry, data->flags, data->elements, data->fd_list, data->caller_uid, data->max_size, data->max_unix_fds, &error); if (error == NULL) g_task_return_pointer(task, response, (GDestroyNotify)g_variant_unref); else g_task_return_error(task, error); } void abrt_p2_entry_read_elements_async(AbrtP2Entry *entry, gint32 flags, GVariant *elements, GUnixFDList *fd_list, uid_t caller_uid, long max_size, long max_unix_fds, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { AbrtP2EntryReadElementsData *data = abrt_p2_entry_read_elements_data_new(); data->flags = flags; data->elements = elements; data->fd_list = fd_list; data->caller_uid = caller_uid; data->max_size = max_size; data->max_unix_fds = max_unix_fds; GTask *task = g_task_new(entry, cancellable, callback, user_data); g_task_set_task_data(task, data, (GDestroyNotify)abrt_p2_entry_read_elements_data_free); g_task_run_in_thread(task, abrt_p2_entry_read_elements_async_task); g_object_unref(task); } GVariant *abrt_p2_entry_read_elements_finish(AbrtP2Entry *entry, GAsyncResult *result, GError **error) { g_return_val_if_fail(g_task_is_valid(result, entry), NULL); return g_task_propagate_pointer(G_TASK(result), error); } /** * Save elements */ GVariant *abrt_p2_entry_save_elements(AbrtP2Entry *entry, gint32 flags, GVariant *elements, GUnixFDList *fd_list, uid_t caller_uid, AbrtP2EntrySaveElementsLimits *limits, GError **error) { struct dump_dir *dd = abrt_p2_entry_open_dump_dir(entry, caller_uid, DD_DONT_WAIT_FOR_LOCK, error); if (dd == NULL) return NULL; abrt_p2_entry_save_elements_in_dump_dir(dd, flags, elements, fd_list, caller_uid, limits, error); dd_close(dd); return NULL; } /** * Save elements in a dump directory */ int abrt_p2_entry_save_elements_in_dump_dir(struct dump_dir *dd, gint32 flags, GVariant *elements, GUnixFDList *fd_list, uid_t caller_uid, AbrtP2EntrySaveElementsLimits *limits, GError **error) { int retval = 0; gchar *name = NULL; GVariant *value = NULL; GVariantIter iter; g_variant_iter_init(&iter, elements); off_t dd_size = dd_compute_size(dd, /*no flags*/0); if (dd_size < 0) { error_msg("Failed to get file system size of dump dir : %s", strerror(-(int)dd_size)); g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Dump directory file system size"); return dd_size; } int dd_items = dd_get_items_count(dd); if (dd_items < 0) { error_msg("Failed to get count of dump dir elements: %s", strerror(-dd_items)); g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Dump directory elements count"); return dd_items; } /* No need to free 'name' and 'container' unless breaking out of the loop */ while (g_variant_iter_loop(&iter, "{sv}", &name, &value)) { log_debug("Saving element: %s", name); struct stat item_stat; memset(&item_stat, 0, sizeof(item_stat)); const int r = dd_item_stat(dd, name, &item_stat); if (r == -EINVAL) { error_msg("Attempt to save prohibited data: '%s'", name); g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED, "Not allowed problem element name"); retval = -EACCES; goto exit_loop_on_error; } else if (r == -ENOENT) { if (limits->elements_count != 0 && dd_items >= limits->elements_count) { error_msg("Cannot create new element '%s': reached the limit for elements %u", name, limits->elements_count); if (flags & ABRT_P2_ENTRY_ELEMENTS_COUNT_LIMIT_FATAL) goto exit_loop_on_too_many_elements; continue; } ++dd_items; } else if (r < 0) { error_msg("Failed to get size of element '%s'", name); if (flags & ABRT_P2_ENTRY_IO_ERROR_FATAL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Failed to get size of underlying data"); retval = r; goto exit_loop_on_error; } continue; } const off_t base_size = dd_size - item_stat.st_size; if ( g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING)) { off_t data_size = 0; const char *data = NULL; if (g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING)) { log_debug("Saving binary element"); /* Using G_VARIANT_TYPE_BYTESTRING only to check the type. */ gsize n_elements = 0; const gsize element_size = sizeof(guchar); data = g_variant_get_fixed_array(value, &n_elements, element_size); data_size = n_elements * element_size; } else { log_debug("Saving text element"); gsize size = 0; data = g_variant_get_string(value, &size); if (size >= (1ULL << (8 * sizeof(off_t) - 1))) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Cannot read huge text data"); retval = -EINVAL; goto exit_loop_on_error; } data_size = (off_t)size; } if (allowed_new_user_problem_entry(caller_uid, name, data) == false) { error_msg("Not allowed for user %lu: %s = %s", (long unsigned)caller_uid, name, data); g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "You are not allowed to create element '%s' containing '%s'", name, data); retval = -EPERM; goto exit_loop_on_error; } /* Do not allow dump dir growing in case it already consumes * more than the limit */ if ( limits->data_size != 0 && data_size > item_stat.st_size && base_size + data_size > limits->data_size) { error_msg("Cannot save text element: " "problem data size limit %lld, " "data size %lld, " "item size %lld, " "base size %lld", (long long int)limits->data_size, (long long int)data_size, (long long int)item_stat.st_size, (long long int)base_size); if (flags & ABRT_P2_ENTRY_DATA_SIZE_LIMIT_FATAL) goto exit_loop_on_too_big_data; continue; } dd_save_binary(dd, name, data, data_size); dd_size = base_size + data_size; } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_HANDLE)) { if (fd_list == NULL) { error_msg("No UnixFDList to get handle of element '%s'", name); if (flags & ABRT_P2_ENTRY_IO_ERROR_FATAL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "No UnixFDList to get handle of element '%s'", name); retval = -EIO; goto exit_loop_on_error; } continue; } log_debug("Saving data from file descriptor"); if (problem_entry_is_post_create_condition(name)) { error_msg("post-create element as file descriptor: %s", name); g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Element '%s' must be of '%s' D-Bus type", name, g_variant_type_peek_string(G_VARIANT_TYPE_STRING)); retval = -EINVAL; goto exit_loop_on_error; } gint32 handle = g_variant_get_handle(value); int fd = g_unix_fd_list_get(fd_list, handle, error); if (*error != NULL) { error_msg("Failed to get file descriptor of %s: %s", name, (*error)->message); if (flags & ABRT_P2_ENTRY_IO_ERROR_FATAL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Failed to get passed file descriptor"); retval = -EIO; goto exit_loop_on_error; } continue; } /* Do not allow dump dir growing */ const off_t max_size = base_size > limits->data_size ? item_stat.st_size : limits->data_size - base_size; /* Make the file descriptor non-blocking. We will not wait for * data. An attacker could use it to stop the service from * function. */ if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) { perror_msg("Failed to set file descriptor of the '%s' item non-blocking:", name); close(fd); if (flags & ABRT_P2_ENTRY_IO_ERROR_FATAL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Failed to set file file descriptor of the '%s' item non-blocking", name); retval = -EIO; goto exit_loop_on_error; } continue; } const off_t r = dd_copy_fd(dd, name, fd, /*copy_flags*/0, max_size); close(fd); if (r < 0) { error_msg("Failed to save file descriptor"); if (flags & ABRT_P2_ENTRY_IO_ERROR_FATAL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Failed to save data of passed file descriptor"); retval = r; goto exit_loop_on_error; } continue; } if (r >= max_size) { error_msg("File descriptor was truncated due to size limit"); if (flags & ABRT_P2_ENTRY_DATA_SIZE_LIMIT_FATAL) goto exit_loop_on_too_big_data; /* the file has been created and its size is 'max_size' */ dd_size = base_size + max_size; } else dd_size = base_size + r ; } else { error_msg("Unsupported type: %s", g_variant_get_type_string(value)); if (flags & ABRT_P2_ENTRY_IO_ERROR_FATAL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "Not supported D-Bus type"); retval = -ENOTSUP; goto exit_loop_on_error; } } } return 0; exit_loop_on_too_big_data: g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_LIMITS_EXCEEDED, "Problem data is too big"); retval = -EFBIG; goto exit_loop_on_error; exit_loop_on_too_many_elements: g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_LIMITS_EXCEEDED, "Too many elements"); retval = -E2BIG; exit_loop_on_error: g_free(name); g_variant_unref(value); return retval; } /** * Asynchronous version of Save elements */ typedef struct { gint32 flags; GVariant *elements; GUnixFDList *fd_list; uid_t caller_uid; AbrtP2EntrySaveElementsLimits limits; } AbrtP2EntrySaveElementsData; #define abrt_p2_entry_save_elements_data_new() \ xmalloc(sizeof(AbrtP2EntrySaveElementsData)) static inline void abrt_p2_entry_save_elements_data_free(AbrtP2EntrySaveElementsData *data) { if (data->fd_list) g_object_unref(data->fd_list); free(data); } static void abrt_p2_entry_save_elements_async_task(GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { AbrtP2Entry *entry = source_object; AbrtP2EntrySaveElementsData *data = task_data; GError *error = NULL; GVariant *response = abrt_p2_entry_save_elements(entry, data->flags, data->elements, data->fd_list, data->caller_uid, &(data->limits), &error); if (error == NULL) g_task_return_pointer(task, response, (GDestroyNotify)g_variant_unref); else g_task_return_error(task, error); } void abrt_p2_entry_save_elements_async(AbrtP2Entry *entry, gint32 flags, GVariant *elements, GUnixFDList *fd_list, uid_t caller_uid, AbrtP2EntrySaveElementsLimits *limits, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { AbrtP2EntrySaveElementsData *data = abrt_p2_entry_save_elements_data_new(); data->flags = flags; data->elements = elements; data->fd_list = fd_list != NULL ? g_object_ref(fd_list) : NULL; data->caller_uid = caller_uid; data->limits = *limits; GTask *task = g_task_new(entry, cancellable, callback, user_data); g_task_set_task_data(task, data, (GDestroyNotify)abrt_p2_entry_save_elements_data_free); g_task_run_in_thread(task, abrt_p2_entry_save_elements_async_task); g_object_unref(task); return; } GVariant *abrt_p2_entry_save_elements_finish(AbrtP2Entry *entry, GAsyncResult *result, GError **error) { g_return_val_if_fail(g_task_is_valid(result, entry), NULL); return g_task_propagate_pointer(G_TASK(result), error); } /** * Delete elements */ GVariant *abrt_p2_entry_delete_elements(AbrtP2Entry *entry, uid_t caller_uid, GVariant *elements, GError **error) { struct dump_dir *dd = abrt_p2_entry_open_dump_dir(entry, caller_uid, DD_DONT_WAIT_FOR_LOCK, error); if (dd == NULL) return NULL; gchar *name = NULL; GVariantIter iter; g_variant_iter_init(&iter, elements); /* No need to free 'name' unless breaking out of the loop */ while (g_variant_iter_loop(&iter, "s", &name)) { log_debug("Deleting element: %s", name); const int r = dd_delete_item(dd, name); if (r == -EINVAL) error_msg("Attempt to remove prohibited data: '%s'", name); } dd_close(dd); return NULL; } /* * Properties */ uid_t abrt_p2_entry_get_owner(AbrtP2Entry *entry, GError **error) { struct dump_dir *dd = dd_opendir(entry->pv->p2e_dirname, DD_OPEN_FD_ONLY); if (dd == NULL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_IO_ERROR, "Failed open dump directory"); return -1; } const uid_t uid = dd_get_owner(dd); dd_close(dd); return uid; }