@@ -47,7 +47,8 @@ client can restart it.)
4. The server code. This code is fairly simple; all it does is maintain
the database of file information. Operations consist either of a request
for information (e.g., a stat(2) call) or notification of a change. The
-server sends back failure or success notices.
+server sends back failure or success notices for requests for information;
+notifications of a change are sent without waiting for a reply.
As a fairly typical example, the progress of a stat(2) call is:
@@ -14,6 +14,8 @@ libpseudo execution flow:
setup pseudo_functions
setup log files (PSEUDO_DEBUG_FILE, PSEUDO_EVLOG_FILE)
pseudo_client.c: pseudo_init_client()
+ wait for this process's asynchronous requests
+ setup PSEUDO_SYNC
setup PSEUDO_DISABLED
setup pseudo_localstate_dir_fd
setup PSEUDO_NOSYMLINKEXP
@@ -30,6 +32,7 @@ libpseudo execution flow:
pseudo_setupenv()
if PSEUDO_UNLOAD
pseudo_dropenv()
+ pseudo_client_flush()
real_exec*()
fork()
@@ -77,6 +80,14 @@ libpseudo execution flow:
unmask signals (if they were blocked above)
return rc
+ # on exit #
+
+ pseudo_wrappers.c: (destructor) _libpseudo_fini(), or _exit()/_Exit()
+ pseudo_exit_flush()
+ if this process has asynchronous requests pending,
+ and the lock is free (or already ours)
+ pseudo_client_flush()
+
pseudo execution flow:
pseudo.c: main()
pseudo_util.c: pseudo_init_util()
@@ -20,8 +20,10 @@ changes a file and then tells another process about it (through a pipe,
by exiting, or any other way), the server has already queued the change
by the time the second process can ask about the file.
-It also means the server reads each request with a single recv(),
-rather than a header and then a path.
+That is what allows requests that need no reply to be sent without
+waiting for the server (see ASYNC below). It also means the server
+reads each request with a single recv(), rather than a header and then
+a path.
typedef struct {
pseudo_msg_type_t type;
@@ -53,26 +55,41 @@ All messages contain every field up to, but not including, path. If
pathlen is not zero, an additional pathlen bytes containing path are
provided; path is null-terminated.
-Every message from client should get a response from server. The server
-usually sends no path back; the exceptions are replies to xattr requests
-(attribute values or name lists) and the list of client PIDs in a refused
-SHUTDOWN request, described below. Note that
-all server responses will in general share a single message object,
+Every message from the client gets a response from the server, except
+ASYNC requests. The server usually sends no path back; the exceptions
+are replies to xattr requests (attribute values or name lists) and the
+list of client PIDs in a refused SHUTDOWN request, described below.
+Note that all server responses will in general share a single message object,
and future operations may cause that object to be reallocated; the same
goes for messages received by the server. Basically, pseudo_msg_receive
is not thread-safe; this is part of (but not all of) the reason that there's
mutex stuff in the wrappers. (The other part is the "antimagic" being
able to blow things up.)
-type is one of PING, OP, FASTOP, SHUTDOWN, ACK, or NAK. The client
-only sends PING, OP, or FASTOP. FASTOP is used for operations whose result
-the client doesn't need, but the server still replies with ACK (or NAK on
-failure) and the client still waits for that reply.
+type is one of PING, OP, SHUTDOWN, ACK, NAK, or ASYNC. The client
+sends PING, OP and ASYNC, and the server replies to PING and OP with ACK
+(or NAK on failure); the client waits for that reply.
A PING on the stream socket registers the client: the reply gives it the
client_id and client_serial to put in its requests. A PING on the
request socket is answered once every request the client sent before it
-has been handled.
+has been handled; the client uses this to wait for its ASYNC requests.
+
+ASYNC is used instead of OP for operations whose result the client
+doesn't use: chmod, chown, creat, link, mkdir, rename, symlink, unlink,
+did-unlink, cancel-unlink and set-xattr, and open and exec when the
+server is logging. These are marked in enums/op.in. The client doesn't
+wait for them, and the server only replies if the client must abort
+(RESULT_ABORT, for instance after a path mismatch). That reply has type
+ASYNC, and arrives on the stream connection ahead of the reply to the
+client's next OP or PING, which is where the client reads it and aborts.
+To make sure an abort isn't lost, a client that has sent ASYNC requests
+sends a PING on the request socket and waits for the reply before
+exec() and when it exits, including through _exit(). A client that is
+killed doesn't, but the server still applies its requests.
+
+Setting PSEUDO_SYNC in a client's environment to a value other than 0
+makes it send OP instead of ASYNC, waiting for every request.
When run with '-S', the pseudo program
runs as a client, sending a SHUTDOWN message to a server -- but only if
@@ -85,7 +102,7 @@ reduces the timeout to 3 seconds, so it shuts down shortly after the
last client goes away rather than waiting for the full timeout period.)
result is the result of a particular operation. It applies only in replies
-to OP messages.
+to OP and ASYNC messages.
client should be the client's PID on send, and the server's client number for
that client on response. (The response isn't checked, and this is just a
@@ -4,4 +4,4 @@ shutdown
op
ack
nak
-fastop
+async
@@ -1,30 +1,30 @@
-op: OP; int wait = 0
+op: OP; int async = 0
chdir, 0
-chmod, 0
-chown, 0
+chmod, 1
+chown, 1
chroot, 0
close, 0
-creat, 0
+creat, 1
dup, 0
-fchmod, 0
-fchown, 0
-fstat, 1
-link, 0
-mkdir, 0
-mknod, 1
-open, 0
-rename, 0
-stat, 1
-unlink, 0
-symlink, 0
-exec, 0
-may-unlink, 1
-did-unlink, 0
-cancel-unlink, 0
-get-xattr, 1
-list-xattr, 1
-remove-xattr, 1
-set-xattr, 0
-create-xattr, 1
-replace-xattr, 1
+fchmod, 1
+fchown, 1
+fstat, 0
+link, 1
+mkdir, 1
+mknod, 0
+open, 1
+rename, 1
+stat, 0
+unlink, 1
+symlink, 1
+exec, 1
+may-unlink, 0
+did-unlink, 1
+cancel-unlink, 1
+get-xattr, 0
+list-xattr, 0
+remove-xattr, 0
+set-xattr, 1
+create-xattr, 0
+replace-xattr, 0
close-range, 0
new file mode 100644
@@ -0,0 +1,16 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * void _Exit(int status)
+ */
+
+ /* NOTE THAT THIS IS NEVER USED!
+ * The hand-written _Exit() in ../pseudo_wrappers.c flushes
+ * asynchronous requests and exits directly.
+ */
+
+ real__Exit(status);
+
+/* return;
+ * }
+ */
new file mode 100644
@@ -0,0 +1,16 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * void _exit(int status)
+ */
+
+ /* NOTE THAT THIS IS NEVER USED!
+ * The hand-written _exit() in ../pseudo_wrappers.c flushes
+ * asynchronous requests and exits directly.
+ */
+
+ real__exit(status);
+
+/* return;
+ * }
+ */
@@ -33,6 +33,10 @@
/* if exec() fails, we may end up taking signals unexpectedly...
* not much we can do about that.
*/
+ /* exec() skips destructors, so collect errors for requests
+ * sent without waiting now
+ */
+ pseudo_client_flush();
sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
rc = real_execv(file, argv);
@@ -27,6 +27,10 @@
/* if exec() fails, we may end up taking signals unexpectedly...
* not much we can do about that.
*/
+ /* exec() skips destructors, so collect errors for requests
+ * sent without waiting now
+ */
+ pseudo_client_flush();
sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
rc = real_execve(file, argv, new_environ);
@@ -19,6 +19,10 @@
if (pseudo_has_unload(new_environ))
new_environ = pseudo_dropenvp(new_environ);
+ /* exec() skips destructors, so collect errors for requests
+ * sent without waiting now
+ */
+ pseudo_client_flush();
sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
rc = real_execveat(dirfd, path, argv, new_environ, flags);
@@ -33,6 +33,10 @@
/* if exec() fails, we may end up taking signals unexpectedly...
* not much we can do about that.
*/
+ /* exec() skips destructors, so collect errors for requests
+ * sent without waiting now
+ */
+ pseudo_client_flush();
sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
rc = real_execvp(file, argv);
@@ -19,6 +19,10 @@
if (pseudo_has_unload(new_environ))
new_environ = pseudo_dropenvp(new_environ);
+ /* exec() skips destructors, so collect errors for requests
+ * sent without waiting now
+ */
+ pseudo_client_flush();
sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
rc = real_execvpe(file, argv, new_environ);
@@ -11,6 +11,10 @@
if (pseudo_has_unload(new_environ))
new_environ = pseudo_dropenvp(new_environ);
+ /* exec() skips destructors, so collect errors for requests
+ * sent without waiting now
+ */
+ pseudo_client_flush();
sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
rc = real_fexecve(fd, argv, new_environ);
@@ -445,6 +445,29 @@ execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int
return rc;
}
+/* _exit() skips destructors, so collect errors for requests sent
+ * without waiting here instead.
+ */
+void
+_exit(int status) {
+ pseudo_exit_flush();
+ if (real__exit)
+ real__exit(status);
+ syscall(SYS_exit_group, status);
+ for (;;)
+ ;
+}
+
+void
+_Exit(int status) {
+ pseudo_exit_flush();
+ if (real__Exit)
+ real__Exit(status);
+ syscall(SYS_exit_group, status);
+ for (;;)
+ ;
+}
+
/* no profiling in fork because it wouldn't work anyway
* half the time
*/
@@ -543,6 +566,17 @@ wrap_execveat(int dirfd, const char *path, char *const *argv, char *const *envp,
return rc;
}
+/* not used, the hand-written wrappers flush and exit directly */
+static void
+wrap__exit(int status) {
+ (void) status;
+}
+
+static void
+wrap__Exit(int status) {
+ (void) status;
+}
+
static int
wrap_fork(void) {
int rc = -1;
@@ -10,3 +10,5 @@ int posix_spawnp(pid_t *pid, const char *file, const posix_spawn_file_actions_t
int execvpe(const char *file, char *const *argv, char *const *envp); /* hand_wrapped=1 */
int fexecve(int fd, char *const *argv, char *const *envp); /* hand_wrapped=1 */
int execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int flags); /* hand_wrapped=1 */
+void _exit(int status); /* hand_wrapped=1 */
+void _Exit(int status); /* hand_wrapped=1 */
@@ -798,6 +798,21 @@ suppresses informational messages or warnings; errors are always shown.
The default is
.IR info,warn,error .
.TP 8
+.B PSEUDO_SYNC
+By default, requests whose result the client doesn't use, such as
+.IR chown(2)
+or
+.IR chmod(2) ,
+are sent to the server without waiting for it to handle them. The
+server still handles every request in the order clients sent it, so
+other processes always see the change. If the server asks the client
+to abort, for instance after a path mismatch, the client aborts at its
+next request that does wait for a reply, or before
+.IR exec(3)
+or exiting. If this variable is set to a value other than 0, the
+client waits for every request, so it aborts in the call that caused
+the problem. This is slower, but can help when debugging.
+.TP 8
.BR PSEUDO_UIDS ,\ PSEUDO_GIDS
These variables are used internally to pass information about the current
emulated user and group identity from one process to another.
@@ -855,9 +870,11 @@ or debugged in that context.
Filesystem performance is noticably worse under
.I pseudo
than it is otherwise. This is probably because nearly every operation
-(other than reads and writes) involves at least one round-trip network
-communication with the server, and probably some kind of database
-activity.
+(other than reads and writes) involves a message to the server, and
+probably some kind of database activity. Operations that ask about a
+file wait for the server's reply; ones that only report a change do not
+(see
+.BR PSEUDO_SYNC ).
.SH SECURITY
.I pseudo
@@ -1087,7 +1087,7 @@ pseudo_server_response(pseudo_msg_t *msg, const char *program, const char *tag,
return 0;
break;
case PSEUDO_MSG_OP:
- case PSEUDO_MSG_FASTOP:
+ case PSEUDO_MSG_ASYNC:
return pseudo_op(msg, program, tag, response_path, response_len);
break;
case PSEUDO_MSG_ACK: /* FALLTHROUGH */
@@ -53,6 +53,13 @@ static int connect_fd = -1;
static int request_fd = -1;
static int client_id = 0;
static unsigned int client_serial = 0;
+/* requests that need no reply are sent without waiting. async_pending
+ * is set while some may not have been handled yet, so their errors are
+ * collected before exec() or exit().
+ */
+static int async_pending = 0;
+static pid_t async_pid = 0;
+static int pseudo_sync_only = 0;
static int server_pid = 0;
int pseudo_localstate_dir_fd = -1;
static int pseudo_pathlock_fd = -1;
@@ -454,8 +461,17 @@ pseudo_init_client(void) {
int need_free = 0;
pseudo_antimagic();
+ /* collect errors for requests this process sent; a forked child
+ * leaves its parent's to the parent
+ */
+ pseudo_client_flush();
+ async_pending = 0;
pseudo_new_pid();
client_disconnect();
+
+ env = pseudo_get_value("PSEUDO_SYNC");
+ pseudo_sync_only = env && *env && strcmp(env, "0");
+ free(env);
#ifdef PSEUDO_PROFILING
if (pseudo_profile_fd > -1) {
close(pseudo_profile_fd);
@@ -1470,6 +1486,15 @@ pseudo_client_send(pseudo_msg_t *msg, size_t len, const char *path, int tries) {
return -1;
}
+/* Send a request that needs no reply, without waiting for the server. */
+static void
+pseudo_client_send_async(pseudo_msg_t *msg, size_t len, const char *path) {
+ msg->type = PSEUDO_MSG_ASYNC;
+ pseudo_client_send(msg, len, path, 0);
+ async_pending = 1;
+ async_pid = getpid();
+}
+
static pseudo_msg_t *
pseudo_client_request(pseudo_msg_t *msg, size_t len, const char *path) {
pseudo_msg_t *response = 0;
@@ -1488,7 +1513,16 @@ pseudo_client_request(pseudo_msg_t *msg, size_t len, const char *path) {
for (; tries < PSEUDO_RETRIES; ++tries) {
tries = pseudo_client_send(msg, len, path, tries);
pseudo_debug(PDBGF_CLIENT | PDBGF_VERBOSE, "sent!\n");
- response = pseudo_msg_receive(connect_fd);
+ /* replies come back in order, so any errors for earlier
+ * asynchronous requests arrive first
+ */
+ while ((response = pseudo_msg_receive(connect_fd)) != NULL &&
+ response->type == PSEUDO_MSG_ASYNC) {
+ pseudo_debug(PDBGF_CLIENT, "error for asynchronous %s: %s\n",
+ pseudo_op_name(response->op), pseudo_res_name(response->result));
+ if (response->result == RESULT_ABORT)
+ pseudo_client_abort();
+ }
if (!response) {
pseudo_debug(PDBGF_CLIENT, "expected response did not occur; retrying\n");
/* the connection may be gone even though the request
@@ -1496,6 +1530,8 @@ pseudo_client_request(pseudo_msg_t *msg, size_t len, const char *path) {
*/
pseudo_client_setup();
} else {
+ /* everything sent before this request has been handled */
+ async_pending = 0;
if (response->type != PSEUDO_MSG_ACK) {
pseudo_debug(PDBGF_CLIENT, "got non-ack response %d\n", response->type);
return 0;
@@ -1511,6 +1547,34 @@ pseudo_client_request(pseudo_msg_t *msg, size_t len, const char *path) {
return 0;
}
+/* Whether this process has asynchronous requests the server may not
+ * have handled yet.
+ */
+int
+pseudo_client_flush_needed(void) {
+ return async_pending && async_pid == getpid() && request_fd != -1;
+}
+
+/* Wait until the server has handled every asynchronous request this
+ * process sent, so any abort they caused happens before exec() or exit().
+ */
+void
+pseudo_client_flush(void) {
+ pseudo_msg_t msg;
+
+ if (!pseudo_client_flush_needed())
+ return;
+ pseudo_antimagic();
+ memset(&msg, 0, sizeof(msg));
+ msg.type = PSEUDO_MSG_PING;
+ msg.op = OP_NONE;
+ msg.client = getpid();
+ pseudo_debug(PDBGF_CLIENT | PDBGF_VERBOSE, "flushing asynchronous requests\n");
+ pseudo_client_request(&msg, 0, NULL);
+ async_pending = 0;
+ pseudo_magic();
+}
+
int
pseudo_client_shutdown(int wait_on_socket) {
pseudo_msg_t msg;
@@ -2299,8 +2363,6 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path
#ifdef PSEUDO_PROFILING
struct timeval tv1_ipc, tv2_ipc;
#endif
- if (!pseudo_op_wait(msg.op))
- msg.type = PSEUDO_MSG_FASTOP;
pseudo_debug(PDBGF_CLIENT | PDBGF_VERBOSE, "sending request [ino %llu]\n", (unsigned long long) msg.ino);
#ifdef PSEUDO_PROFILING
gettimeofday(&tv1_ipc, NULL);
@@ -2308,6 +2370,8 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path
if (pseudo_local_only) {
/* disable server */
result = NULL;
+ } else if (!pseudo_sync_only && pseudo_op_async(msg.op)) {
+ pseudo_client_send_async(&msg, pathlen, path);
} else {
result = pseudo_client_request(&msg, pathlen, path);
}
@@ -2334,7 +2398,7 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path
(int) result->uid,
(int) result->gid);
}
- } else if (msg.type != PSEUDO_MSG_FASTOP) {
+ } else if (msg.type != PSEUDO_MSG_ASYNC) {
pseudo_debug(PDBGF_OP, "(%d) no answer", getpid());
}
} else if (!result) {
@@ -19,6 +19,8 @@ extern void pseudo_magic(void);
extern void pseudo_client_touchuid(void);
extern void pseudo_client_touchgid(void);
extern int pseudo_client_shutdown(int);
+extern void pseudo_client_flush(void);
+extern int pseudo_client_flush_needed(void);
extern int pseudo_fd(int fd, int how);
extern int pseudo_client_path_lock(int shared);
extern void pseudo_client_path_unlock(int fd);
@@ -508,7 +508,6 @@ serve_client(int i) {
if (in) {
char *response_path = 0;
size_t response_pathlen;
- int send_response = 1;
pseudo_debug(PDBGF_SERVER | PDBGF_VERBOSE, "got a message (%d): %s\n", in->type, (in->pathlen ? in->path : "<no path>"));
/* handle incoming ping */
if (in->type == PSEUDO_MSG_PING && !clients[i].pid) {
@@ -570,20 +569,6 @@ serve_client(int i) {
} else {
in->pathlen = 0;
}
- if (in->type == PSEUDO_MSG_FASTOP) {
- /* respond instantly */
- send_response = 0;
- int t_type = in->type;
- int t_pathlen = in->pathlen;
- in->type = PSEUDO_MSG_ACK;
- in->pathlen = 0;
- if ((rc = pseudo_msg_send(clients[i].fd, in, in->pathlen, response_path)) != 0) {
- pseudo_debug(PDBGF_SERVER, "failed to send response to client %d [%d]: %d (%s)\n",
- i, (int) clients[i].pid, rc, strerror(errno));
- }
- in->type = t_type;
- in->pathlen = t_pathlen;
- }
} else {
/* the server's listen fd is "a client", and
* so is the program connecting to request a shutdown.
@@ -621,13 +606,9 @@ serve_client(int i) {
die_peacefully = 1;
}
}
- if (send_response) {
- if ((rc = pseudo_msg_send(clients[i].fd, in, in->pathlen, response_path)) != 0) {
- pseudo_debug(PDBGF_SERVER, "failed to send response to client %d [%d]: %d (%s)\n",
- i, (int) clients[i].pid, rc, strerror(errno));
- }
- } else {
- rc = 1;
+ if ((rc = pseudo_msg_send(clients[i].fd, in, in->pathlen, response_path)) != 0) {
+ pseudo_debug(PDBGF_SERVER, "failed to send response to client %d [%d]: %d (%s)\n",
+ i, (int) clients[i].pid, rc, strerror(errno));
}
free(response_path);
} else {
@@ -654,8 +635,10 @@ serve_client(int i) {
return rc;
}
-/* Handle one request from the datagram socket, replying on the client's
- * stream connection.
+/* Handle one request from the datagram socket. Synchronous requests
+ * (and PING, which a client uses to wait until everything it sent so far
+ * has been handled) get a reply on the client's stream connection.
+ * Asynchronous ones only get a reply if the client must abort.
*/
static void
serve_request(pseudo_msg_t *in) {
@@ -663,7 +646,7 @@ serve_request(pseudo_msg_t *in) {
char *response_path = 0;
size_t response_pathlen = -1;
int id = in->client_id;
- int valid, rc;
+ int valid, reply = 1, rc;
gettimeofday(&tv1, NULL);
++messages;
@@ -681,7 +664,6 @@ serve_request(pseudo_msg_t *in) {
in->type = PSEUDO_MSG_ACK;
break;
case PSEUDO_MSG_OP:
- case PSEUDO_MSG_FASTOP:
if (pseudo_server_response(in, valid ? clients[id].program : NULL,
valid ? clients[id].tag : NULL,
&response_path, &response_pathlen)) {
@@ -690,12 +672,18 @@ serve_request(pseudo_msg_t *in) {
in->type = PSEUDO_MSG_ACK;
}
break;
+ case PSEUDO_MSG_ASYNC:
+ pseudo_server_response(in, valid ? clients[id].program : NULL,
+ valid ? clients[id].tag : NULL,
+ &response_path, &response_pathlen);
+ reply = (in->result == RESULT_ABORT);
+ break;
default:
pdb_log_msg(SEVERITY_WARN, in, NULL, NULL, "invalid request type %d", in->type);
in->type = PSEUDO_MSG_NAK;
break;
}
- if (valid) {
+ if (reply && valid) {
pseudo_debug(PDBGF_SERVER | PDBGF_VERBOSE, "response: %d (%s)\n",
in->result, pseudo_res_name(in->result));
in->client = id;
@@ -58,6 +58,7 @@ static struct pseudo_variables pseudo_env[] = {
{ "PSEUDO_DISABLED", 15, NULL },
{ "PSEUDO_UNLOAD", 13, NULL },
{ "PSEUDO_ALLOW_FSYNC", 18, NULL },
+ { "PSEUDO_SYNC", 11, NULL },
#ifdef PSEUDO_PROFILING
{ "PSEUDO_PROFILE_PATH", 19, NULL },
#endif
@@ -57,8 +57,10 @@ static pthread_mutex_t pseudo_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t pseudo_mutex_holder;
static int pseudo_mutex_recursion = 0;
static int pseudo_getlock(void);
+static int pseudo_trylock(void);
static void pseudo_droplock(void);
static int pseudo_lock_held(void);
+static void pseudo_exit_flush(void);
static size_t pseudo_dechroot(char *, size_t);
static void pseudo_sigblock(sigset_t *);
@@ -67,6 +69,7 @@ static sigset_t pseudo_saved_sigmask;
/* Constructor only exists in libpseudo */
static void _libpseudo_init(void) __attribute__ ((constructor));
+static void _libpseudo_fini(void) __attribute__ ((destructor));
static int _libpseudo_initted = 0;
@@ -119,6 +122,32 @@ _libpseudo_init(void) {
pseudo_droplock();
}
+/* Requests that need no reply are sent without waiting, so wait at exit
+ * for the server to handle them: if one of them makes the server ask the
+ * client to abort, that happens before the process is gone. Another
+ * thread may be blocked indefinitely inside a wrapper, holding the lock,
+ * so don't wait for the lock; that thread's next request will collect
+ * any abort if it gets one.
+ */
+static void
+pseudo_exit_flush(void) {
+ sigset_t saved;
+
+ if (!_libpseudo_initted || pseudo_disabled || !pseudo_client_flush_needed())
+ return;
+ pseudo_sigblock(&saved);
+ if (pseudo_trylock() == 0) {
+ pseudo_client_flush();
+ pseudo_droplock();
+ }
+ sigprocmask(SIG_SETMASK, &saved, NULL);
+}
+
+static void
+_libpseudo_fini(void) {
+ pseudo_exit_flush();
+}
+
void
pseudo_reinit_libpseudo(void) {
_libpseudo_init();
@@ -228,6 +257,20 @@ pseudo_getlock(void) {
}
static int
+pseudo_trylock(void) {
+ if (pthread_equal(pseudo_mutex_holder, pthread_self())) {
+ ++pseudo_mutex_recursion;
+ return 0;
+ }
+ if (pthread_mutex_trylock(&pseudo_mutex) == 0) {
+ pseudo_mutex_recursion = 1;
+ pseudo_mutex_holder = pthread_self();
+ return 0;
+ }
+ return -1;
+}
+
+static int
pseudo_lock_held(void) {
return pthread_equal(pseudo_mutex_holder, pthread_self());
}
new file mode 100644
@@ -0,0 +1,44 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * chown() a file whose database entry still has another name, which makes
+ * the server tell the client to abort. Prints "returned" if chown()
+ * returns, then does one of the following, which must deliver the abort
+ * if chown() didn't:
+ * stat a synchronous request
+ * _exit leave without running destructors
+ * execvpe run another program
+ *
+ * usage: test-async-abort <path> <stat|_exit|execvpe>
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+int
+main(int argc, char **argv) {
+ struct stat st;
+
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s <path> <stat|_exit|execvpe>\n", argv[0]);
+ return 2;
+ }
+ if (chown(argv[1], 1234, 5678) == -1)
+ perror("chown");
+ printf("returned\n");
+ fflush(stdout);
+ if (!strcmp(argv[2], "_exit")) {
+ _exit(0);
+ } else if (!strcmp(argv[2], "execvpe")) {
+ char *args[] = { "true", NULL };
+ char *env[] = { NULL };
+ execvpe("true", args, env);
+ perror("execvpe");
+ return 2;
+ }
+ (void) stat(".", &st);
+ printf("survived\n");
+ return 0;
+}
new file mode 100755
@@ -0,0 +1,40 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# A chown() whose database entry has another name makes the server tell
+# the client to abort. chown() is sent without waiting, so the client
+# must still abort: at its next request that waits for a reply, or when
+# it leaves with _exit() or execs another program. With PSEUDO_SYNC set,
+# it must abort inside chown() itself.
+#
+# The temporary tree has to live below $PWD so that pseudo tracks it.
+
+tmpdir="$(mktemp -d -p "$PWD" async_abort_XXXXXX)"
+trap "rm -rf '$tmpdir'" EXIT TERM INT
+
+rc=0
+check() {
+ local sync="$1" how="$2" expect="$3" out status
+ : > "$tmpdir/a"
+ # rename it behind pseudo's back, so the database still says "a"
+ PSEUDO_DISABLED=1 mv "$tmpdir/a" "$tmpdir/b"
+ out=$(PSEUDO_SYNC=$sync ./test/test-async-abort "$tmpdir/b" "$how" 2>/dev/null)
+ status=$?
+ # put the name back so the database entry can be removed cleanly
+ PSEUDO_DISABLED=1 mv "$tmpdir/b" "$tmpdir/a"
+ rm -f "$tmpdir/a"
+ if [ $status -ne 134 ]; then
+ echo "FAILED: $how: expected abort (PSEUDO_SYNC=${sync:-unset}), got status $status"
+ rc=1
+ elif [ "$out" != "$expect" ]; then
+ echo "FAILED: $how: PSEUDO_SYNC=${sync:-unset}: expected output '$expect', got '$out'"
+ rc=1
+ fi
+}
+
+for how in stat _exit execvpe; do
+ check "" $how "returned"
+done
+check 1 stat ""
+exit $rc
new file mode 100644
@@ -0,0 +1,188 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * chown() and chmod() are sent to the server without waiting for a reply.
+ * Check that another process never sees the old ownership or mode once it
+ * has been told, through a pipe or by the process exiting, that the change
+ * was made.
+ *
+ * usage: test-async-order <directory> <count>
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+static char *dir;
+static int count;
+#define WRITERS 8
+
+static void
+name(char *buf, size_t len, int i) {
+ snprintf(buf, len, "%s/f%d", dir, i);
+}
+
+static mode_t
+mode_for(int i) {
+ return (i % 2) ? 0640 : 0604;
+}
+
+static int
+check(int i, int want_mode) {
+ char path[4096];
+ struct stat st;
+
+ name(path, sizeof(path), i);
+ if (stat(path, &st) == -1) {
+ fprintf(stderr, "stat %s: %s\n", path, strerror(errno));
+ return 1;
+ }
+ if (st.st_uid != (uid_t) (1000 + i) || st.st_gid != (gid_t) (2000 + i) ||
+ (want_mode && (st.st_mode & 07777) != mode_for(i))) {
+ fprintf(stderr, "%s: got %d:%d mode %o, expected %d:%d mode %o\n",
+ path, (int) st.st_uid, (int) st.st_gid,
+ (unsigned) (st.st_mode & 07777), 1000 + i, 2000 + i,
+ want_mode ? (unsigned) mode_for(i) : 0);
+ return 1;
+ }
+ return 0;
+}
+
+static void
+chown_one(int i) {
+ char path[4096];
+
+ name(path, sizeof(path), i);
+ if (chown(path, 1000 + i, 2000 + i) == -1) {
+ perror("chown");
+ _exit(2);
+ }
+}
+
+/* Each change is handed on through a pipe to a process that is already
+ * running: chown in one process, chmod in a second, stat in a third.
+ */
+static int
+pipe_chain(void) {
+ int p12[2], p23[2];
+ pid_t c2, c3;
+ int i, bad = 0, status;
+
+ if (pipe(p12) == -1 || pipe(p23) == -1) {
+ perror("pipe");
+ return 2;
+ }
+ c2 = fork();
+ if (c2 == 0) {
+ char path[4096];
+ close(p12[1]);
+ close(p23[0]);
+ while (read(p12[0], &i, sizeof(i)) == sizeof(i)) {
+ name(path, sizeof(path), i);
+ if (chmod(path, mode_for(i)) == -1) {
+ perror("chmod");
+ _exit(2);
+ }
+ if (write(p23[1], &i, sizeof(i)) != sizeof(i))
+ _exit(2);
+ }
+ _exit(0);
+ }
+ c3 = fork();
+ if (c3 == 0) {
+ close(p12[0]);
+ close(p12[1]);
+ close(p23[1]);
+ while (read(p23[0], &i, sizeof(i)) == sizeof(i))
+ bad += check(i, 1);
+ _exit(bad ? 1 : 0);
+ }
+ close(p12[0]);
+ close(p23[0]);
+ close(p23[1]);
+ /* send all the changes in a burst before handing them on, newest
+ * first, so the others ask about changes still queued behind
+ * older ones
+ */
+ for (i = 0; i < count; ++i)
+ chown_one(i);
+ for (i = count - 1; i >= 0; --i) {
+ if (write(p12[1], &i, sizeof(i)) != sizeof(i)) {
+ perror("write");
+ return 2;
+ }
+ }
+ close(p12[1]);
+ if (waitpid(c2, &status, 0) == -1 || !WIFEXITED(status) || WEXITSTATUS(status))
+ bad = 1;
+ if (waitpid(c3, &status, 0) == -1 || !WIFEXITED(status) || WEXITSTATUS(status))
+ bad = 1;
+ return bad;
+}
+
+/* Several children change their files at once, so the server has a
+ * backlog from each, and leave with _exit(), which skips the exit-time
+ * flush. The parent checks, newest change first, as soon as they have
+ * all been reaped.
+ */
+static int
+exit_handoff(void) {
+ pid_t c[WRITERS];
+ int i, w, bad = 0, status;
+
+ for (w = 0; w < WRITERS; ++w) {
+ c[w] = fork();
+ if (c[w] == 0) {
+ for (i = 0; i < count; ++i)
+ chown_one(count * (w + 1) + i);
+ _exit(0);
+ }
+ }
+ for (w = 0; w < WRITERS; ++w) {
+ if (waitpid(c[w], &status, 0) == -1 || !WIFEXITED(status) || WEXITSTATUS(status))
+ bad = 2;
+ }
+ if (bad)
+ return bad;
+ for (i = count * (WRITERS + 1) - 1; i >= count; --i)
+ bad += check(i, 0);
+ return bad ? 1 : 0;
+}
+
+int
+main(int argc, char **argv) {
+ char path[4096];
+ int i, fd, rc;
+
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s <directory> <count>\n", argv[0]);
+ return 2;
+ }
+ dir = argv[1];
+ count = atoi(argv[2]);
+ for (i = 0; i < count * (WRITERS + 1); ++i) {
+ name(path, sizeof(path), i);
+ fd = open(path, O_CREAT | O_WRONLY, 0600);
+ if (fd == -1) {
+ perror(path);
+ return 2;
+ }
+ close(fd);
+ }
+ rc = pipe_chain();
+ if (rc) {
+ fprintf(stderr, "pipe hand-off failed\n");
+ return rc;
+ }
+ rc = exit_handoff();
+ if (rc) {
+ fprintf(stderr, "exit hand-off failed\n");
+ return rc;
+ }
+ return 0;
+}
new file mode 100755
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Ownership and mode changes are sent to the server without waiting for a
+# reply. Another process that learns of a change, through a pipe or by
+# reaping the process that made it, must see the new values. Run it
+# both with and without PSEUDO_SYNC, which makes every request wait.
+#
+# The temporary tree has to live below $PWD so that pseudo tracks it.
+
+tmpdir="$(mktemp -d -p "$PWD" async_order_XXXXXX)"
+trap "rm -rf '$tmpdir'" EXIT TERM INT
+
+rc=0
+for sync in "" 1; do
+ rm -f "$tmpdir"/f*
+ if ! PSEUDO_SYNC=$sync ./test/test-async-order "$tmpdir" 500; then
+ echo "FAILED: ordering broken (PSEUDO_SYNC=${sync:-unset})"
+ rc=1
+ fi
+done
+exit $rc