diff --git a/activity.c b/activity.c index a2866fa8f..b9890709b 100644 --- a/activity.c +++ b/activity.c @@ -54,13 +54,13 @@ static int setup_user(struct su_context *ctx, char* user) { void app_send_result(struct su_context *ctx, policy_t policy) { char fromUid[16]; - sprintf(fromUid, "%d", ctx->from.uid); + sprintf(fromUid, "%d", ctx->info->uid); char toUid[16]; sprintf(toUid, "%d", ctx->to.uid); char pid[16]; - sprintf(pid, "%d", ctx->from.pid); + sprintf(pid, "%d", ctx->info->pid); char user[16]; int notify = setup_user(ctx, user); diff --git a/db.c b/db.c index 93c2dcf1a..8cf60b6d3 100644 --- a/db.c +++ b/db.c @@ -9,67 +9,56 @@ #include #include #include +#include #include "magisk.h" #include "su.h" -struct callback_data_t { - struct su_context *ctx; - policy_t policy; -}; - -static int database_callback(void *v, int argc, char **argv, char **azColName){ - struct callback_data_t *data = (struct callback_data_t *)v; - policy_t policy = INTERACTIVE; - int i; +static int database_callback(void *v, int argc, char **argv, char **azColName) { + struct su_context *ctx = (struct su_context *) v; + policy_t policy = QUERY; time_t until = 0; - for(i = 0; i < argc; i++) { + for(int i = 0; i < argc; i++) { if (strcmp(azColName[i], "policy") == 0) { if (argv[i] != NULL) { policy = atoi(argv[i]); } - } - else if (strcmp(azColName[i], "until") == 0) { + } else if (strcmp(azColName[i], "until") == 0) { if (argv[i] != NULL) { until = atol(argv[i]); } } } - if (policy == DENY) { - data->policy = DENY; - return -1; - } else if (policy == ALLOW && (until == 0 || until > time(NULL))) { - data->policy = ALLOW; - // even though we allow, continue, so we can see if there's another policy - // that denies... - } + if (policy == DENY) + ctx->info->policy = DENY; + else if (policy == ALLOW && (until == 0 || until > time(NULL))) + ctx->info->policy = ALLOW; return 0; } -policy_t database_check(struct su_context *ctx) { +void database_check(struct su_context *ctx) { sqlite3 *db = NULL; + + // Check if file is readable + if (access(ctx->user.database_path, R_OK) == -1) + return; char query[512]; - snprintf(query, sizeof(query), "select policy, until from policies where uid=%d", ctx->from.uid); + snprintf(query, sizeof(query), "SELECT policy, until FROM policies WHERE uid=%d", ctx->info->uid % 100000); int ret = sqlite3_open_v2(ctx->user.database_path, &db, SQLITE_OPEN_READONLY, NULL); if (ret) { - LOGE("sqlite3 open failure: %d", ret); + LOGD("sqlite3 open failure: %s\n", sqlite3_errstr(ret)); sqlite3_close(db); - return INTERACTIVE; + return; } char *err = NULL; - struct callback_data_t data; - data.ctx = ctx; - data.policy = INTERACTIVE; - ret = sqlite3_exec(db, query, database_callback, &data, &err); + ret = sqlite3_exec(db, query, database_callback, ctx, &err); sqlite3_close(db); if (err != NULL) { - LOGE("sqlite3_exec: %s", err); - return DENY; + LOGE("sqlite3_exec: %s\n", err); + ctx->info->policy = DENY; } - - return data.policy; } diff --git a/su.c b/su.c index af33ef523..02021eed8 100644 --- a/su.c +++ b/su.c @@ -27,7 +27,7 @@ #include "resetprop.h" #include "su.h" -static struct su_context *su_ctx; +struct su_context *su_ctx; static void usage(int status) { FILE *stream = (status == EXIT_SUCCESS) ? stdout : stderr; @@ -46,7 +46,7 @@ static void usage(int status) { " -v, --version display version number and exit\n" " -V display version code and exit,\n" " this is used almost exclusively by Superuser.apk\n"); - exit(status); + exit2(status); } static char *concat_commands(int argc, char *argv[]) { @@ -61,16 +61,6 @@ static char *concat_commands(int argc, char *argv[]) { return strdup(command); } -static int get_multiuser_mode() { - char *prop = getprop(MULTIUSER_MODE_PROP); - if (prop) { - int ret = atoi(prop); - free(prop); - return ret; - } - return MULTIUSER_MODE_OWNER_ONLY; -} - static void populate_environment(const struct su_context *ctx) { struct passwd *pw; @@ -96,8 +86,7 @@ static __attribute__ ((noreturn)) void allow() { umask(su_ctx->umask); - // no need to log if called by root - if (su_ctx->from.uid != UID_ROOT) + if (su_ctx->notify) app_send_result(su_ctx, ALLOW); char *binary = su_ctx->to.shell; @@ -129,11 +118,10 @@ static __attribute__ ((noreturn)) void allow() { } static __attribute__ ((noreturn)) void deny() { - // no need to log if called by root - if (su_ctx->from.uid != UID_ROOT) + if (su_ctx->notify) app_send_result(su_ctx, DENY); - LOGW("su: request rejected (%u->%u)", su_ctx->from.uid, su_ctx->to.uid); + LOGW("su: request rejected (%u->%u)", su_ctx->info->uid, su_ctx->to.uid); fprintf(stderr, "%s\n", strerror(EACCES)); exit(EXIT_FAILURE); } @@ -147,7 +135,17 @@ static void socket_cleanup() { static void cleanup_signal(int sig) { socket_cleanup(); - exit(EXIT_FAILURE); + exit2(EXIT_FAILURE); +} + +__attribute__ ((noreturn)) void exit2(int status) { + // Handle the pipe, or the daemon will get stuck + if (su_ctx->info->policy == QUERY) { + xwrite(pipefd[1], &su_ctx->info->policy, sizeof(su_ctx->info->policy)); + close(pipefd[0]); + close(pipefd[1]); + } + exit(status); } int su_daemon_main(int argc, char **argv) { @@ -199,35 +197,8 @@ int su_daemon_main(int argc, char **argv) { } } - // Default values - struct su_context ctx = { - .from = { - .pid = su_credentials.pid, - .uid = su_credentials.uid, - }, - .to = { - .uid = UID_ROOT, - .login = 0, - .keepenv = 0, - .shell = DEFAULT_SHELL, - .command = NULL, - .argv = argv, - .argc = argc, - }, - .user = { - .android_user_id = 0, - .multiuser_mode = get_multiuser_mode(), - .database_path = APP_DATA_PATH REQUESTOR_DATABASE_PATH, - .base_path = APP_DATA_PATH REQUESTOR - }, - .umask = 022, - }; - su_ctx = &ctx; - - struct stat st; int c, socket_serv_fd, fd; char result[64]; - policy_t dballow; struct option long_opts[] = { { "command", required_argument, NULL, 'c' }, { "help", no_argument, NULL, 'h' }, @@ -242,30 +213,31 @@ int su_daemon_main(int argc, char **argv) { while ((c = getopt_long(argc, argv, "c:hlmps:Vvuz:", long_opts, NULL)) != -1) { switch (c) { case 'c': - ctx.to.command = concat_commands(argc, argv); + su_ctx->to.command = concat_commands(argc, argv); optind = argc; + su_ctx->notify = 1; break; case 'h': usage(EXIT_SUCCESS); break; case 'l': - ctx.to.login = 1; + su_ctx->to.login = 1; break; case 'm': case 'p': - ctx.to.keepenv = 1; + su_ctx->to.keepenv = 1; break; case 's': - ctx.to.shell = optarg; + su_ctx->to.shell = optarg; break; case 'V': printf("%d\n", MAGISK_VER_CODE); - exit(EXIT_SUCCESS); + exit2(EXIT_SUCCESS); case 'v': printf("%s\n", MAGISKSU_VER_STR); - exit(EXIT_SUCCESS); + exit2(EXIT_SUCCESS); case 'u': - switch (ctx.user.multiuser_mode) { + switch (su_ctx->user.multiuser_mode) { case MULTIUSER_MODE_USER: printf("Owner only: Only owner has root access\n"); break; @@ -276,7 +248,7 @@ int su_daemon_main(int argc, char **argv) { printf("User independent: Each user has its own separate root rules\n"); break; } - exit(EXIT_SUCCESS); + exit2(EXIT_SUCCESS); case 'z': // Do nothing, placed here for legacy support :) break; @@ -286,8 +258,12 @@ int su_daemon_main(int argc, char **argv) { usage(2); } } + + su_ctx->to.argc = argc; + su_ctx->to.argv = argv; + if (optind < argc && !strcmp(argv[optind], "-")) { - ctx.to.login = 1; + su_ctx->to.login = 1; optind++; } /* username or uid */ @@ -299,14 +275,14 @@ int su_daemon_main(int argc, char **argv) { /* It seems we shouldn't do this at all */ errno = 0; - ctx.to.uid = strtoul(argv[optind], &endptr, 10); + su_ctx->to.uid = strtoul(argv[optind], &endptr, 10); if (errno || *endptr) { LOGE("Unknown id: %s\n", argv[optind]); fprintf(stderr, "Unknown id: %s\n", argv[optind]); exit(EXIT_FAILURE); } } else { - ctx.to.uid = pw->pw_uid; + su_ctx->to.uid = pw->pw_uid; } optind++; } @@ -314,54 +290,28 @@ int su_daemon_main(int argc, char **argv) { optind++; } - // The su_context setup is done, now every error leads to deny + // Setup done, now every error leads to deny err_handler = deny; - // It's in multiuser mode - if (ctx.from.uid > 99999) { - ctx.user.android_user_id = ctx.from.uid / 100000; - ctx.from.uid %= 100000; - switch (ctx.user.multiuser_mode) { - case MULTIUSER_MODE_OWNER_ONLY: - deny(); - case MULTIUSER_MODE_USER: - snprintf(ctx.user.database_path, PATH_MAX, "%s/%d/%s", - USER_DATA_PATH, ctx.user.android_user_id, REQUESTOR_DATABASE_PATH); - snprintf(ctx.user.base_path, PATH_MAX, "%s/%d/%s", - USER_DATA_PATH, ctx.user.android_user_id, REQUESTOR); - break; - } - } - - // verify if Magisk Manager is installed - xstat(ctx.user.base_path, &st); - - // always allow if this is Magisk Manager - if (ctx.from.uid == (st.st_uid % 100000)) { - allow(); - } - - // odd perms on superuser data dir - if (st.st_gid != st.st_uid) { - LOGE("Bad uid/gid %d/%d for Superuser Requestor application", - (int)st.st_uid, (int)st.st_gid); - deny(); - } - // Check property of root configuration char *root_prop = getprop(ROOT_ACCESS_PROP); if (root_prop) { int prop_status = atoi(root_prop); switch (prop_status) { case ROOT_ACCESS_DISABLED: + LOGE("Root access is disabled!\n"); exit(EXIT_FAILURE); case ROOT_ACCESS_APPS_ONLY: - if (ctx.from.uid == UID_SHELL) + if (su_ctx->info->uid == UID_SHELL) { + LOGE("Root access is disabled for ADB!\n"); exit(EXIT_FAILURE); + } break; case ROOT_ACCESS_ADB_ONLY: - if (ctx.from.uid != UID_SHELL) + if (su_ctx->info->uid != UID_SHELL) { + LOGE("Root access limited to ADB only!\n"); exit(EXIT_FAILURE); + } break; case ROOT_ACCESS_APPS_AND_ADB: default: @@ -373,62 +323,38 @@ int su_daemon_main(int argc, char **argv) { setprop(ROOT_ACCESS_PROP, xstr(ROOT_ACCESS_APPS_AND_ADB)); } - // Allow root to start root - if (ctx.from.uid == UID_ROOT) { + // New request or no db exist, notify user for response + if (su_ctx->info->policy == QUERY) { + socket_serv_fd = socket_create_temp(su_ctx->sock_path, sizeof(su_ctx->sock_path)); + setup_sighandlers(cleanup_signal); + + // Start activity + app_send_request(su_ctx); + + atexit(socket_cleanup); + + fd = socket_accept(socket_serv_fd); + socket_send_request(fd, su_ctx); + socket_receive_result(fd, result, sizeof(result)); + + close(fd); + close(socket_serv_fd); + socket_cleanup(); + + if (strcmp(result, "socket:ALLOW") == 0) + su_ctx->info->policy = ALLOW; + else + su_ctx->info->policy = DENY; + + // Report the policy to main daemon + xwrite(pipefd[1], &su_ctx->info->policy, sizeof(su_ctx->info->policy)); + close(pipefd[0]); + close(pipefd[1]); + } + + if (su_ctx->info->policy == ALLOW) allow(); - } - - mkdir(REQUESTOR_CACHE_PATH, 0770); - if (chown(REQUESTOR_CACHE_PATH, st.st_uid, st.st_gid)) { - PLOGE("chown (%s, %u, %u)", REQUESTOR_CACHE_PATH, st.st_uid, st.st_gid); - } - - if (setgroups(0, NULL)) { - PLOGE("setgroups"); - } - if (setegid(st.st_gid)) { - PLOGE("setegid (%u)", st.st_gid); - } - if (seteuid(st.st_uid)) { - PLOGE("seteuid (%u)", st.st_uid); - } - - // If db exits, check directly instead query application - dballow = database_check(&ctx); - switch (dballow) { - case INTERACTIVE: - break; - case ALLOW: - allow(); - case DENY: - default: + else deny(); - } - - // New request or no db exist, notify app for response - socket_serv_fd = socket_create_temp(ctx.sock_path, sizeof(ctx.sock_path)); - setup_sighandlers(cleanup_signal); - - // Start activity - app_send_request(su_ctx); - - atexit(socket_cleanup); - - fd = socket_accept(socket_serv_fd); - socket_send_request(fd, &ctx); - socket_receive_result(fd, result, sizeof(result)); - - close(fd); - close(socket_serv_fd); - socket_cleanup(); - - if (strcmp(result, "socket:DENY") == 0) { - deny(); - } else if (strcmp(result, "socket:ALLOW") == 0) { - allow(); - } else { - LOGE("unknown response from Superuser Requestor: %s", result); - deny(); - } } diff --git a/su.h b/su.h index e4edad3a8..4614ef77d 100644 --- a/su.h +++ b/su.h @@ -7,6 +7,8 @@ #include #include +#include "list.h" + #define MAGISKSU_VER_STR xstr(MAGISK_VERSION) ":MAGISKSU (topjohnwu)" // Property check for root access @@ -43,9 +45,20 @@ #define DEFAULT_SHELL "/system/bin/sh" +typedef enum { + QUERY = 0, + DENY = 1, + ALLOW = 2, +} policy_t; + struct su_initiator { pid_t pid; unsigned uid; + policy_t policy; + pthread_mutex_t lock; + int count; + int clock; + struct list_head pos; }; struct su_request { @@ -76,24 +89,21 @@ struct su_user_info { }; struct su_context { - struct su_initiator from; + struct su_initiator *info; struct su_request to; struct su_user_info user; + int notify; mode_t umask; char sock_path[PATH_MAX]; }; -typedef enum { - INTERACTIVE = 0, - DENY = 1, - ALLOW = 2, -} policy_t; - -extern struct ucred su_credentials; +extern struct su_context *su_ctx; +extern int pipefd[2]; // su.c int su_daemon_main(int argc, char **argv); +__attribute__ ((noreturn)) void exit2(int status); // su_client.c @@ -109,7 +119,7 @@ void app_send_request(struct su_context *ctx); // db.c -policy_t database_check(struct su_context *ctx); +void database_check(struct su_context *ctx); // misc.c diff --git a/su_client.c b/su_daemon.c similarity index 52% rename from su_client.c rename to su_daemon.c index 2b4bda7a8..39917fa89 100644 --- a/su_client.c +++ b/su_daemon.c @@ -1,11 +1,12 @@ /* su_client.c - The entrypoint for su, connect to daemon and send correct info */ - +#define _GNU_SOURCE #include #include #include #include +#include #include #include #include @@ -16,17 +17,24 @@ #include "magisk.h" #include "daemon.h" +#include "resetprop.h" #include "utils.h" #include "su.h" #include "pts.h" +#include "list.h" // Constants for the atty bitfield #define ATTY_IN 1 #define ATTY_OUT 2 #define ATTY_ERR 4 -struct ucred su_credentials; -static __thread int client_fd; +#define TIMEOUT 3 + +static struct list_head active_list, waiting_list; +static pthread_t su_collector = 0; +static pthread_mutex_t list_lock = PTHREAD_MUTEX_INITIALIZER; + +int pipefd[2]; static void sighandler(int sig) { restore_stdin(); @@ -53,27 +61,181 @@ static void sighandler(int sig) { static void sigpipe_handler(int sig) { LOGD("su: Client killed unexpectedly\n"); - close(client_fd); - pthread_exit(NULL); +} + +static int get_multiuser_mode() { + char *prop = getprop(MULTIUSER_MODE_PROP); + if (prop) { + int ret = atoi(prop); + free(prop); + return ret; + } + return MULTIUSER_MODE_OWNER_ONLY; +} + + +// Maintain the lists periodically +static void *collector(void *args) { + LOGD("su: collector started\n"); + struct list_head *pos, *temp; + struct su_initiator *node; + while(1) { + sleep(1); + pthread_mutex_lock(&list_lock); + list_for_each(pos, &active_list) { + node = list_entry(pos, struct su_initiator, pos); + --node->clock; + // Timeout, move to waiting list + if (node->clock == 0) { + temp = pos; + pos = pos->prev; + list_pop(temp); + list_insert_end(&waiting_list, temp); + } + } + list_for_each(pos, &waiting_list) { + node = list_entry(pos, struct su_initiator, pos); + // Nothing is using the info, remove it + if (node->count == 0) { + temp = pos; + pos = pos->prev; + list_pop(temp); + pthread_mutex_destroy(&node->lock); + free(node); + } + } + pthread_mutex_unlock(&list_lock); + } } void su_daemon_receiver(int client) { - LOGD("su: get request from client: %d\n", client); + LOGD("su: request from client: %d\n", client); + + struct su_initiator *info = NULL, *node; + struct list_head *pos; + int new_request = 0; + + pthread_mutex_lock(&list_lock); + + if (!su_collector) { + init_list_head(&active_list); + init_list_head(&waiting_list); + xpthread_create(&su_collector, NULL, collector, NULL); + } + + // Get client credntial + struct ucred credential; + get_client_cred(client, &credential); + + // Search for existing in the active list + list_for_each(pos, &active_list) { + node = list_entry(pos, struct su_initiator, pos); + if (node->uid == credential.uid) + info = node; + } + + // If no exist, create a new request + if (info == NULL) { + new_request = 1; + info = malloc(sizeof(*info)); + info->uid = credential.uid; + info->policy = QUERY; + info->count = 0; + pthread_mutex_init(&info->lock, NULL); + list_insert_end(&active_list, &info->pos); + } + info->clock = TIMEOUT; /* Reset timer */ + ++info->count; /* Increment reference count */ + + pthread_mutex_unlock(&list_lock); + + LOGD("su: request from uid=[%d] (#%d)\n", info->uid, info->count); + + // Lock before the policy is determined + pthread_mutex_lock(&info->lock); + + info->pid = credential.pid; + + // Default values + struct su_context ctx = { + .info = info, + .to = { + .uid = UID_ROOT, + .login = 0, + .keepenv = 0, + .shell = DEFAULT_SHELL, + .command = NULL, + }, + .user = { + .android_user_id = info->uid / 100000, + .multiuser_mode = get_multiuser_mode(), + }, + .umask = 022, + .notify = new_request, + }; + su_ctx = &ctx; + + snprintf(su_ctx->user.database_path, PATH_MAX, "%s/%d/%s", + USER_DATA_PATH, su_ctx->user.android_user_id, REQUESTOR_DATABASE_PATH); + snprintf(su_ctx->user.base_path, PATH_MAX, "%s/%d/%s", + USER_DATA_PATH, su_ctx->user.android_user_id, REQUESTOR); + + // verify if Magisk Manager is installed + struct stat st; + xstat(su_ctx->user.base_path, &st); + // odd perms on superuser data dir + if (st.st_gid != st.st_uid) { + LOGE("Bad uid/gid %d/%d for Superuser Requestor application", st.st_uid, st.st_gid); + info->policy = DENY; + } + + // Not cached, do the checks + if (info->policy == QUERY) { + if (su_ctx->user.android_user_id && + su_ctx->user.multiuser_mode == MULTIUSER_MODE_OWNER_ONLY) { + info->policy = DENY; + su_ctx->notify = 0; + } + + // always allow if this is Magisk Manager + if (info->policy == QUERY && (info->uid % 100000) == (st.st_uid % 100000)) { + info->policy = ALLOW; + su_ctx->notify = 0; + } + + // always allow if it's root + if (info->uid == UID_ROOT) { + info->policy = ALLOW; + su_ctx->notify = 0; + } + + // If not determined, check database + if (info->policy == QUERY) + database_check(su_ctx); + + // If still not determined, open a pipe and wait for results + if (info->policy == QUERY) + pipe2(pipefd, O_CLOEXEC); + } // Fork a new process, the child process will need to setsid, // open a pseudo-terminal if needed, and will eventually run exec // The parent process will wait for the result and // send the return code back to our client int child = fork(); - if (child < 0) { - write(client, &child, sizeof(child)); - close(client); + if (child < 0) PLOGE("fork"); - return; - } else if (child != 0) { - // For sighandler to close it - client_fd = client; + if (child) { + // Wait for results + if (info->policy == QUERY) { + xxread(pipefd[0], &info->policy, sizeof(info->policy)); + close(pipefd[0]); + close(pipefd[1]); + } + + // The policy is determined, unlock + pthread_mutex_unlock(&info->lock); // Wait result LOGD("su: wait_result waiting for %d\n", child); @@ -91,10 +253,13 @@ void su_daemon_receiver(int client) { code = -1; // Pass the return code back to the client - write_int(client, code); + write(client, &code, sizeof(code)); /* Might SIGPIPE, ignored */ LOGD("su: return code to client: %d\n", code); close(client); + // Decrement reference count + --info->count; + return; } @@ -106,14 +271,11 @@ void su_daemon_receiver(int client) { // Become session leader xsetsid(); - // Get the credentials - get_client_cred(client, &su_credentials); - // Let's read some info from the socket int argc = read_int(client); if (argc < 0 || argc > 512) { LOGE("unable to allocate args: %d", argc); - exit(1); + exit2(1); } LOGD("su: argc=[%d]\n", argc); @@ -149,9 +311,9 @@ void su_daemon_receiver(int client) { xstat(pts_slave, &stbuf); //If caller is not root, ensure the owner of pts_slave is the caller - if(stbuf.st_uid != su_credentials.uid && su_credentials.uid != 0) { + if(stbuf.st_uid != credential.uid && credential.uid != 0) { LOGE("su: Wrong permission of pts_slave"); - exit(1); + exit2(1); } // Opening the TTY has to occur after the @@ -182,6 +344,20 @@ void su_daemon_receiver(int client) { close(ptsfd); + mkdir(REQUESTOR_CACHE_PATH, 0770); + + if (chown(REQUESTOR_CACHE_PATH, st.st_uid, st.st_gid)) + PLOGE("chown (%s, %u, %u)", REQUESTOR_CACHE_PATH, st.st_uid, st.st_gid); + + if (setgroups(0, NULL)) + PLOGE("setgroups"); + + if (setegid(st.st_gid)) + PLOGE("setegid (%u)", st.st_gid); + + if (seteuid(st.st_uid)) + PLOGE("seteuid (%u)", st.st_uid); + su_daemon_main(argc, argv); } diff --git a/su_socket.c b/su_socket.c index 69c090d16..35a21a0e3 100644 --- a/su_socket.c +++ b/su_socket.c @@ -12,6 +12,7 @@ #include #include #include +#include #include "magisk.h" #include "utils.h" @@ -28,7 +29,7 @@ int socket_create_temp(char *path, size_t len) { memset(&sun, 0, sizeof(sun)); sun.sun_family = AF_LOCAL; - snprintf(path, len, "%s/.socket%d", REQUESTOR_CACHE_PATH, getpid()); + snprintf(path, len, "%s/.socket%d", REQUESTOR_CACHE_PATH, (int) syscall(SYS_gettid)); snprintf(sun.sun_path, sizeof(sun.sun_path), "%s", path); /* @@ -52,8 +53,8 @@ int socket_accept(int serv_fd) { fd_set fds; int rc; - /* Wait 20 seconds for a connection, then give up. */ - tv.tv_sec = 20; + /* Wait 60 seconds for a connection, then give up. */ + tv.tv_sec = 60; tv.tv_usec = 0; FD_ZERO(&fds); FD_SET(serv_fd, &fds); @@ -95,7 +96,7 @@ do { \ } while (0) void socket_send_request(int fd, const struct su_context *ctx) { - write_token(fd, "uid", ctx->from.uid); + write_token(fd, "uid", ctx->info->uid); write_token(fd, "eof", 1); }