* [pseudo][PATCH 00/34] Various fixes
@ 2026-11-11 4:20 Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 01/34] pseudo_client: serialize path changes across clients Mark Hatle
` (33 more replies)
0 siblings, 34 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
This is a large set. It fixes pretty much all of the issues
I'm aware of, including many found during recent development.
The first 4 changes are related to the long standing issue of parallel
symlink/renames. This is effectively fixed by putting in a shared
lock that serializes disk operations across processes.
Much of the additional work was syncronization for newer Linux systems,
removal of non Linux ports, removal of obsolete code, etc.
Update the pseudo.1 man page and other documentation to match the
current state of the implementation. These files had slowly gone out
of sync over the past about 10 years.
Some simple performance optimizations were also included in this set.
The goal is to eliminate various calls in an attempt to offset the
serialized path lock usage. The items look like they do improve
performance over large numbers of files, but without a significantly
large build, it's unclear if they will make a measurable difference.
Mark Hatle (34):
pseudo_client: serialize path changes across clients
ports: hold the path lock in create and metadata wrappers
ports: check created files under the path lock before recording them
test-parallel-ops: Add a parallel create/rename/link/chmod test
ports: Remove PSEUDO_NO_REAL_AT_FUNCTIONS
ports/unix: Remove guts overridden by the Linux port
ports: Let the port define how pseudo stats files
patches/yocto-older-glibc-symbols.patch: Update to latest YP version
Remove unused #if 0 debug code
Replace #if 0 strerror() debug messages with a comment
pseudo_util: Remove stale with_libpseudo() comment
ports/common/subports: Show the unknown OS name in the error
ports/linux: Drop redundant PSEUDO_LINK_SYMLINK_BEHAVIOR override
pseudo_server: Skip events for clients closed earlier in the batch
pseudo_server: Drop an idle client when out of file descriptors
pseudo_server: Always use epoll
configure: Fix --disable-* options
pseudo_server: Lower highest_client when the highest client closes
pseudo_server: Set highest_client to the new client when growing
pseudo_util: Store PSEUDO_SEVERITY as severity names
pseudo: Fix the -k severity option
pseudo_util: Make -v and numeric PSEUDO_DEBUG add debug flags
AGENTS.md: Add to assist future developers
pseudo_util: Fix the date format in the event log dump
pseudo: List -p in the usage message
pseudo_util: Write the event log to PSEUDO_EVLOG_FILE
pseudo.1: Update to match the current code
ports/linux: Don't require <linux/mount.h>
configure: Require GCC 5 or later
doc: Update README and doc/ to match the code
configure: Fix the --enable-profiling default
pseudo_util: Skip unneeded lstat calls when canonicalizing paths
wrappers: Don't re-block signals in nested wrapper calls
Wrap execvpe(), fexecve() and execveat()
AGENTS.md | 85 +++++++++
Makefile.in | 3 +-
README | 41 +++-
README.security | 4 +-
configure | 47 +++--
doc/chroot | 10 +-
doc/database | 75 +++++---
doc/overview | 21 +--
doc/passwd | 7 +-
doc/program_flow | 16 +-
doc/pseudo_ipc | 37 ++--
doc/utils | 6 +-
makewrappers | 17 ++
patches/yocto-older-glibc-symbols.patch | 4 +-
ports/common/guts/execveat.c | 29 +++
ports/common/guts/execvpe.c | 29 +++
ports/common/guts/fexecve.c | 21 +++
ports/common/pseudo_wrappers.c | 141 ++++++++++++++
ports/common/subports | 2 +-
ports/common/wrapfuncs.in | 3 +
ports/linux/guts/__fxstatat.c | 12 --
ports/linux/guts/__fxstatat64.c | 14 --
ports/linux/guts/__xmknodat.c | 23 +--
ports/linux/guts/fopen64.c | 11 +-
ports/linux/guts/freopen64.c | 11 +-
ports/linux/guts/mkostemp64.c | 4 +-
ports/linux/guts/open_tree.c | 4 +-
ports/linux/guts/open_tree_attr.c | 4 +-
ports/linux/guts/openat.c | 51 ++---
ports/linux/openat2/guts/openat2.c | 44 ++---
ports/linux/portdefs.h | 26 +--
ports/linux/statx/wrapfuncs.in | 2 +-
ports/linux/wrapfuncs.in | 40 ++--
ports/linux/xattr/wrapfuncs.in | 16 +-
ports/unix/guts/faccessat2.c | 12 --
ports/unix/guts/fchmodat.c | 18 --
ports/unix/guts/fchownat.c | 12 --
ports/unix/guts/fopen.c | 5 +-
ports/unix/guts/freopen.c | 5 +-
ports/unix/guts/lchown.c | 15 --
ports/unix/guts/linkat.c | 11 --
ports/unix/guts/lutimes.c | 16 --
ports/unix/guts/mkdirat.c | 17 --
ports/unix/guts/mkfifoat.c | 22 ---
ports/unix/guts/mknod.c | 15 --
ports/unix/guts/mknodat.c | 81 --------
ports/unix/guts/readlinkat.c | 8 -
ports/unix/guts/renameat.c | 12 --
ports/unix/guts/symlinkat.c | 12 --
ports/unix/guts/tmpnam.c | 20 --
ports/unix/guts/unlinkat.c | 25 ---
ports/unix/guts/utimes.c | 15 --
ports/unix/pseudo_wrappers.c | 7 +-
ports/unix/wrapfuncs.in | 56 +++---
pseudo.1 | 318 ++++++++++++++++++++++++++++----
pseudo.c | 5 +-
pseudo.h | 30 ++-
pseudo_client.c | 128 +++++++++++--
pseudo_client.h | 16 +-
pseudo_server.c | 265 +++++---------------------
pseudo_util.c | 93 ++++++----
pseudo_wrappers.c | 9 +-
templates/wrapfuncs.c | 30 +--
test/test-debug-flags.sh | 59 ++++++
test/test-exec.c | 50 +++++
test/test-exec.sh | 31 ++++
test/test-execl.c | 4 -
test/test-execl.sh | 13 --
test/test-open_tree_attr-syscall.c | 1 -
test/test-parallel-ops.sh | 53 ++++++
70 files changed, 1364 insertions(+), 985 deletions(-)
create mode 100644 AGENTS.md
create mode 100644 ports/common/guts/execveat.c
create mode 100644 ports/common/guts/execvpe.c
create mode 100644 ports/common/guts/fexecve.c
delete mode 100644 ports/unix/guts/lchown.c
delete mode 100644 ports/unix/guts/lutimes.c
delete mode 100644 ports/unix/guts/mknod.c
delete mode 100644 ports/unix/guts/mknodat.c
delete mode 100644 ports/unix/guts/tmpnam.c
delete mode 100644 ports/unix/guts/utimes.c
create mode 100755 test/test-debug-flags.sh
create mode 100644 test/test-exec.c
create mode 100755 test/test-exec.sh
delete mode 100644 test/test-execl.c
delete mode 100755 test/test-execl.sh
create mode 100755 test/test-parallel-ops.sh
--
1.8.3.1
^ permalink raw reply [flat|nested] 35+ messages in thread
* [pseudo][PATCH 01/34] pseudo_client: serialize path changes across clients
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 02/34] ports: hold the path lock in create and metadata wrappers Mark Hatle
` (32 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
test-parallel-rename and test-parallel-symlinks failed with "inode
mismatch" and "path mismatch" errors, and clients were aborted by the
server.
rename() and unlink() look at the destination, send OP_MAY_UNLINK,
make the change on disk, and then send OP_DID_UNLINK and OP_RENAME, as
separate steps. The wrapper lock only covers one process, so another
client can rename onto the same name in between. The first client then
marks and removes the wrong inode, and its OP_RENAME finds a different
entry at the destination. ln -sf does the same, as it creates a
temporary symlink and renames it over the link.
Readers race in the same way: a client can lstat() a name before
another client replaces it, but only report the result to the server
afterwards. If the filesystem has meanwhile reused the inode number
for a new file, the server sees the inode under the wrong path, and
aborts the reader.
Add a path lock shared by all clients of a server, using fcntl() locks
on pseudo.pathlock in the local state directory. makewrappers gains a
path_lock option which holds the lock around the guts of a wrapper:
exclusive (path_lock=1) for rename, renameat, unlink, unlinkat and
rmdir, and shared (path_lock=shared) for the stat family, so readers
don't block each other but can't observe a name in the middle of a
change.
fcntl() locks belong to the process, so a forked child doesn't share
them, and they are released if the process dies. The descriptor is
kept open and protected like pseudo's other internal descriptors. If
the lock can't be taken the operation runs unlocked as before.
Both tests now pass consistently, including with PSEUDO_DEBUG set. The
lock adds about 4us to an lstat() (about 12%) and 2% to a rename.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
makewrappers | 17 +++++++++
ports/linux/statx/wrapfuncs.in | 2 +-
ports/linux/wrapfuncs.in | 24 ++++++------
ports/unix/wrapfuncs.in | 10 ++---
pseudo.h | 1 +
pseudo_client.c | 83 +++++++++++++++++++++++++++++++++++++++++-
pseudo_client.h | 2 +
templates/wrapfuncs.c | 2 +-
8 files changed, 121 insertions(+), 20 deletions(-)
diff --git a/makewrappers b/makewrappers
index 1ac46bf..303ae32 100755
--- a/makewrappers
+++ b/makewrappers
@@ -248,6 +248,11 @@ class Function:
self.noignore_path = False
self.hand_wrapped = None
self.async_skip = None
+ # Hold the client path lock (pseudo_client_path_lock()) around
+ # the guts: path_lock=1 (exclusive) for operations that check,
+ # change and then record a path in separate steps, and
+ # path_lock=shared for operations that only look at a path.
+ self.path_lock = None
# used for the copyright date when creating stub functions
self.date = datetime.date.today().year
# Used to define pointers that should EFAULT if null
@@ -492,6 +497,18 @@ class Function:
else:
return "rc ="
+ def path_lock_begin(self):
+ """take the client path lock, for path_lock=1 functions"""
+ if self.path_lock:
+ return "int path_lock_fd = pseudo_client_path_lock(%d);\n\t\t\t" % (self.path_lock == "shared")
+ return ""
+
+ def path_lock_end(self):
+ """release the client path lock, for path_lock=1 functions"""
+ if self.path_lock:
+ return "\n\t\t\tpseudo_client_path_unlock(path_lock_fd);"
+ return ""
+
def def_return(self):
"""return default value (or just return)"""
if self.type == 'void':
diff --git a/ports/linux/statx/wrapfuncs.in b/ports/linux/statx/wrapfuncs.in
index a81fb91..3ca40ac 100644
--- a/ports/linux/statx/wrapfuncs.in
+++ b/ports/linux/statx/wrapfuncs.in
@@ -1 +1 @@
-int statx(int dirfd, const char *path, int flags, unsigned int mask, struct statx *statxbuf); /* efault=path */
+int statx(int dirfd, const char *path, int flags, unsigned int mask, struct statx *statxbuf); /* efault=path, path_lock=shared */
diff --git a/ports/linux/wrapfuncs.in b/ports/linux/wrapfuncs.in
index d417ed3..65f2d7a 100644
--- a/ports/linux/wrapfuncs.in
+++ b/ports/linux/wrapfuncs.in
@@ -2,12 +2,12 @@ int open(const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFO
int __open_2(const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
int __open64_2(const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
char *get_current_dir_name(void);
-int __xstat(int ver, const char *path, struct stat *buf);
-int __lxstat(int ver, const char *path, struct stat *buf); /* flags=AT_SYMLINK_NOFOLLOW */
+int __xstat(int ver, const char *path, struct stat *buf); /* path_lock=shared */
+int __lxstat(int ver, const char *path, struct stat *buf); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
int __fxstat(int ver, int fd, struct stat *buf);
int lchmod(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW */
int lchown(const char *path, uid_t owner, gid_t group); /* flags=AT_SYMLINK_NOFOLLOW */
-int __fxstatat(int ver, int dirfd, const char *path, struct stat *buf, int flags);
+int __fxstatat(int ver, int dirfd, const char *path, struct stat *buf, int flags); /* path_lock=shared */
int openat(int dirfd, const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
int __openat_2(int dirfd, const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
int open_tree(int dirfd, const char *path, unsigned int flags); /* preserve_paths=1, noignore_path=1 */
@@ -25,18 +25,18 @@ int open64(const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NO
int openat64(int dirfd, const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
int __openat64_2(int dirfd, const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
int creat64(const char *path, mode_t mode);
-int stat(const char *path, struct stat *buf); /* real_func=pseudo_stat */
-int lstat(const char *path, struct stat *buf); /* real_func=pseudo_lstat, flags=AT_SYMLINK_NOFOLLOW */
+int stat(const char *path, struct stat *buf); /* real_func=pseudo_stat, path_lock=shared */
+int lstat(const char *path, struct stat *buf); /* real_func=pseudo_lstat, flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
int fstat(int fd, struct stat *buf); /* real_func=pseudo_fstat */
-int fstatat(int dirfd, const char *path, struct stat *buf, int flags);
-int stat64(const char *path, struct stat64 *buf); /* real_func=pseudo_stat64 */
-int lstat64(const char *path, struct stat64 *buf); /* real_func=pseudo_lstat64, flags=AT_SYMLINK_NOFOLLOW */
+int fstatat(int dirfd, const char *path, struct stat *buf, int flags); /* path_lock=shared */
+int stat64(const char *path, struct stat64 *buf); /* real_func=pseudo_stat64, path_lock=shared */
+int lstat64(const char *path, struct stat64 *buf); /* real_func=pseudo_lstat64, flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
int fstat64(int fd, struct stat64 *buf); /* real_func=pseudo_fstat64 */
-int fstatat64(int dirfd, const char *path, struct stat64 *buf, int flags);
-int __xstat64(int ver, const char *path, struct stat64 *buf);
-int __lxstat64(int ver, const char *path, struct stat64 *buf); /* flags=AT_SYMLINK_NOFOLLOW */
+int fstatat64(int dirfd, const char *path, struct stat64 *buf, int flags); /* path_lock=shared */
+int __xstat64(int ver, const char *path, struct stat64 *buf); /* path_lock=shared */
+int __lxstat64(int ver, const char *path, struct stat64 *buf); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
int __fxstat64(int ver, int fd, struct stat64 *buf);
-int __fxstatat64(int ver, int dirfd, const char *path, struct stat64 *buf, int flags);
+int __fxstatat64(int ver, int dirfd, const char *path, struct stat64 *buf, int flags); /* path_lock=shared */
FILE *fopen64(const char *path, const char *mode); /* noignore_path=1 */
int nftw64(const char *path, int (*fn)(const char *, const struct stat64 *, int, struct FTW *), int nopenfd, int flag); /* noignore_path=1 */
FILE *freopen64(const char *path, const char *mode, FILE *stream); /* noignore_path=1 */
diff --git a/ports/unix/wrapfuncs.in b/ports/unix/wrapfuncs.in
index d797235..5956742 100644
--- a/ports/unix/wrapfuncs.in
+++ b/ports/unix/wrapfuncs.in
@@ -53,13 +53,13 @@ int mkstemp(char *template); /* flags=AT_SYMLINK_NOFOLLOW */
int mkostemp(char *template, int oflags); /* flags=AT_SYMLINK_NOFOLLOW */
int mkstemps(char *template, int suffixlen); /* flags=AT_SYMLINK_NOFOLLOW */
int mkostemps(char *template, int suffixlen, int oflags); /* flags=AT_SYMLINK_NOFOLLOW */
-int rename(const char *oldpath, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW */
-int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW */
-int rmdir(const char *path); /* flags=AT_SYMLINK_NOFOLLOW */
+int rename(const char *oldpath, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int rmdir(const char *path); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int symlink(const char *oldname, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW */
int symlinkat(const char *oldname, int dirfd, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW */
-int unlink(const char *path); /* flags=AT_SYMLINK_NOFOLLOW */
-int unlinkat(int dirfd, const char *path, int rflags); /* flags=AT_SYMLINK_NOFOLLOW */
+int unlink(const char *path); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int unlinkat(int dirfd, const char *path, int rflags); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
# primarily for use with chroot()
ssize_t readlink(const char *path, char *buf, size_t bufsiz); /* flags=AT_SYMLINK_NOFOLLOW */
ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t bufsiz); /* flags=AT_SYMLINK_NOFOLLOW */
diff --git a/pseudo.h b/pseudo.h
index 0047f2f..6d50b77 100644
--- a/pseudo.h
+++ b/pseudo.h
@@ -129,6 +129,7 @@ extern char *pseudo_version;
#define PSEUDO_LOCKFILE "pseudo.lock"
#define PSEUDO_LOGFILE "pseudo.log"
+#define PSEUDO_PATHLOCK "pseudo.pathlock"
#define PSEUDO_PIDFILE "pseudo.pid"
#define PSEUDO_SOCKET "pseudo.socket"
diff --git a/pseudo_client.c b/pseudo_client.c
index 7f425dd..ffd3450 100644
--- a/pseudo_client.c
+++ b/pseudo_client.c
@@ -48,6 +48,7 @@ static char *base_path(int dirfd, const char *path, int leave_last);
static int connect_fd = -1;
static int server_pid = 0;
int pseudo_localstate_dir_fd = -1;
+static int pseudo_pathlock_fd = -1;
int pseudo_pwd_fd = -1;
int pseudo_pwd_lck_fd = -1;
char *pseudo_pwd_lck_name = NULL;
@@ -533,6 +534,7 @@ pseudo_init_client(void) {
/* Ensure that all of the values are reset */
server_pid = 0;
pseudo_localstate_dir_fd = -1;
+ pseudo_pathlock_fd = -1;
pseudo_pwd_fd = -1;
pseudo_pwd_lck_fd = -1;
pseudo_pwd_lck_name = NULL;
@@ -1149,6 +1151,80 @@ pseudo_fd(int fd, int how) {
return(newfd);
}
+/* Operations such as rename() and unlink() look at the destination, change
+ * it on disk, and then tell the server what happened, in several separate
+ * steps. Another client doing the same to the same name in between makes
+ * the database describe a file that is no longer there, which the server
+ * reports as an inode or path mismatch. Wrappers marked path_lock=1 hold
+ * this lock, shared by every client of the server, for the whole sequence.
+ *
+ * fcntl() locks belong to the process, so a forked child does not share
+ * them even though it inherits the descriptor, and they are released if
+ * the process dies. The descriptor is kept open and protected like
+ * pseudo's other internal descriptors.
+ *
+ * Wrappers that only look at a path (stat() and friends) take the lock
+ * shared: they don't block each other, but can't observe a path between
+ * the change on disk and the database update.
+ *
+ * Returns the descriptor to pass to pseudo_client_path_unlock(), or -1 if
+ * the lock could not be taken, in which case the operation runs unlocked.
+ */
+int
+pseudo_client_path_lock(int shared) {
+ static char *lock_path = NULL;
+ struct flock lk = { .l_type = shared ? F_RDLCK : F_WRLCK, .l_whence = SEEK_SET };
+ int save_errno = errno;
+ int locked = 0, retried = 0;
+
+ pseudo_antimagic();
+ for (;;) {
+ if (pseudo_pathlock_fd == -1) {
+ if (!lock_path)
+ lock_path = pseudo_localstatedir_path(PSEUDO_PATHLOCK);
+ if (lock_path)
+ pseudo_pathlock_fd = pseudo_fd(open(lock_path,
+ O_RDWR | O_CREAT | O_CLOEXEC, 0644), MOVE_FD);
+ if (pseudo_pathlock_fd == -1) {
+ pseudo_debug(PDBGF_CLIENT, "path lock: can't open '%s': %s\n",
+ lock_path ? lock_path : "<nil>", strerror(errno));
+ break;
+ }
+ }
+ if (fcntl(pseudo_pathlock_fd, F_SETLKW, &lk) == 0) {
+ locked = 1;
+ break;
+ }
+ if (errno == EINTR)
+ continue;
+ /* closed behind our back: reopen it once */
+ if (errno == EBADF && !retried) {
+ pseudo_pathlock_fd = -1;
+ retried = 1;
+ continue;
+ }
+ pseudo_debug(PDBGF_CLIENT, "path lock: can't lock '%s': %s\n",
+ lock_path ? lock_path : "<nil>", strerror(errno));
+ break;
+ }
+ pseudo_magic();
+ errno = save_errno;
+ return locked ? pseudo_pathlock_fd : -1;
+}
+
+void
+pseudo_client_path_unlock(int fd) {
+ struct flock lk = { .l_type = F_UNLCK, .l_whence = SEEK_SET };
+ int save_errno = errno;
+
+ if (fd != -1) {
+ pseudo_antimagic();
+ fcntl(fd, F_SETLK, &lk);
+ pseudo_magic();
+ }
+ errno = save_errno;
+}
+
static int
client_connect(void) {
/* we have a server pid, is it responsive? */
@@ -2018,6 +2094,8 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path
startfd = pseudo_util_evlog_fd + 1;
if (pseudo_localstate_dir_fd >= startfd)
startfd = pseudo_localstate_dir_fd + 1;
+ if (pseudo_pathlock_fd >= startfd)
+ startfd = pseudo_pathlock_fd + 1;
if (pseudo_pwd_fd >= startfd)
startfd = pseudo_pwd_fd + 1;
if (pseudo_pwd_lck_fd >= startfd)
@@ -2031,7 +2109,8 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path
*/
for (i = fd; i < startfd && i <= close_range_maxfd; ++i) {
if (i == pseudo_util_debug_fd || i == pseudo_util_evlog_fd ||
- i == pseudo_localstate_dir_fd || i == pseudo_pwd_fd ||
+ i == pseudo_localstate_dir_fd || i == pseudo_pathlock_fd ||
+ i == pseudo_pwd_fd ||
i == pseudo_pwd_lck_fd || i == pseudo_grp_fd ||
i == connect_fd)
continue;
@@ -2057,6 +2136,8 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path
pseudo_util_debug_fd = pseudo_fd(fd, COPY_FD);
} else if (fd == pseudo_localstate_dir_fd) {
pseudo_localstate_dir_fd = pseudo_fd(fd, COPY_FD);
+ } else if (fd == pseudo_pathlock_fd) {
+ pseudo_pathlock_fd = pseudo_fd(fd, COPY_FD);
} else if (fd == pseudo_pwd_fd) {
pseudo_pwd_fd = pseudo_fd(fd, COPY_FD);
/* since we have a FILE * on it, we close that... */
diff --git a/pseudo_client.h b/pseudo_client.h
index 19f3901..3958f6d 100644
--- a/pseudo_client.h
+++ b/pseudo_client.h
@@ -27,6 +27,8 @@ extern void pseudo_client_touchuid(void);
extern void pseudo_client_touchgid(void);
extern int pseudo_client_shutdown(int);
extern int pseudo_fd(int fd, int how);
+extern int pseudo_client_path_lock(int shared);
+extern void pseudo_client_path_unlock(int fd);
#define MOVE_FD 0
#define COPY_FD 1
#define PSEUDO_MIN_FD 20
diff --git a/templates/wrapfuncs.c b/templates/wrapfuncs.c
index 9385a40..a57c997 100644
--- a/templates/wrapfuncs.c
+++ b/templates/wrapfuncs.c
@@ -68,7 +68,7 @@ ${maybe_async_skip}
} else {
/* exec*() use this to restore the sig mask */
pseudo_saved_sigmask = saved;
- ${rc_assign} wrap_$name(${call_args});
+ ${path_lock_begin}${rc_assign} wrap_$name(${call_args});${path_lock_end}
}
}
${variadic_end}
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 02/34] ports: hold the path lock in create and metadata wrappers
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 01/34] pseudo_client: serialize path changes across clients Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 03/34] ports: check created files under the path lock before recording them Mark Hatle
` (31 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Like rename() and unlink(), the wrappers that create a name, or that
look at a path and then change its metadata in the database, do so in
separate steps: the real operation on disk, then a request to the
server. Another client renaming onto or removing the same name in
between leaves the database out of step with the filesystem, which the
server reports as an inode or path mismatch, and may abort a client.
Hold the path lock exclusively in the wrappers that create a name:
link, linkat, symlink, symlinkat, mkdir, mkdirat, mkfifo, mkfifoat,
mknod, mknodat, __xmknod, __xmknodat, bind, mkdtemp and the mkstemp
family. None of these can block indefinitely.
Hold it shared in the wrappers that look at a path and then record or
query its metadata: chmod, lchmod, fchmodat, chown, lchown, fchownat,
fts_read, fts_children, and the path based xattr calls.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/linux/wrapfuncs.in | 16 +++++++--------
ports/linux/xattr/wrapfuncs.in | 16 +++++++--------
ports/unix/wrapfuncs.in | 46 +++++++++++++++++++++---------------------
3 files changed, 39 insertions(+), 39 deletions(-)
diff --git a/ports/linux/wrapfuncs.in b/ports/linux/wrapfuncs.in
index 65f2d7a..a682383 100644
--- a/ports/linux/wrapfuncs.in
+++ b/ports/linux/wrapfuncs.in
@@ -5,17 +5,17 @@ char *get_current_dir_name(void);
int __xstat(int ver, const char *path, struct stat *buf); /* path_lock=shared */
int __lxstat(int ver, const char *path, struct stat *buf); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
int __fxstat(int ver, int fd, struct stat *buf);
-int lchmod(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW */
-int lchown(const char *path, uid_t owner, gid_t group); /* flags=AT_SYMLINK_NOFOLLOW */
+int lchmod(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
+int lchown(const char *path, uid_t owner, gid_t group); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
int __fxstatat(int ver, int dirfd, const char *path, struct stat *buf, int flags); /* path_lock=shared */
int openat(int dirfd, const char *path, int flags, ...{mode_t mode}); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
int __openat_2(int dirfd, const char *path, int flags); /* flags=((flags&O_NOFOLLOW)||((flags&(O_CREAT|O_EXCL))==(O_CREAT|O_EXCL))), noignore_path=1 */
int open_tree(int dirfd, const char *path, unsigned int flags); /* preserve_paths=1, noignore_path=1 */
int open_tree_attr(int dirfd, const char *path, unsigned int flags, struct mount_attr *attr, size_t size); /* preserve_paths=1, noignore_path=1*/
-int mknod(const char *path, mode_t mode, dev_t dev); /* real_func=pseudo_mknod */
-int mknodat(int dirfd, const char *path, mode_t mode, dev_t dev); /* real_func=pseudo_mknodat */
-int __xmknod(int ver, const char *path, mode_t mode, dev_t *dev); /* flags=AT_SYMLINK_NOFOLLOW */
-int __xmknodat(int ver, int dirfd, const char *path, mode_t mode, dev_t *dev); /* flags=AT_SYMLINK_NOFOLLOW */
+int mknod(const char *path, mode_t mode, dev_t dev); /* real_func=pseudo_mknod, path_lock=1 */
+int mknodat(int dirfd, const char *path, mode_t mode, dev_t dev); /* real_func=pseudo_mknodat, path_lock=1 */
+int __xmknod(int ver, const char *path, mode_t mode, dev_t *dev); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int __xmknodat(int ver, int dirfd, const char *path, mode_t mode, dev_t *dev); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int fcntl(int fd, int cmd, ...{struct flock *lock}); /* noignore_path=1 */
int fcntl64(int fd, int cmd, ...{struct flock *lock}); /* noignore_path=1 */
# just so we know the inums of symlinks
@@ -44,8 +44,8 @@ int ftw64(const char *path, int (*fn)(const char *, const struct stat64 *, int),
int glob64(const char *pattern, int flags, int (*errfunc)(const char *, int), glob64_t *pglob);
int scandir64(const char *path, struct dirent64 ***namelist, int (*filter)(const struct dirent64 *), int (*compar)());
int truncate64(const char *path, off64_t length);
-int mkostemp64(char *template, int oflags); /* flags=AT_SYMLINK_NOFOLLOW */
-int mkstemp64(char *template); /* flags=AT_SYMLINK_NOFOLLOW */
+int mkostemp64(char *template, int oflags); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mkstemp64(char *template); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups);
int setgroups(size_t size, const gid_t *list);
int setfsgid(gid_t fsgid);
diff --git a/ports/linux/xattr/wrapfuncs.in b/ports/linux/xattr/wrapfuncs.in
index 09eba23..89b1c68 100644
--- a/ports/linux/xattr/wrapfuncs.in
+++ b/ports/linux/xattr/wrapfuncs.in
@@ -1,12 +1,12 @@
-ssize_t getxattr(const char *path, const char *name, void *value, size_t size); /* flags=0, version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
-ssize_t lgetxattr(const char *path, const char *name, void *value, size_t size); /* flags=AT_SYMLINK_NOFOLLOW, version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
+ssize_t getxattr(const char *path, const char *name, void *value, size_t size); /* flags=0, version="GLIBC_2.3", version-aarch64="GLIBC_2.17", path_lock=shared */
+ssize_t lgetxattr(const char *path, const char *name, void *value, size_t size); /* flags=AT_SYMLINK_NOFOLLOW, version="GLIBC_2.3", version-aarch64="GLIBC_2.17", path_lock=shared */
ssize_t fgetxattr(int filedes, const char *name, void *value, size_t size); /* version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
-int setxattr(const char *path, const char *name, const void *value, size_t size, int xflags); /* flags=0, version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
-int lsetxattr(const char *path, const char *name, const void *value, size_t size, int xflags); /* flags=AT_SYMLINK_NOFOLLOW, version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
+int setxattr(const char *path, const char *name, const void *value, size_t size, int xflags); /* flags=0, version="GLIBC_2.3", version-aarch64="GLIBC_2.17", path_lock=shared */
+int lsetxattr(const char *path, const char *name, const void *value, size_t size, int xflags); /* flags=AT_SYMLINK_NOFOLLOW, version="GLIBC_2.3", version-aarch64="GLIBC_2.17", path_lock=shared */
int fsetxattr(int filedes, const char *name, const void *value, size_t size, int xflags); /* version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
-ssize_t listxattr(const char *path, char *list, size_t size); /* flags=0, version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
-ssize_t llistxattr(const char *path, char *list, size_t size); /* flags=AT_SYMLINK_NOFOLLOW, version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
+ssize_t listxattr(const char *path, char *list, size_t size); /* flags=0, version="GLIBC_2.3", version-aarch64="GLIBC_2.17", path_lock=shared */
+ssize_t llistxattr(const char *path, char *list, size_t size); /* flags=AT_SYMLINK_NOFOLLOW, version="GLIBC_2.3", version-aarch64="GLIBC_2.17", path_lock=shared */
ssize_t flistxattr(int filedes, char *list, size_t size); /* version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
-int removexattr(const char *path, const char *name); /* flags=0, version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
-int lremovexattr(const char *path, const char *name); /* flags=AT_SYMLINK_NOFOLLOW, version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
+int removexattr(const char *path, const char *name); /* flags=0, version="GLIBC_2.3", version-aarch64="GLIBC_2.17", path_lock=shared */
+int lremovexattr(const char *path, const char *name); /* flags=AT_SYMLINK_NOFOLLOW, version="GLIBC_2.3", version-aarch64="GLIBC_2.17", path_lock=shared */
int fremovexattr(int filedes, const char *name); /* version="GLIBC_2.3", version-aarch64="GLIBC_2.17" */
diff --git a/ports/unix/wrapfuncs.in b/ports/unix/wrapfuncs.in
index 5956742..5ffee83 100644
--- a/ports/unix/wrapfuncs.in
+++ b/ports/unix/wrapfuncs.in
@@ -4,7 +4,7 @@ char *getwd(char *buf);
int close(int fd); /* noignore_path=1 */
int fchmod(int fd, mode_t mode);
int fchown(int fd, uid_t owner, gid_t group);
-int lchown(const char *path, uid_t owner, gid_t group); /* flags=AT_SYMLINK_NOFOLLOW */
+int lchown(const char *path, uid_t owner, gid_t group); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
int dup2(int oldfd, int newfd); /* noignore_path=1 */
int dup(int fd); /* noignore_path=1 */
int chdir(const char *path); /* noignore_path=1 */
@@ -13,13 +13,13 @@ int access(const char *path, int mode);
int faccessat(int dirfd, const char *path, int mode, int flags);
int faccessat2(int dirfd, const char *path, int mode, int flags);
FTS *fts_open(char * const *path_argv, int options, int (*compar)(const FTSENT **, const FTSENT **));
-FTSENT *fts_read(FTS *ftsp); /* noignore_path=1 */
-FTSENT *fts_children(FTS *ftsp, int options); /* noignore_path=1 */
+FTSENT *fts_read(FTS *ftsp); /* noignore_path=1, path_lock=shared */
+FTSENT *fts_children(FTS *ftsp, int options); /* noignore_path=1, path_lock=shared */
int ftw(const char *path, int (*fn)(const char *, const struct stat *, int), int nopenfd);
int nftw(const char *path, int (*fn)(const char *, const struct stat *, int, struct FTW *), int nopenfd, int flag);
int glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *pglob);
int lutimes(const char *path, const struct timeval *tv); /* flags=AT_SYMLINK_NOFOLLOW */
-char *mkdtemp(char *template);
+char *mkdtemp(char *template); /* path_lock=1 */
char *mktemp(char *template);
long pathconf(const char *path, int name);
char *realpath(const char *name, char *resolved_name); /* version="GLIBC_2.3" */
@@ -37,27 +37,27 @@ int fclose(FILE *fp); /* noignore_path=1 */
FILE *freopen(const char *path, const char *mode, FILE *stream); /* noignore_path=1 */
int chroot(const char *path); /* noignore_path=1 */
int acct(const char *path);
-int chmod(const char *path, mode_t mode);
-int chown(const char *path, uid_t owner, gid_t group);
-int fchmodat(int dirfd, const char *path, mode_t mode, int flags);
-int fchownat(int dirfd, const char *path, uid_t owner, gid_t group, int flags);
-int link(const char *oldname, const char *newname); /* flags=AT_SYMLINK_NOFOLLOW */
-int linkat(int olddirfd, const char *oldname, int newdirfd, const char *newname, int flags);
-int mkdir(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW */
-int mkdirat(int dirfd, const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW */
-int mkfifo(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW */
-int mkfifoat(int dirfd, const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW */
-int mknod(const char *path, mode_t mode, dev_t dev); /* flags=AT_SYMLINK_NOFOLLOW */
-int mknodat(int dirfd, const char *path, mode_t mode, dev_t dev); /* flags=AT_SYMLINK_NOFOLLOW */
-int mkstemp(char *template); /* flags=AT_SYMLINK_NOFOLLOW */
-int mkostemp(char *template, int oflags); /* flags=AT_SYMLINK_NOFOLLOW */
-int mkstemps(char *template, int suffixlen); /* flags=AT_SYMLINK_NOFOLLOW */
-int mkostemps(char *template, int suffixlen, int oflags); /* flags=AT_SYMLINK_NOFOLLOW */
+int chmod(const char *path, mode_t mode); /* path_lock=shared */
+int chown(const char *path, uid_t owner, gid_t group); /* path_lock=shared */
+int fchmodat(int dirfd, const char *path, mode_t mode, int flags); /* path_lock=shared */
+int fchownat(int dirfd, const char *path, uid_t owner, gid_t group, int flags); /* path_lock=shared */
+int link(const char *oldname, const char *newname); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int linkat(int olddirfd, const char *oldname, int newdirfd, const char *newname, int flags); /* path_lock=1 */
+int mkdir(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mkdirat(int dirfd, const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mkfifo(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mkfifoat(int dirfd, const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mknod(const char *path, mode_t mode, dev_t dev); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mknodat(int dirfd, const char *path, mode_t mode, dev_t dev); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mkstemp(char *template); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mkostemp(char *template, int oflags); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mkstemps(char *template, int suffixlen); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int mkostemps(char *template, int suffixlen, int oflags); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int rename(const char *oldpath, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int rmdir(const char *path); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
-int symlink(const char *oldname, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW */
-int symlinkat(const char *oldname, int dirfd, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW */
+int symlink(const char *oldname, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
+int symlinkat(const char *oldname, int dirfd, const char *newpath); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int unlink(const char *path); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int unlinkat(int dirfd, const char *path, int rflags); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
# primarily for use with chroot()
@@ -74,4 +74,4 @@ void sync(void); /* async_skip= */
int sync_file_range(int fd, off64_t offset, off64_t nbytes, unsigned int flags); /* async_skip=0 */
int msync(void *addr, size_t length, int flags); /* async_skip=0 */
mode_t umask(mode_t mask);
-int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
+int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); /* path_lock=1 */
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 03/34] ports: check created files under the path lock before recording them
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 01/34] pseudo_client: serialize path changes across clients Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 02/34] ports: hold the path lock in create and metadata wrappers Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 04/34] test-parallel-ops: Add a parallel create/rename/link/chmod test Mark Hatle
` (30 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
open() and friends with O_CREAT create a file and then send OP_CREAT
for the path, so another client can rename onto or remove the name in
between, like the other wrappers that create names. They can't hold
the path lock across the real call, as opening a FIFO can block
indefinitely, which could deadlock against a client waiting for the
lock.
Instead, take the lock after the file has been opened, and only send
OP_CREAT if the path still names the file that was opened, using a new
pseudo_client_path_matches_fd(). With the lock held no other client is
part way through changing the path, so if it now names a different
file, that client has already recorded the change and recording ours
would be wrong.
This covers openat (and so open, creat and their variants), openat2,
fopen, freopen, fopen64 and freopen64. openat and openat2 now also take
the recorded stat data from the descriptor rather than from an earlier
lookup of the path.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/linux/guts/fopen64.c | 5 ++++-
ports/linux/guts/freopen64.c | 5 ++++-
ports/linux/guts/openat.c | 8 +++++++-
ports/linux/openat2/guts/openat2.c | 8 +++++++-
ports/unix/guts/fopen.c | 5 ++++-
ports/unix/guts/freopen.c | 5 ++++-
pseudo_client.c | 25 +++++++++++++++++++++++++
pseudo_client.h | 1 +
8 files changed, 56 insertions(+), 6 deletions(-)
diff --git a/ports/linux/guts/fopen64.c b/ports/linux/guts/fopen64.c
index e76da69..a6efd18 100644
--- a/ports/linux/guts/fopen64.c
+++ b/ports/linux/guts/fopen64.c
@@ -22,8 +22,11 @@
pseudo_debug(PDBGF_FILE, "fopen64 '%s': fd %d <FILE %p>\n", path, fd, (void *) rc);
if (real___fxstat64(_STAT_VER, fd, &buf) != -1) {
if (!existed) {
+ int path_lock_fd = pseudo_client_path_lock(0);
real_fchmod(fd, PSEUDO_FS_MODE(0666 & ~pseudo_umask, 0));
- pseudo_client_op(OP_CREAT, 0, -1, -1, path, &buf);
+ if (pseudo_client_path_matches_fd(path, fd, 0))
+ pseudo_client_op(OP_CREAT, 0, -1, -1, path, &buf);
+ pseudo_client_path_unlock(path_lock_fd);
}
pseudo_client_op(OP_OPEN, pseudo_access_fopen(mode), fd, -1, path, &buf);
} else {
diff --git a/ports/linux/guts/freopen64.c b/ports/linux/guts/freopen64.c
index 5fc9073..6057eda 100644
--- a/ports/linux/guts/freopen64.c
+++ b/ports/linux/guts/freopen64.c
@@ -21,8 +21,11 @@
pseudo_debug(PDBGF_FILE, "freopen64 '%s': fd %d\n", path, fd);
if (real___fxstat64(_STAT_VER, fd, &buf) != -1) {
if (!existed) {
+ int path_lock_fd = pseudo_client_path_lock(0);
real_fchmod(fd, PSEUDO_FS_MODE(0666 & ~pseudo_umask, 0));
- pseudo_client_op(OP_CREAT, 0, -1, -1, path, &buf);
+ if (pseudo_client_path_matches_fd(path, fd, 0))
+ pseudo_client_op(OP_CREAT, 0, -1, -1, path, &buf);
+ pseudo_client_path_unlock(path_lock_fd);
}
pseudo_client_op(OP_OPEN, pseudo_access_fopen(mode), fd, -1, path, &buf);
} else {
diff --git a/ports/linux/guts/openat.c b/ports/linux/guts/openat.c
index 0a44063..4fd83dd 100644
--- a/ports/linux/guts/openat.c
+++ b/ports/linux/guts/openat.c
@@ -147,9 +147,15 @@
if (stat_rc != -1) {
buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, mode);
if (!existed) {
+ int path_lock_fd = pseudo_client_path_lock(0);
real_fchmod(rc, PSEUDO_FS_MODE(mode, 0));
// file has no path, but has been created
- pseudo_client_op(OP_CREAT, 0, -1, dirfd, path, &buf);
+ if (pseudo_client_path_matches_fd(path, rc, flags & O_NOFOLLOW) &&
+ real___fxstat64(_STAT_VER, rc, &buf) != -1) {
+ buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, mode);
+ pseudo_client_op(OP_CREAT, 0, -1, dirfd, path, &buf);
+ }
+ pseudo_client_path_unlock(path_lock_fd);
}
pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(flags), rc, dirfd, path, &buf);
} else {
diff --git a/ports/linux/openat2/guts/openat2.c b/ports/linux/openat2/guts/openat2.c
index f1c523e..06fcec8 100644
--- a/ports/linux/openat2/guts/openat2.c
+++ b/ports/linux/openat2/guts/openat2.c
@@ -182,9 +182,15 @@
if (stat_rc != -1) {
buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, my_how.mode);
if (!existed) {
+ int path_lock_fd = pseudo_client_path_lock(0);
real_fchmod(rc, PSEUDO_FS_MODE(my_how.mode, 0));
// file has no path, but has been created
- pseudo_client_op(OP_CREAT, 0, -1, dirfd, pseudo_path, &buf);
+ if (pseudo_client_path_matches_fd(pseudo_path, rc, my_how.flags & O_NOFOLLOW) &&
+ real___fxstat64(_STAT_VER, rc, &buf) != -1) {
+ buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, my_how.mode);
+ pseudo_client_op(OP_CREAT, 0, -1, dirfd, pseudo_path, &buf);
+ }
+ pseudo_client_path_unlock(path_lock_fd);
}
pseudo_client_op(OP_OPEN, PSEUDO_ACCESS(my_how.flags), rc, dirfd, pseudo_path, &buf);
} else {
diff --git a/ports/unix/guts/fopen.c b/ports/unix/guts/fopen.c
index 26a6abc..78388f7 100644
--- a/ports/unix/guts/fopen.c
+++ b/ports/unix/guts/fopen.c
@@ -21,8 +21,11 @@
pseudo_debug(PDBGF_OP, "fopen '%s': fd %d <FILE %p>\n", path, fd, (void *) rc);
if (base_fstat(fd, &buf) != -1) {
if (!existed) {
+ int path_lock_fd = pseudo_client_path_lock(0);
real_fchmod(fd, PSEUDO_FS_MODE(0666 & ~pseudo_umask, 0));
- pseudo_client_op(OP_CREAT, 0, -1, -1, path, &buf);
+ if (pseudo_client_path_matches_fd(path, fd, 0))
+ pseudo_client_op(OP_CREAT, 0, -1, -1, path, &buf);
+ pseudo_client_path_unlock(path_lock_fd);
}
pseudo_client_op(OP_OPEN, pseudo_access_fopen(mode), fd, -1, path, &buf);
} else {
diff --git a/ports/unix/guts/freopen.c b/ports/unix/guts/freopen.c
index 703b6aa..5d63c9f 100644
--- a/ports/unix/guts/freopen.c
+++ b/ports/unix/guts/freopen.c
@@ -21,8 +21,11 @@
pseudo_debug(PDBGF_OP, "freopen '%s': fd %d\n", path, fd);
if (base_fstat(fd, &buf) != -1) {
if (!existed) {
+ int path_lock_fd = pseudo_client_path_lock(0);
real_fchmod(fd, PSEUDO_FS_MODE(0666 & ~pseudo_umask, 0));
- pseudo_client_op(OP_CREAT, 0, -1, -1, path, &buf);
+ if (pseudo_client_path_matches_fd(path, fd, 0))
+ pseudo_client_op(OP_CREAT, 0, -1, -1, path, &buf);
+ pseudo_client_path_unlock(path_lock_fd);
}
pseudo_client_op(OP_OPEN, pseudo_access_fopen(mode), fd, -1, path, &buf);
} else {
diff --git a/pseudo_client.c b/pseudo_client.c
index ffd3450..a3d461e 100644
--- a/pseudo_client.c
+++ b/pseudo_client.c
@@ -1225,6 +1225,31 @@ pseudo_client_path_unlock(int fd) {
errno = save_errno;
}
+/* Operations that can block, such as open() on a FIFO, can't hold the path
+ * lock across the real call. Instead they take the lock afterwards and
+ * check that the path still names the file they opened before recording
+ * it. With the lock held, no other client is part way through changing the
+ * path, so if it now names a different file, that client has already
+ * recorded the change, and recording ours would be wrong.
+ */
+int
+pseudo_client_path_matches_fd(const char *path, int fd, int nofollow) {
+ PSEUDO_STATBUF pbuf, fbuf;
+ int save_errno = errno;
+ int rc;
+
+ if (!path || fd < 0 || !pseudo_real_fstat || !pseudo_real_lstat || !pseudo_real_stat)
+ return 1;
+ rc = pseudo_real_fstat(fd, &fbuf) == 0 &&
+ (nofollow ? pseudo_real_lstat(path, &pbuf) : pseudo_real_stat(path, &pbuf)) == 0 &&
+ pbuf.st_dev == fbuf.st_dev && pbuf.st_ino == fbuf.st_ino;
+ if (!rc)
+ pseudo_debug(PDBGF_FILE, "'%s' no longer names fd %d, not recording it.\n",
+ path, fd);
+ errno = save_errno;
+ return rc;
+}
+
static int
client_connect(void) {
/* we have a server pid, is it responsive? */
diff --git a/pseudo_client.h b/pseudo_client.h
index 3958f6d..716c441 100644
--- a/pseudo_client.h
+++ b/pseudo_client.h
@@ -29,6 +29,7 @@ extern int pseudo_client_shutdown(int);
extern int pseudo_fd(int fd, int how);
extern int pseudo_client_path_lock(int shared);
extern void pseudo_client_path_unlock(int fd);
+extern int pseudo_client_path_matches_fd(const char *path, int fd, int nofollow);
#define MOVE_FD 0
#define COPY_FD 1
#define PSEUDO_MIN_FD 20
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 04/34] test-parallel-ops: Add a parallel create/rename/link/chmod test
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (2 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 03/34] ports: check created files under the path lock before recording them Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 05/34] ports: Remove PSEUDO_NO_REAL_AT_FUNCTIONS Mark Hatle
` (29 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Four workers repeatedly create, rename onto, chmod, chown, symlink,
hard link and remove the same names in one directory. The test fails
if any of the operations fail, or if the server logged an inode or path
mismatch while it ran.
Without the path lock in the create, metadata and open wrappers, the
server reports mismatches and occasionally aborts a client.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
test/test-parallel-ops.sh | 53 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
create mode 100755 test/test-parallel-ops.sh
diff --git a/test/test-parallel-ops.sh b/test/test-parallel-ops.sh
new file mode 100755
index 0000000..2fccd30
--- /dev/null
+++ b/test/test-parallel-ops.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Several clients create, rename, link, chmod and remove the same names in
+# one directory at once. pseudo must not let one client's operation land in
+# the middle of another client's change to the same name, or the server
+# finds the database out of step with the filesystem. It then logs an
+# "inode mismatch" or "path mismatch" error, and may abort a client.
+#
+# The temporary tree has to live below $PWD so that pseudo tracks it.
+
+tmpdir="$(mktemp -d -p "$PWD" parallel_ops_XXXXXX)"
+trap "rm -rf '$tmpdir'" EXIT TERM INT
+
+log="${PSEUDO_LOCALSTATEDIR:-$PWD/var/pseudo}/pseudo.log"
+mismatches() {
+ grep -c "mismatch" "$log" 2>/dev/null || true
+}
+before=$(mismatches)
+
+cd "$tmpdir" || exit 1
+: > target
+
+worker() {
+ for i in $(seq 50); do
+ echo "$1 $i" > "tmp.$1" || echo "FAILED: create tmp.$1" >> failed
+ mv -f "tmp.$1" target || echo "FAILED: mv tmp.$1 target" >> failed
+ chmod 640 target 2>/dev/null
+ chown 0:0 target 2>/dev/null
+ ln -sf target lnk || echo "FAILED: ln -sf target lnk" >> failed
+ mkdir dir 2>/dev/null; rmdir dir 2>/dev/null
+ mkfifo "f.$1" && mv -f "f.$1" fifo || echo "FAILED: mkfifo/mv f.$1" >> failed
+ ln -f target "hard.$1" 2>/dev/null && rm -f "hard.$1"
+ done
+}
+
+for w in 1 2 3 4; do
+ worker $w &
+done
+wait
+
+rc=0
+if [ -e failed ]; then
+ sort failed | uniq -c
+ rc=1
+fi
+after=$(mismatches)
+if [ "${after:-0}" -ne "${before:-0}" ]; then
+ echo "FAILED: $((after - before)) mismatch error(s) in $log"
+ rc=1
+fi
+exit $rc
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 05/34] ports: Remove PSEUDO_NO_REAL_AT_FUNCTIONS
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (3 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 04/34] test-parallel-ops: Add a parallel create/rename/link/chmod test Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 06/34] ports/unix: Remove guts overridden by the Linux port Mark Hatle
` (28 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
PSEUDO_NO_REAL_AT_FUNCTIONS was defined, along with substitute AT_FDCWD
and AT_SYMLINK_* values, when <fcntl.h> did not provide AT_FDCWD. The
guts then used non-*at() fallbacks, or failed with ENOSYS when given a
directory descriptor.
Linux always provides the *at() functions, and pseudo.h includes
<fcntl.h>, so the macro is never defined and none of the fallbacks are
ever built. Remove them, keeping the *at() code, and make a missing
AT_FDCWD a build error instead.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/linux/guts/__fxstatat.c | 12 ------------
ports/linux/guts/__fxstatat64.c | 14 --------------
ports/linux/guts/__xmknodat.c | 17 -----------------
ports/linux/guts/openat.c | 35 -----------------------------------
ports/linux/openat2/guts/openat2.c | 28 ++--------------------------
ports/unix/guts/faccessat2.c | 12 ------------
ports/unix/guts/fchmodat.c | 18 ------------------
ports/unix/guts/fchownat.c | 12 ------------
ports/unix/guts/linkat.c | 11 -----------
ports/unix/guts/mkdirat.c | 17 -----------------
ports/unix/guts/mkfifoat.c | 22 ----------------------
ports/unix/guts/mknodat.c | 17 -----------------
ports/unix/guts/readlinkat.c | 8 --------
ports/unix/guts/renameat.c | 12 ------------
ports/unix/guts/symlinkat.c | 12 ------------
ports/unix/guts/unlinkat.c | 25 -------------------------
pseudo.h | 19 ++++++-------------
17 files changed, 8 insertions(+), 283 deletions(-)
diff --git a/ports/linux/guts/__fxstatat.c b/ports/linux/guts/__fxstatat.c
index 23dfa73..99198f7 100644
--- a/ports/linux/guts/__fxstatat.c
+++ b/ports/linux/guts/__fxstatat.c
@@ -11,19 +11,7 @@
struct stat64 buf64;
/* populate buffer with complete data */
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- if (flags & AT_SYMLINK_NOFOLLOW) {
- rc = real___lxstat(ver, path, buf);
- } else {
- rc = real___xstat(ver, path, buf);
- }
-#else
real___fxstatat(ver, dirfd, path, buf, flags);
-#endif
/* obtain fake data */
rc = wrap___fxstatat64(ver, dirfd, path, &buf64, flags);
/* overwrite */
diff --git a/ports/linux/guts/__fxstatat64.c b/ports/linux/guts/__fxstatat64.c
index ecf2952..6f58aa2 100644
--- a/ports/linux/guts/__fxstatat64.c
+++ b/ports/linux/guts/__fxstatat64.c
@@ -11,27 +11,13 @@
pseudo_msg_t *msg;
int save_errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
-#endif
if (flags & AT_SYMLINK_NOFOLLOW) {
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = real___lxstat64(ver, path, buf);
-#else
rc = real___fxstatat64(ver, dirfd, path, buf, flags);
-#endif
if (rc == -1) {
return rc;
}
} else {
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = real___xstat64(ver, path, buf);
-#else
rc = real___fxstatat64(ver, dirfd, path, buf, flags);
-#endif
if (rc == -1) {
return rc;
}
diff --git a/ports/linux/guts/__xmknodat.c b/ports/linux/guts/__xmknodat.c
index 8c34427..84d535b 100644
--- a/ports/linux/guts/__xmknodat.c
+++ b/ports/linux/guts/__xmknodat.c
@@ -23,15 +23,7 @@
/* we don't use underlying call, so _ver is irrelevant to us */
(void) ver;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- rc = real___xstat64(_STAT_VER, path, &buf);
-#else
rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
-#endif
if (rc != -1) {
/* if we can stat the file, you can't mknod it */
errno = EEXIST;
@@ -41,13 +33,8 @@
errno = EINVAL;
return -1;
}
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = real_open(path, O_CREAT | O_WRONLY | O_EXCL,
- PSEUDO_FS_MODE(mode, 0));
-#else
rc = real_openat(dirfd, path, O_CREAT | O_WRONLY | O_EXCL,
PSEUDO_FS_MODE(mode, 0));
-#endif
if (rc == -1) {
return -1;
}
@@ -73,11 +60,7 @@
}
if (rc == -1) {
int save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- real_unlink(path);
-#else
real_unlinkat(dirfd, path, AT_SYMLINK_NOFOLLOW);
-#endif
errno = save_errno;
}
diff --git a/ports/linux/guts/openat.c b/ports/linux/guts/openat.c
index 4fd83dd..f5469e6 100644
--- a/ports/linux/guts/openat.c
+++ b/ports/linux/guts/openat.c
@@ -17,13 +17,6 @@
/* mask out mode bits appropriately */
mode = mode & ~pseudo_umask;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
-#endif
-
#ifdef PSEUDO_FORCE_ASYNC
/* Yes, I'm aware that every Linux system I've seen has
* DSYNC and RSYNC being the same value as SYNC.
@@ -54,15 +47,7 @@
/* note "else" in #ifdef O_TMPFILE above */
if (flags & O_CREAT) {
save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (flags & O_NOFOLLOW) {
- rc = real___lxstat64(_STAT_VER, path, &buf);
- } else {
- rc = real___xstat64(_STAT_VER, path, &buf);
- }
-#else
rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
-#endif
existed = (rc != -1);
if (!existed)
pseudo_debug(PDBGF_FILE, "openat_creat: %s -> 0%o\n", path, mode);
@@ -75,15 +60,7 @@
*/
if (!(flags & O_NONBLOCK) && ((flags & (O_WRONLY | O_RDONLY | O_RDWR)) != O_RDWR)) {
save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (flags & O_NOFOLLOW) {
- rc = real___lxstat64(_STAT_VER, path, &buf);
- } else {
- rc = real___xstat64(_STAT_VER, path, &buf);
- }
-#else
rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
-#endif
if (rc != -1 && S_ISFIFO(buf.st_mode)) {
overly_magic_nonblocking = 1;
}
@@ -99,11 +76,7 @@
* is if a file is going to be created, in which case it's
* not a directory.
*/
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = real_open(path, flags, PSEUDO_FS_MODE(mode, 0));
-#else
rc = real_openat(dirfd, path, flags, PSEUDO_FS_MODE(mode, 0));
-#endif
if (overly_magic_nonblocking) {
save_errno = errno;
sigprocmask(SIG_SETMASK, &local_saved_sigmask, NULL);
@@ -132,15 +105,7 @@
return rc;
}
#endif
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (flags & O_NOFOLLOW) {
- stat_rc = real___lxstat64(_STAT_VER, path, &buf);
- } else {
- stat_rc = real___xstat64(_STAT_VER, path, &buf);
- }
-#else
stat_rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
-#endif
pseudo_debug(PDBGF_FILE, "openat(path %s), flags %o, stat rc %d, stat mode %o\n",
path, flags, stat_rc, buf.st_mode);
diff --git a/ports/linux/openat2/guts/openat2.c b/ports/linux/openat2/guts/openat2.c
index 06fcec8..5171647 100644
--- a/ports/linux/openat2/guts/openat2.c
+++ b/ports/linux/openat2/guts/openat2.c
@@ -44,7 +44,7 @@
/* mask out mode bits appropriately */
my_how.mode = my_how.mode & ~pseudo_umask;
-#if defined(PSEUDO_NO_REAL_AT_FUNCTIONS) || ! defined(SYS_openat2)
+#ifndef SYS_openat2
if (dirfd != AT_FDCWD) {
errno = ENOSYS;
return -1;
@@ -81,15 +81,7 @@
/* note "else" in #ifdef O_TMPFILE above */
if (my_how.flags & O_CREAT) {
save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (my_how.flags & O_NOFOLLOW) {
- rc = real___lxstat64(_STAT_VER, pseudo_path, &buf);
- } else {
- rc = real___xstat64(_STAT_VER, pseudo_path, &buf);
- }
-#else
rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
-#endif
existed = (rc != -1);
if (!existed)
pseudo_debug(PDBGF_FILE, "openat2_creat: %s -> 0%lld\n", pseudo_path, my_how.mode);
@@ -102,15 +94,7 @@
*/
if (!(my_how.flags & O_NONBLOCK) && ((my_how.flags & (O_WRONLY | O_RDONLY | O_RDWR)) != O_RDWR)) {
save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (my_how.flags & O_NOFOLLOW) {
- rc = real___lxstat64(_STAT_VER, pseudo_path, &buf);
- } else {
- rc = real___xstat64(_STAT_VER, pseudo_path, &buf);
- }
-#else
rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
-#endif
if (rc != -1 && S_ISFIFO(buf.st_mode)) {
overly_magic_nonblocking = 1;
}
@@ -126,7 +110,7 @@
* is if a file is going to be created, in which case it's
* not a directory.
*/
-#if defined(PSEUDO_NO_REAL_AT_FUNCTIONS) || ! defined(SYS_openat2)
+#ifndef SYS_openat2
pseudo_debug(PDBGF_SYSCALL, "openat2, calling open.\n");
rc = real_open(path, my_how.flags, PSEUDO_FS_MODE(my_how.mode, 0));
#else
@@ -167,15 +151,7 @@
return rc;
}
#endif
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (my_how.flags & O_NOFOLLOW) {
- stat_rc = real___lxstat64(_STAT_VER, pseudo_path, &buf);
- } else {
- stat_rc = real___xstat64(_STAT_VER, pseudo_path, &buf);
- }
-#else
stat_rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
-#endif
pseudo_debug(PDBGF_FILE, "openat2(path %s), flags %lld, stat rc %d, stat mode %o\n",
pseudo_path, my_how.flags, stat_rc, buf.st_mode);
diff --git a/ports/unix/guts/faccessat2.c b/ports/unix/guts/faccessat2.c
index 1283cc6..d4a2a42 100644
--- a/ports/unix/guts/faccessat2.c
+++ b/ports/unix/guts/faccessat2.c
@@ -10,19 +10,7 @@
*/
PSEUDO_STATBUF buf;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- if (flags & AT_SYMLINK_NOFOLLOW) {
- rc = base_lstat(path, &buf);
- } else {
- rc = base_stat(path, &buf);
- }
-#else
rc = base_fstatat(dirfd, path, &buf, flags & AT_SYMLINK_NOFOLLOW);
-#endif
if (rc == -1)
return rc;
diff --git a/ports/unix/guts/fchmodat.c b/ports/unix/guts/fchmodat.c
index 5a31151..12dc9e3 100644
--- a/ports/unix/guts/fchmodat.c
+++ b/ports/unix/guts/fchmodat.c
@@ -11,19 +11,7 @@
PSEUDO_STATBUF buf;
int save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- if (flags & AT_SYMLINK_NOFOLLOW) {
- rc = base_lstat(path, &buf);
- } else {
- rc = base_stat(path, &buf);
- }
-#else
rc = base_fstatat(dirfd, path, &buf, flags);
-#endif
if (rc == -1) {
return rc;
}
@@ -50,13 +38,7 @@
#endif
/* user bits added so "root" can always access files. */
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- /* note: if path was a symlink, and AT_SYMLINK_NOFOLLOW was
- * specified, we already bailed previously. */
- real_chmod(path, PSEUDO_FS_MODE(mode, S_ISDIR(buf.st_mode)));
-#else
rc = real_fchmodat(dirfd, path, PSEUDO_FS_MODE(mode, S_ISDIR(buf.st_mode)), flags);
-#endif
/* we otherwise ignore failures from underlying fchmod, because pseudo
* may believe you are permitted to change modes that the filesystem
* doesn't. Note that we also don't need to know whether the
diff --git a/ports/unix/guts/fchownat.c b/ports/unix/guts/fchownat.c
index 76d276a..3af774f 100644
--- a/ports/unix/guts/fchownat.c
+++ b/ports/unix/guts/fchownat.c
@@ -12,19 +12,7 @@
PSEUDO_STATBUF buf;
int save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- if (flags & AT_SYMLINK_NOFOLLOW) {
- rc = base_lstat(path, &buf);
- } else {
- rc = base_stat(path, &buf);
- }
-#else
rc = base_fstatat(dirfd, path, &buf, flags);
-#endif
if (rc == -1) {
return rc;
}
diff --git a/ports/unix/guts/linkat.c b/ports/unix/guts/linkat.c
index f0cedd4..e22d66a 100644
--- a/ports/unix/guts/linkat.c
+++ b/ports/unix/guts/linkat.c
@@ -27,12 +27,6 @@
rflags = (flags & AT_SYMLINK_FOLLOW) ? 0 : AT_SYMLINK_NOFOLLOW;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (olddirfd != AT_FDCWD || newdirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
-#endif
oldpath = oldname;
if (oldpath && *oldpath && pseudo_chroot_len && !strncmp(oldpath, pseudo_chroot, pseudo_chroot_len) &&
oldpath[pseudo_chroot_len] == '/') {
@@ -54,11 +48,6 @@
*/
#ifdef O_TMPFILE
if (!strncmp(oldpath, "/proc/self/fd/", 14) && (flags & AT_SYMLINK_FOLLOW)) {
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- /* only linkat() lets you do this horrible thing */
- errno = ENOSYS;
- return -1;
-#endif
tmpfile_fd = pseudo_atoi_wrapper(oldpath + 14);
// call actual link
rc = real_linkat(AT_FDCWD, oldpath, AT_FDCWD, newpath, AT_SYMLINK_FOLLOW);
diff --git a/ports/unix/guts/mkdirat.c b/ports/unix/guts/mkdirat.c
index f66e99a..4db3826 100644
--- a/ports/unix/guts/mkdirat.c
+++ b/ports/unix/guts/mkdirat.c
@@ -10,34 +10,17 @@
*/
/* mask out mode bits appropriately */
mode = mode & ~pseudo_umask;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
-
- rc = real_mkdir(path, PSEUDO_FS_MODE(mode, 1));
-#else
rc = real_mkdirat(dirfd, path, PSEUDO_FS_MODE(mode, 1));
-#endif
if (rc != -1) {
PSEUDO_STATBUF buf;
int stat_rc;
int save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- stat_rc = base_lstat(path, &buf);
-#else
stat_rc = base_fstatat(dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
-#endif
if (stat_rc != -1) {
buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, mode);
pseudo_client_op(OP_MKDIR, 0, -1, dirfd, path, &buf);
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- real_fchmod(path, PSEUDO_FS_MODE(mode, 1));
-#else
real_fchmodat(dirfd, path, PSEUDO_FS_MODE(mode, 1), 0);
-#endif
} else {
pseudo_debug(PDBGF_OP, "mkdir of %s succeeded, but stat failed: %s\n",
path, strerror(errno));
diff --git a/ports/unix/guts/mkfifoat.c b/ports/unix/guts/mkfifoat.c
index bc547eb..c068841 100644
--- a/ports/unix/guts/mkfifoat.c
+++ b/ports/unix/guts/mkfifoat.c
@@ -16,29 +16,12 @@
/* mask out mode bits appropriately */
mode = mode & ~pseudo_umask;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- rc = base_stat(path, &buf);
-#else
rc = base_fstatat(dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
-#endif
if (rc != -1) {
/* if we can stat the file, you can't mkfifo it */
errno = EEXIST;
return -1;
}
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = real_mkfifo(path, PSEUDO_FS_MODE(mode, 0));
- if (rc == -1) {
- return -1;
- }
- save_errno = errno;
- rc = base_stat(path, &buf);
- real_chmod(path, PSEUDO_FS_MODE(mode, 0));
-#else
rc = real_mkfifoat(dirfd, path, PSEUDO_FS_MODE(mode, 0));
if (rc == -1) {
return -1;
@@ -46,7 +29,6 @@
save_errno = errno;
rc = base_fstatat(dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
real_fchmodat(dirfd, path, PSEUDO_FS_MODE(mode, 0), 0);
-#endif
/* if the stat failed, we are going to give up and nuke
* any file we may have created, and hope for the best.
*/
@@ -67,11 +49,7 @@
}
if (rc == -1) {
save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- real_unlink(path);
-#else
real_unlinkat(dirfd, path, AT_SYMLINK_NOFOLLOW);
-#endif
errno = save_errno;
}
diff --git a/ports/unix/guts/mknodat.c b/ports/unix/guts/mknodat.c
index 9e86c93..3e8b207 100644
--- a/ports/unix/guts/mknodat.c
+++ b/ports/unix/guts/mknodat.c
@@ -21,27 +21,14 @@
pseudo_debug(PDBGF_FILE, "mknodat creating '%s', mode 0%o\n",
path ? path : "<no name>", (int) mode);
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- rc = base_stat(path, &buf);
-#else
rc = base_fstatat(dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
-#endif
if (rc != -1) {
/* if we can stat the file, you can't mknod it */
errno = EEXIST;
return -1;
}
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = real_open(path, O_CREAT | O_WRONLY | O_EXCL,
- PSEUDO_FS_MODE(mode, 0));
-#else
rc = real_openat(dirfd, path, O_CREAT | O_WRONLY | O_EXCL,
PSEUDO_FS_MODE(mode, 0));
-#endif
if (rc == -1) {
return -1;
}
@@ -68,11 +55,7 @@
}
if (rc == -1) {
save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- real_unlink(path);
-#else
real_unlinkat(dirfd, path, AT_SYMLINK_NOFOLLOW);
-#endif
errno = save_errno;
}
diff --git a/ports/unix/guts/readlinkat.c b/ports/unix/guts/readlinkat.c
index 86832da..2d1bce0 100644
--- a/ports/unix/guts/readlinkat.c
+++ b/ports/unix/guts/readlinkat.c
@@ -8,15 +8,7 @@
* wrap_readlinkat(int dirfd, const char *path, char *buf, size_t bufsiz) {
* ssize_t rc = -1;
*/
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- rc = real_readlink(path, buf, bufsiz);
-#else
rc = real_readlinkat(dirfd, path, buf, bufsiz);
-#endif
if (rc > 0) {
rc = pseudo_dechroot(buf, rc);
diff --git a/ports/unix/guts/renameat.c b/ports/unix/guts/renameat.c
index 69130c6..98391dd 100644
--- a/ports/unix/guts/renameat.c
+++ b/ports/unix/guts/renameat.c
@@ -20,13 +20,6 @@
olddirfd, oldpath ? oldpath : "<nil>",
newdirfd, newpath ? newpath : "<nil>");
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (olddirfd != AT_FDCWD || newdirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
-#endif
-
if (!oldpath || !newpath) {
errno = EFAULT;
return -1;
@@ -34,13 +27,8 @@
save_errno = errno;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- newrc = base_lstat(newpath, &newbuf);
- oldrc = base_lstat(oldpath, &oldbuf);
-#else
oldrc = base_fstatat(olddirfd, oldpath, &oldbuf, AT_SYMLINK_NOFOLLOW);
newrc = base_fstatat(newdirfd, newpath, &newbuf, AT_SYMLINK_NOFOLLOW);
-#endif
/* nothing to do for a "rename" of a link to itself */
if (newrc != -1 && oldrc != -1 &&
diff --git a/ports/unix/guts/symlinkat.c b/ports/unix/guts/symlinkat.c
index 5673912..0efe4d1 100644
--- a/ports/unix/guts/symlinkat.c
+++ b/ports/unix/guts/symlinkat.c
@@ -17,25 +17,13 @@
snprintf(roldname, len, "%s%s", pseudo_chroot, oldname);
}
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- rc = real_symlink(roldname ? roldname : oldname, newpath);
-#else
rc = real_symlinkat(roldname ? roldname : oldname, dirfd, newpath);
-#endif
if (rc == -1) {
free(roldname);
return rc;
}
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = base_lstat(newpath, &buf);
-#else
rc = base_fstatat(dirfd, newpath, &buf, AT_SYMLINK_NOFOLLOW);
-#endif
if (rc == -1) {
int save_errno = errno;
pseudo_warning("symlinkat: couldn't stat '%s' even though symlink creation succeeded (%s).\n",
diff --git a/ports/unix/guts/unlinkat.c b/ports/unix/guts/unlinkat.c
index 9889b2e..d0f795b 100644
--- a/ports/unix/guts/unlinkat.c
+++ b/ports/unix/guts/unlinkat.c
@@ -13,39 +13,14 @@
PSEUDO_STATBUF buf;
int old_db_entry = 0;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- if (dirfd != AT_FDCWD) {
- errno = ENOSYS;
- return -1;
- }
- if (rflags) {
- /* the only supported flag is AT_REMOVEDIR. We'd never call
- * with that flag unless the real AT functions exist, so
- * something must have gone horribly wrong....
- */
- pseudo_info("wrap_unlinkat called with flags (0x%x), path '%s'\n",
- rflags, path ? path : "<nil>");
- errno = ENOSYS;
- return -1;
- }
-#endif
-
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = base_lstat(path, &buf);
-#else
rc = base_fstatat(dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
-#endif
if (rc == -1) {
return rc;
}
msg = pseudo_client_op(OP_MAY_UNLINK, 0, -1, dirfd, path, &buf);
if (msg && msg->result == RESULT_SUCCEED)
old_db_entry = 1;
-#ifdef PSEUDO_NO_REAL_AT_FUNCTIONS
- rc = real_unlink(path);
-#else
rc = real_unlinkat(dirfd, path, rflags);
-#endif
if (old_db_entry) {
if (rc == -1) {
save_errno = errno;
diff --git a/pseudo.h b/pseudo.h
index 6d50b77..53bf13f 100644
--- a/pseudo.h
+++ b/pseudo.h
@@ -135,23 +135,16 @@ extern char *pseudo_version;
#define PSEUDO_SEVERITY_DEFAULT "info,warn,error"
-/* some systems might not have *at(). We like to define operations in
- * terms of each other, and for instance, open(...) is the same as
- * openat(AT_FDCWD, ...). If no AT_FDCWD is provided, any value that can't
- * be a valid file descriptor will do. Using -2 because -1 could be
- * mistaken for a failed syscall return. AT_SYMLINK_NOFOLLOW has to be
- * non-zero; AT_SYMLINK_FOLLOW has to be non-zero and different. Finally,
- * if this happened, we set our own flag we can use to indicate that dummy
- * implementations of the _at functions are needed.
+/* pseudo defines operations in terms of each other; for instance,
+ * open(...) is the same as openat(AT_FDCWD, ...). This requires the *at()
+ * functions and AT_FDCWD, AT_SYMLINK_NOFOLLOW and AT_SYMLINK_FOLLOW from
+ * <fcntl.h>.
*/
#ifndef AT_FDCWD
-#define AT_FDCWD -2
-#define AT_SYMLINK_NOFOLLOW 1
-#define AT_SYMLINK_FOLLOW 2
-#define PSEUDO_NO_REAL_AT_FUNCTIONS
+#error "pseudo requires *at() function support (AT_FDCWD, AT_SYMLINK_NOFOLLOW, AT_SYMLINK_FOLLOW)"
#endif
-/* Likewise, someone might not have O_LARGEFILE (the flag equivalent to
+/* Someone might not have O_LARGEFILE (the flag equivalent to
* using open64()). Since open64() is the same as O_LARGEFILE in flags,
* we implement it that way... If the system has no O_LARGEFILE, we'll
* just call open() with nothing special.
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 06/34] ports/unix: Remove guts overridden by the Linux port
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (4 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 05/34] ports: Remove PSEUDO_NO_REAL_AT_FUNCTIONS Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 07/34] ports: Let the port define how pseudo stats files Mark Hatle
` (27 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
lchown, lutimes, mknod, mknodat, tmpnam and utimes are wrapped by both
ports/unix and ports/linux. makewrappers always uses the Linux
versions, reporting "Info: <name> from linux overriding unix", so the
ports/unix guts and wrapfuncs.in entries are never used now that Linux
is the only port.
They were removed as a single commit, so if necessary they can be
restored via a git revert if a future port requires them.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
README.security | 2 +-
ports/unix/guts/lchown.c | 15 -----------
ports/unix/guts/lutimes.c | 16 ------------
ports/unix/guts/mknod.c | 15 -----------
ports/unix/guts/mknodat.c | 64 -----------------------------------------------
ports/unix/guts/tmpnam.c | 20 ---------------
ports/unix/guts/utimes.c | 15 -----------
ports/unix/wrapfuncs.in | 6 -----
8 files changed, 1 insertion(+), 152 deletions(-)
delete mode 100644 ports/unix/guts/lchown.c
delete mode 100644 ports/unix/guts/lutimes.c
delete mode 100644 ports/unix/guts/mknod.c
delete mode 100644 ports/unix/guts/mknodat.c
delete mode 100644 ports/unix/guts/tmpnam.c
delete mode 100644 ports/unix/guts/utimes.c
diff --git a/README.security b/README.security
index f4378c3..8817450 100644
--- a/README.security
+++ b/README.security
@@ -92,7 +92,7 @@ that protection in several ways:
* Several wrappers check a path and then act on it by path in a second
step. For example, fchmodat (ports/unix/guts/fchmodat.c) stats the
path and later calls the real chmod on the same path, and mknodat
- (ports/unix/guts/mknodat.c) removes the path again by name if the
+ (ports/linux/guts/__xmknodat.c) removes the path again by name if the
database update fails.
If anything else can change the tree while pseudo is working on it, an
diff --git a/ports/unix/guts/lchown.c b/ports/unix/guts/lchown.c
deleted file mode 100644
index 3a14e2f..0000000
--- a/ports/unix/guts/lchown.c
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright (c) 2008,2011 Wind River Systems; see
- * guts/COPYRIGHT for information.
- *
- * SPDX-License-Identifier: LGPL-2.1-only
- *
- * int lchown(const char *path, uid_t owner, gid_t group)
- * int rc = -1;
- */
-
- rc = wrap_chown(path, owner, group);
-
-/* return rc;
- * }
- */
diff --git a/ports/unix/guts/lutimes.c b/ports/unix/guts/lutimes.c
deleted file mode 100644
index 07cab66..0000000
--- a/ports/unix/guts/lutimes.c
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Copyright (c) 2010 Wind River Systems; see
- * guts/COPYRIGHT for information.
- *
- * SPDX-License-Identifier: LGPL-2.1-only
- *
- * static int
- * wrap_lutimes(const char *path, const struct timeval *tv) {
- * int rc = -1;
- */
-
- rc = real_lutimes(path, tv);
-
-/* return rc;
- * }
- */
diff --git a/ports/unix/guts/mknod.c b/ports/unix/guts/mknod.c
deleted file mode 100644
index aff3a0b..0000000
--- a/ports/unix/guts/mknod.c
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright (c) 2011,2014 Wind River Systems; see
- * guts/COPYRIGHT for information.
- *
- * SPDX-License-Identifier: LGPL-2.1-only
- *
- * int mknod(const char *path, mode_t mode, dev_t dev)
- * int rc = -1;
- */
-
- rc = wrap_mknodat(AT_FDCWD, path, mode, dev);
-
-/* return rc;
- * }
- */
diff --git a/ports/unix/guts/mknodat.c b/ports/unix/guts/mknodat.c
deleted file mode 100644
index 3e8b207..0000000
--- a/ports/unix/guts/mknodat.c
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (c) 2011 Wind River Systems; see
- * guts/COPYRIGHT for information.
- *
- * SPDX-License-Identifier: LGPL-2.1-only
- *
- * int mknodat(int dirfd, const char *path, mode_t mode, dev_t dev)
- * int rc = -1;
- */
-
- pseudo_msg_t *msg;
- PSEUDO_STATBUF buf;
- int save_errno = errno;
-
- /* mask out mode bits appropriately */
- mode = mode & ~pseudo_umask;
- /* if you don't specify a type, assume regular file */
- if (!(mode & S_IFMT)) {
- mode |= S_IFREG;
- }
- pseudo_debug(PDBGF_FILE, "mknodat creating '%s', mode 0%o\n",
- path ? path : "<no name>", (int) mode);
-
- rc = base_fstatat(dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
- if (rc != -1) {
- /* if we can stat the file, you can't mknod it */
- errno = EEXIST;
- return -1;
- }
- rc = real_openat(dirfd, path, O_CREAT | O_WRONLY | O_EXCL,
- PSEUDO_FS_MODE(mode, 0));
- if (rc == -1) {
- return -1;
- }
- real_fchmod(rc, PSEUDO_FS_MODE(mode, 0));
- base_fstat(rc, &buf);
- /* mknod does not really open the file. We don't have
- * to use wrap_close because we've never exposed this file
- * descriptor to the client code.
- */
- real_close(rc);
-
- /* mask in the mode type bits again */
- buf.st_mode = (PSEUDO_DB_MODE(buf.st_mode, mode) & 07777) |
- (mode & ~07777);
- buf.st_rdev = dev;
- msg = pseudo_client_op(OP_MKNOD, 0, -1, dirfd, path, &buf);
- if (msg && msg->result != RESULT_SUCCEED) {
- errno = EPERM;
- rc = -1;
- } else {
- /* just pretend we worked */
- errno = save_errno;
- rc = 0;
- }
- if (rc == -1) {
- save_errno = errno;
- real_unlinkat(dirfd, path, AT_SYMLINK_NOFOLLOW);
- errno = save_errno;
- }
-
-/* return rc;
- * }
- */
diff --git a/ports/unix/guts/tmpnam.c b/ports/unix/guts/tmpnam.c
deleted file mode 100644
index 60b66e0..0000000
--- a/ports/unix/guts/tmpnam.c
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (c) 2010 Wind River Systems; see
- * guts/COPYRIGHT for information.
- *
- * SPDX-License-Identifier: LGPL-2.1-only
- *
- * static char *
- * wrap_tmpnam(char *s) {
- * char * rc = NULL;
- */
-
- /* let gcc know we're ignoring this */
- (void) s;
- pseudo_warning("tmpnam() is so ludicrously insecure as to defy implementation.");
- errno = ENOMEM;
- rc = NULL;
-
-/* return rc;
- * }
- */
diff --git a/ports/unix/guts/utimes.c b/ports/unix/guts/utimes.c
deleted file mode 100644
index ec49f02..0000000
--- a/ports/unix/guts/utimes.c
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright (c) 2010 Wind River Systems; see
- * guts/COPYRIGHT for information.
- *
- * SPDX-License-Identifier: LGPL-2.1-only
- *
- * static int
- * wrap_utimes(const char *path, const struct timeval *times) {
- * int rc = -1;
- */
- rc = real_utimes(path, times);
-
-/* return rc;
- * }
- */
diff --git a/ports/unix/wrapfuncs.in b/ports/unix/wrapfuncs.in
index 5ffee83..95fa89c 100644
--- a/ports/unix/wrapfuncs.in
+++ b/ports/unix/wrapfuncs.in
@@ -4,7 +4,6 @@ char *getwd(char *buf);
int close(int fd); /* noignore_path=1 */
int fchmod(int fd, mode_t mode);
int fchown(int fd, uid_t owner, gid_t group);
-int lchown(const char *path, uid_t owner, gid_t group); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=shared */
int dup2(int oldfd, int newfd); /* noignore_path=1 */
int dup(int fd); /* noignore_path=1 */
int chdir(const char *path); /* noignore_path=1 */
@@ -18,7 +17,6 @@ FTSENT *fts_children(FTS *ftsp, int options); /* noignore_path=1, path_lock=shar
int ftw(const char *path, int (*fn)(const char *, const struct stat *, int), int nopenfd);
int nftw(const char *path, int (*fn)(const char *, const struct stat *, int, struct FTW *), int nopenfd, int flag);
int glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *pglob);
-int lutimes(const char *path, const struct timeval *tv); /* flags=AT_SYMLINK_NOFOLLOW */
char *mkdtemp(char *template); /* path_lock=1 */
char *mktemp(char *template);
long pathconf(const char *path, int name);
@@ -27,10 +25,8 @@ int remove(const char *path); /* flags=AT_SYMLINK_NOFOLLOW */
DIR *opendir(const char *path); /* noignore_path=1 */
int closedir(DIR *dirp); /* noignore_path=1 */
char *tempnam(const char *template, const char *pfx);
-char *tmpnam(char *s);
int truncate(const char *path, off_t length);
int utime(const char *path, const struct utimbuf *buf);
-int utimes(const char *path, const struct timeval *times);
# needed because libc stdio does horrible things with inline asm syscalls
FILE *fopen(const char *path, const char *mode); /* noignore_path=1 */
int fclose(FILE *fp); /* noignore_path=1 */
@@ -47,8 +43,6 @@ int mkdir(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_loc
int mkdirat(int dirfd, const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int mkfifo(const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int mkfifoat(int dirfd, const char *path, mode_t mode); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
-int mknod(const char *path, mode_t mode, dev_t dev); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
-int mknodat(int dirfd, const char *path, mode_t mode, dev_t dev); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int mkstemp(char *template); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int mkostemp(char *template, int oflags); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
int mkstemps(char *template, int suffixlen); /* flags=AT_SYMLINK_NOFOLLOW, path_lock=1 */
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 07/34] ports: Let the port define how pseudo stats files
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (5 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 06/34] ports/unix: Remove guts overridden by the Linux port Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 08/34] patches/yocto-older-glibc-symbols.patch: Update to latest YP version Mark Hatle
` (26 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
stat64(), lstat64() and fstat64() are not POSIX. They come from the
Large File Summit transitional interface, which glibc provides but the
BSDs and macOS don't, and musl is phasing out. The __xstat64() family
called with _STAT_VER is glibc internal, and only kept for binary
compatibility since glibc 2.33.
pseudo_client.h chose between two sets of base_*stat() definitions with
PSEUDO_STATBUF_64, which the Linux port always set, while several guts
called real___xstat64(_STAT_VER, ...) and friends directly, and shared
code used struct stat64.
Move the base_stat(), base_lstat(), base_fstat() and base_fstatat()
definitions into ports/linux/portdefs.h, next to PSEUDO_STATBUF, and
make pseudo_client.h fail with #error if a port doesn't define them.
Use the base_*() macros instead of the direct calls in the guts, except
in the __fxstat64 and __fxstatat64 wrappers, which pass through the
caller's version. Use PSEUDO_STATBUF instead of struct stat64 in shared
code and for local buffers in the guts.
real_stat64() and friends are pseudo_stat64() and friends, which call
real___xstat64(_STAT_VER, ...), so the behaviour is unchanged.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/linux/guts/__xmknodat.c | 6 +++---
ports/linux/guts/fopen64.c | 6 +++---
ports/linux/guts/freopen64.c | 6 +++---
ports/linux/guts/mkostemp64.c | 4 ++--
ports/linux/guts/open_tree.c | 4 ++--
ports/linux/guts/open_tree_attr.c | 4 ++--
ports/linux/guts/openat.c | 10 +++++-----
ports/linux/openat2/guts/openat2.c | 10 +++++-----
ports/linux/portdefs.h | 10 +++++++++-
pseudo.h | 6 ++++--
pseudo_client.c | 6 +++---
pseudo_client.h | 13 +++----------
pseudo_util.c | 4 ++--
13 files changed, 46 insertions(+), 43 deletions(-)
diff --git a/ports/linux/guts/__xmknodat.c b/ports/linux/guts/__xmknodat.c
index 84d535b..21ad641 100644
--- a/ports/linux/guts/__xmknodat.c
+++ b/ports/linux/guts/__xmknodat.c
@@ -9,7 +9,7 @@
* int rc = -1;
*/
pseudo_msg_t *msg;
- struct stat64 buf;
+ PSEUDO_STATBUF buf;
/* mask out mode bits appropriately */
mode = mode & ~pseudo_umask;
@@ -23,7 +23,7 @@
/* we don't use underlying call, so _ver is irrelevant to us */
(void) ver;
- rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
+ rc = base_fstatat(dirfd, path, &buf, AT_SYMLINK_NOFOLLOW);
if (rc != -1) {
/* if we can stat the file, you can't mknod it */
errno = EEXIST;
@@ -39,7 +39,7 @@
return -1;
}
real_fchmod(rc, PSEUDO_FS_MODE(mode, 0));
- real___fxstat64(_STAT_VER, rc, &buf);
+ base_fstat(rc, &buf);
/* mknod does not really open the file. We don't have
* to use wrap_close because we've never exposed this file
* descriptor to the client code.
diff --git a/ports/linux/guts/fopen64.c b/ports/linux/guts/fopen64.c
index a6efd18..6355334 100644
--- a/ports/linux/guts/fopen64.c
+++ b/ports/linux/guts/fopen64.c
@@ -8,10 +8,10 @@
* wrap_fopen64(const char *path, const char *mode) {
* FILE * rc = 0;
*/
- struct stat64 buf;
+ PSEUDO_STATBUF buf;
int save_errno;
- int existed = (real___xstat64(_STAT_VER, path, &buf) != -1);
+ int existed = (base_stat(path, &buf) != -1);
rc = real_fopen64(path, mode);
save_errno = errno;
@@ -20,7 +20,7 @@
int fd = fileno(rc);
pseudo_debug(PDBGF_FILE, "fopen64 '%s': fd %d <FILE %p>\n", path, fd, (void *) rc);
- if (real___fxstat64(_STAT_VER, fd, &buf) != -1) {
+ if (base_fstat(fd, &buf) != -1) {
if (!existed) {
int path_lock_fd = pseudo_client_path_lock(0);
real_fchmod(fd, PSEUDO_FS_MODE(0666 & ~pseudo_umask, 0));
diff --git a/ports/linux/guts/freopen64.c b/ports/linux/guts/freopen64.c
index 6057eda..99ca9cf 100644
--- a/ports/linux/guts/freopen64.c
+++ b/ports/linux/guts/freopen64.c
@@ -8,9 +8,9 @@
* wrap_freopen64(const char *path, const char *mode, FILE *stream) {
* FILE * rc = NULL;
*/
- struct stat64 buf;
+ PSEUDO_STATBUF buf;
int save_errno;
- int existed = (real___xstat64(_STAT_VER, path, &buf) != -1);
+ int existed = (base_stat(path, &buf) != -1);
rc = real_freopen64(path, mode, stream);
save_errno = errno;
@@ -19,7 +19,7 @@
int fd = fileno(rc);
pseudo_debug(PDBGF_FILE, "freopen64 '%s': fd %d\n", path, fd);
- if (real___fxstat64(_STAT_VER, fd, &buf) != -1) {
+ if (base_fstat(fd, &buf) != -1) {
if (!existed) {
int path_lock_fd = pseudo_client_path_lock(0);
real_fchmod(fd, PSEUDO_FS_MODE(0666 & ~pseudo_umask, 0));
diff --git a/ports/linux/guts/mkostemp64.c b/ports/linux/guts/mkostemp64.c
index 502211b..23c30c4 100644
--- a/ports/linux/guts/mkostemp64.c
+++ b/ports/linux/guts/mkostemp64.c
@@ -8,7 +8,7 @@
* wrap_mkstemp64(char *template, int oflags) {
* int rc = -1;
*/
- struct stat64 buf;
+ PSEUDO_STATBUF buf;
int save_errno;
size_t len;
char *tmp_template;
@@ -35,7 +35,7 @@
if (rc != -1) {
save_errno = errno;
- if (real___fxstat64(_STAT_VER, rc, &buf) != -1) {
+ if (base_fstat(rc, &buf) != -1) {
real_fchmod(rc, PSEUDO_FS_MODE(0600, 0));
pseudo_client_op(OP_CREAT, 0, -1, -1, tmp_template, &buf);
pseudo_client_op(OP_OPEN, PSA_READ | PSA_WRITE, rc, -1, tmp_template, &buf);
diff --git a/ports/linux/guts/open_tree.c b/ports/linux/guts/open_tree.c
index f2e39ad..01d3dfc 100644
--- a/ports/linux/guts/open_tree.c
+++ b/ports/linux/guts/open_tree.c
@@ -8,7 +8,7 @@
* int rc = -1;
*/
- struct stat64 buf;
+ PSEUDO_STATBUF buf;
int save_errno;
char *pseudo_path;
@@ -46,7 +46,7 @@
save_errno = errno;
int stat_rc;
- stat_rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (flags & AT_SYMLINK_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+ stat_rc = base_fstatat(dirfd, pseudo_path, &buf, (flags & AT_SYMLINK_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
pseudo_debug(PDBGF_FILE, "open_tree(path %s), flags %o, stat rc %d, stat mode %o\n",
pseudo_path, flags, stat_rc, buf.st_mode);
diff --git a/ports/linux/guts/open_tree_attr.c b/ports/linux/guts/open_tree_attr.c
index 8e00c8c..96f32bb 100644
--- a/ports/linux/guts/open_tree_attr.c
+++ b/ports/linux/guts/open_tree_attr.c
@@ -10,7 +10,7 @@
/* Mostly based on open_tree wrapper */
- struct stat64 buf;
+ PSEUDO_STATBUF buf;
int save_errno;
char *pseudo_path;
@@ -44,7 +44,7 @@
save_errno = errno;
int stat_rc;
- stat_rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (flags & AT_SYMLINK_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+ stat_rc = base_fstatat(dirfd, pseudo_path, &buf, (flags & AT_SYMLINK_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
pseudo_debug(PDBGF_FILE, "open_tree_attr(path %s), flags %o, stat rc %d, stat mode %o\n",
pseudo_path, flags, stat_rc, buf.st_mode);
diff --git a/ports/linux/guts/openat.c b/ports/linux/guts/openat.c
index f5469e6..9d32283 100644
--- a/ports/linux/guts/openat.c
+++ b/ports/linux/guts/openat.c
@@ -8,7 +8,7 @@
* wrap_openat(int dirfd, const char *path, int flags, ...mode_t mode) {
* int rc = -1;
*/
- struct stat64 buf;
+ PSEUDO_STATBUF buf;
int overly_magic_nonblocking = 0;
int existed = 1;
int save_errno;
@@ -47,7 +47,7 @@
/* note "else" in #ifdef O_TMPFILE above */
if (flags & O_CREAT) {
save_errno = errno;
- rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+ rc = base_fstatat(dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
existed = (rc != -1);
if (!existed)
pseudo_debug(PDBGF_FILE, "openat_creat: %s -> 0%o\n", path, mode);
@@ -60,7 +60,7 @@
*/
if (!(flags & O_NONBLOCK) && ((flags & (O_WRONLY | O_RDONLY | O_RDWR)) != O_RDWR)) {
save_errno = errno;
- rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+ rc = base_fstatat(dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
if (rc != -1 && S_ISFIFO(buf.st_mode)) {
overly_magic_nonblocking = 1;
}
@@ -105,7 +105,7 @@
return rc;
}
#endif
- stat_rc = real___fxstatat64(_STAT_VER, dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+ stat_rc = base_fstatat(dirfd, path, &buf, (flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
pseudo_debug(PDBGF_FILE, "openat(path %s), flags %o, stat rc %d, stat mode %o\n",
path, flags, stat_rc, buf.st_mode);
@@ -116,7 +116,7 @@
real_fchmod(rc, PSEUDO_FS_MODE(mode, 0));
// file has no path, but has been created
if (pseudo_client_path_matches_fd(path, rc, flags & O_NOFOLLOW) &&
- real___fxstat64(_STAT_VER, rc, &buf) != -1) {
+ base_fstat(rc, &buf) != -1) {
buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, mode);
pseudo_client_op(OP_CREAT, 0, -1, dirfd, path, &buf);
}
diff --git a/ports/linux/openat2/guts/openat2.c b/ports/linux/openat2/guts/openat2.c
index 5171647..bb4a125 100644
--- a/ports/linux/openat2/guts/openat2.c
+++ b/ports/linux/openat2/guts/openat2.c
@@ -8,7 +8,7 @@
* int openat2(int dirfd, const char *path, const struct open_how *how, size_t size)
* int rc = -1;
*/
- struct stat64 buf;
+ PSEUDO_STATBUF buf;
int overly_magic_nonblocking = 0;
int existed = 1;
int save_errno;
@@ -81,7 +81,7 @@
/* note "else" in #ifdef O_TMPFILE above */
if (my_how.flags & O_CREAT) {
save_errno = errno;
- rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+ rc = base_fstatat(dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
existed = (rc != -1);
if (!existed)
pseudo_debug(PDBGF_FILE, "openat2_creat: %s -> 0%lld\n", pseudo_path, my_how.mode);
@@ -94,7 +94,7 @@
*/
if (!(my_how.flags & O_NONBLOCK) && ((my_how.flags & (O_WRONLY | O_RDONLY | O_RDWR)) != O_RDWR)) {
save_errno = errno;
- rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+ rc = base_fstatat(dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
if (rc != -1 && S_ISFIFO(buf.st_mode)) {
overly_magic_nonblocking = 1;
}
@@ -151,7 +151,7 @@
return rc;
}
#endif
- stat_rc = real___fxstatat64(_STAT_VER, dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
+ stat_rc = base_fstatat(dirfd, pseudo_path, &buf, (my_how.flags & O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0);
pseudo_debug(PDBGF_FILE, "openat2(path %s), flags %lld, stat rc %d, stat mode %o\n",
pseudo_path, my_how.flags, stat_rc, buf.st_mode);
@@ -162,7 +162,7 @@
real_fchmod(rc, PSEUDO_FS_MODE(my_how.mode, 0));
// file has no path, but has been created
if (pseudo_client_path_matches_fd(pseudo_path, rc, my_how.flags & O_NOFOLLOW) &&
- real___fxstat64(_STAT_VER, rc, &buf) != -1) {
+ base_fstat(rc, &buf) != -1) {
buf.st_mode = PSEUDO_DB_MODE(buf.st_mode, my_how.mode);
pseudo_client_op(OP_CREAT, 0, -1, dirfd, pseudo_path, &buf);
}
diff --git a/ports/linux/portdefs.h b/ports/linux/portdefs.h
index ac1d971..3ac9466 100644
--- a/ports/linux/portdefs.h
+++ b/ports/linux/portdefs.h
@@ -4,8 +4,16 @@
*/
#define PRELINK_LIBRARIES "LD_PRELOAD"
#define PRELINK_PATH "LD_LIBRARY_PATH"
-#define PSEUDO_STATBUF_64 1
#define PSEUDO_STATBUF struct stat64
+/* How pseudo stats files itself, bypassing its own wrappers. glibc's
+ * stat64() and friends were inline wrappers around __xstat64() and
+ * friends before glibc 2.33, and pseudo still uses those to work with
+ * binaries built against older glibc.
+ */
+#define base_lstat real_lstat64
+#define base_fstat real_fstat64
+#define base_stat real_stat64
+#define base_fstatat(dirfd, path, buf, flags) real___fxstatat64(_STAT_VER, dirfd, path, buf, flags)
#define PSEUDO_LINKPATH_SEPARATOR " "
/* Linux NEVER follows symlinks for link(2)... except on old kernels
* I don't care about.
diff --git a/pseudo.h b/pseudo.h
index 53bf13f..d83281c 100644
--- a/pseudo.h
+++ b/pseudo.h
@@ -104,8 +104,6 @@ extern ssize_t pseudo_sys_path_max(void);
extern ssize_t pseudo_path_max(void);
#define PSEUDO_PWD_MAX 4096
extern int pseudo_etc_file(const char *filename, char *realname, int flags, const char **search_dirs, int dircount);
-extern void pseudo_stat32_from64(struct stat *, const struct stat64 *);
-extern void pseudo_stat64_from32(struct stat64 *, const struct stat *);
extern long pseudo_strtol_wrapper(const char *, char **, int);
extern long long pseudo_strtoll_wrapper(const char *, char **, int);
@@ -180,3 +178,7 @@ typedef struct {
} pseudo_profile_t;
#endif
#include "pseudo_ports.h"
+
+/* convert between struct stat and the port's PSEUDO_STATBUF */
+extern void pseudo_stat32_from64(struct stat *, const PSEUDO_STATBUF *);
+extern void pseudo_stat64_from32(PSEUDO_STATBUF *, const struct stat *);
diff --git a/pseudo_client.c b/pseudo_client.c
index a3d461e..4c9c2e8 100644
--- a/pseudo_client.c
+++ b/pseudo_client.c
@@ -218,7 +218,7 @@ typedef struct {
static pseudo_msg_t xattrdb_data;
pseudo_msg_t *
-pseudo_xattrdb_save(int fd, const char *path, const struct stat64 *buf) {
+pseudo_xattrdb_save(int fd, const char *path, const PSEUDO_STATBUF *buf) {
int rc = -1;
if (!path && fd < 0)
return NULL;
@@ -249,7 +249,7 @@ pseudo_xattrdb_save(int fd, const char *path, const struct stat64 *buf) {
}
pseudo_msg_t *
-pseudo_xattrdb_load(int fd, const char *path, const struct stat64 *buf) {
+pseudo_xattrdb_load(int fd, const char *path, const PSEUDO_STATBUF *buf) {
int rc = -1, retryrc = -1;
if (!path && fd < 0)
return NULL;
@@ -1845,7 +1845,7 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path
#ifdef PSEUDO_XATTRDB
if (buf) {
- struct stat64 bufcopy = *buf;
+ PSEUDO_STATBUF bufcopy = *buf;
int do_save = 0;
/* maybe use xattr instead */
/* note: if we use xattr, logging won't work reliably
diff --git a/pseudo_client.h b/pseudo_client.h
index 716c441..ef959e6 100644
--- a/pseudo_client.h
+++ b/pseudo_client.h
@@ -10,16 +10,9 @@ extern pseudo_msg_t *pseudo_client_op(pseudo_op_t op, int access, int fd, int di
extern int pseudo_client_ignore_path(const char *path);
extern int pseudo_client_ignore_fd(int fd);
extern void pseudo_client_linked_paths(const char *oldpath, const char *newpath);
-#if PSEUDO_STATBUF_64
-#define base_lstat real_lstat64
-#define base_fstat real_fstat64
-#define base_stat real_stat64
-#define base_fstatat(dirfd, path, buf, flags) real___fxstatat64(_STAT_VER, dirfd, path, buf, flags)
-#else
-#define base_lstat real_lstat
-#define base_fstat real_fstat
-#define base_stat real_stat
-#define base_fstatat(dirfd, path, buf, flags) real___fxstatat(_STAT_VER, dirfd, path, buf, flags)
+/* The port defines how pseudo stats files itself, on PSEUDO_STATBUF */
+#if !defined(base_lstat) || !defined(base_fstat) || !defined(base_stat) || !defined(base_fstatat)
+#error "pseudo: the port must define base_lstat, base_fstat, base_stat and base_fstatat"
#endif
extern void pseudo_antimagic(void);
extern void pseudo_magic(void);
diff --git a/pseudo_util.c b/pseudo_util.c
index 9845bd2..25f8bc0 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -1776,7 +1776,7 @@ pseudo_evlog_logfile(char *defname, int prefer_fd) {
}
void
-pseudo_stat32_from64(struct stat *buf32, const struct stat64 *buf) {
+pseudo_stat32_from64(struct stat *buf32, const PSEUDO_STATBUF *buf) {
buf32->st_dev = buf->st_dev;
buf32->st_ino = buf->st_ino;
buf32->st_mode = buf->st_mode;
@@ -1793,7 +1793,7 @@ pseudo_stat32_from64(struct stat *buf32, const struct stat64 *buf) {
}
void
-pseudo_stat64_from32(struct stat64 *buf64, const struct stat *buf) {
+pseudo_stat64_from32(PSEUDO_STATBUF *buf64, const struct stat *buf) {
buf64->st_dev = buf->st_dev;
buf64->st_ino = buf->st_ino;
buf64->st_mode = buf->st_mode;
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 08/34] patches/yocto-older-glibc-symbols.patch: Update to latest YP version
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (6 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 07/34] ports: Let the port define how pseudo stats files Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 09/34] Remove unused #if 0 debug code Mark Hatle
` (25 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
As of openembedded-core:
commit 994e508b2a0ede8b5cc4fe39444cf25dc9a53faf
Author: Richard Purdie <richard.purdie@linuxfoundation.org>
Date: Tue May 6 10:01:21 2025 +0100
pseudo: Upgrade to version 1.9.1
This brings in:
* nftw, nftw64: add wrapper
* ftw, nftw, ftw64, nftw64: add tests
* Move ftw and ftw64 to calling ntfw and nftw64
* makewrappers: Introduce 'array' support
* pseudo_util.c: Avoid warning when we intentionally discard const
* pseudo_client.c: Fix warning
* yocto-older-glibc-symbols.path: Add as a reference patch
* pseudo/pseudo_client: Add wrapper functions to operate correctly with glibc 2.38 onwards
* configure: Prune PIE flags
* test/test-parallel-rename.sh: Add parallel rename test
* test/test-parallel-symlinks.sh: Add parallel symlink test
* ports/linux/guts: Add .gitignore to ignore generated files
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
patches/yocto-older-glibc-symbols.patch | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/patches/yocto-older-glibc-symbols.patch b/patches/yocto-older-glibc-symbols.patch
index c453b5f..f42b32b 100644
--- a/patches/yocto-older-glibc-symbols.patch
+++ b/patches/yocto-older-glibc-symbols.patch
@@ -28,10 +28,10 @@ diff --git a/Makefile.in b/Makefile.in
@@ -120,7 +120,7 @@ $(PSEUDODB): pseudodb.o $(SHOBJS) $(DBOBJS) pseudo_ipc.o | $(BIN)
libpseudo: $(LIBPSEUDO)
- $(LIBPSEUDO): $(WRAPOBJS) pseudo_client.o pseudo_ipc.o $(SHOBJS) | $(LIB)
+ $(LIBPSEUDO): $(WRAPOBJS) pseudo_client.o pseudo_client_scanf.o pseudo_ipc.o $(SHOBJS) | $(LIB)
- $(CC) $(CFLAGS) $(CFLAGS_PSEUDO) -shared -o $(LIBPSEUDO) \
+ $(CC) $(CFLAGS) -Lprebuilt/$(shell uname -m)-linux/lib/ $(CFLAGS_PSEUDO) -shared -o $(LIBPSEUDO) \
- pseudo_client.o pseudo_ipc.o \
+ pseudo_client.o pseudo_client_scanf.o pseudo_ipc.o \
$(WRAPOBJS) $(SHOBJS) $(LDFLAGS) $(CLIENT_LDFLAGS)
diff --git a/pseudo_wrappers.c b/pseudo_wrappers.c
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 09/34] Remove unused #if 0 debug code
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (7 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 08/34] patches/yocto-older-glibc-symbols.patch: Update to latest YP version Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 10/34] Replace #if 0 strerror() debug messages with a comment Mark Hatle
` (24 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Remove an abort() check for clients named "pseudo" in
pseudo_client_request(), and dump_env() in pseudo_util.c, which have
been disabled with #if 0 and are not referred to.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_client.c | 5 -----
pseudo_util.c | 16 ----------------
2 files changed, 21 deletions(-)
diff --git a/pseudo_client.c b/pseudo_client.c
index 4c9c2e8..bbe0829 100644
--- a/pseudo_client.c
+++ b/pseudo_client.c
@@ -1399,11 +1399,6 @@ pseudo_client_request(pseudo_msg_t *msg, size_t len, const char *path) {
pseudo_msg_t *response = 0;
int tries = 0;
int rc;
- #if 0
- extern char *program_invocation_short_name;
- if (!strcmp(program_invocation_short_name, "pseudo"))
- abort();
- #endif
if (!msg)
return 0;
diff --git a/pseudo_util.c b/pseudo_util.c
index 25f8bc0..78c2e1b 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -106,22 +106,6 @@ int (*pseudo_real_unsetenv)(const char *) = unsetenv;
char * (*pseudo_real_getenv)(const char *) = getenv;
int (*pseudo_real_setenv)(const char *, const char *, int) = setenv;
-#if 0
-static void
-dump_env(char **envp) {
- size_t i = 0;
- for (i = 0; envp[i]; i++) {
- pseudo_debug(PDBGF_ENV, "dump_envp: [%d]%s\n", (int) i, envp[i]);
- }
-
- for (i = 0; pseudo_env[i].key; i++) {
- pseudo_debug(PDBGF_ENV, "dump_envp: {%d}%s=%s\n", (int) i, pseudo_env[i].key, pseudo_env[i].value);
- }
-
- pseudo_debug(PDBGF_ENV, "dump_envp: _in_init %d\n", pseudo_util_initted);
-}
-#endif
-
int
pseudo_has_unload(char * const *envp) {
static const char unload[] = "PSEUDO_UNLOAD";
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 10/34] Replace #if 0 strerror() debug messages with a comment
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (8 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 09/34] Remove unused #if 0 debug code Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 11/34] pseudo_util: Remove stale with_libpseudo() comment Mark Hatle
` (23 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
The wrapper template and popen() kept a disabled debug message using
strerror(), with a comment that strerror() can hang on systems using
locale data for it. Keep the explanation, as a comment by the message
that prints errno as a number, and drop the disabled code.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/unix/pseudo_wrappers.c | 7 +------
templates/wrapfuncs.c | 7 +------
2 files changed, 2 insertions(+), 12 deletions(-)
diff --git a/ports/unix/pseudo_wrappers.c b/ports/unix/pseudo_wrappers.c
index 41398e4..b0faca8 100644
--- a/ports/unix/pseudo_wrappers.c
+++ b/ports/unix/pseudo_wrappers.c
@@ -30,12 +30,7 @@ popen(const char *command, const char *mode) {
save_errno = errno;
pseudo_droplock();
sigprocmask(SIG_SETMASK, &saved, NULL);
-#if 0
-/* This can cause hangs on some recentish systems which use locale
- * stuff for strerror...
- */
- pseudo_debug(PDBGF_WRAPPER, "completed: popen (maybe: %s)\n", strerror(save_errno));
-#endif
+ /* not strerror(), which can hang on systems using locale data for it */
pseudo_debug(PDBGF_WRAPPER, "completed: popen (errno: %d)\n", save_errno);
errno = save_errno;
return rc;
diff --git a/templates/wrapfuncs.c b/templates/wrapfuncs.c
index a57c997..d7fb548 100644
--- a/templates/wrapfuncs.c
+++ b/templates/wrapfuncs.c
@@ -78,12 +78,7 @@ quick_exit:
pseudo_droplock();
sigprocmask(SIG_SETMASK, &saved, NULL);
pseudo_debug(PDBGF_WRAPPER | PDBGF_VERBOSE, "${name} - yielded lock, restored signals\n");
-#if 0
-/* This can cause hangs on some recentish systems which use locale
- * stuff for strerror...
- */
- pseudo_debug(PDBGF_WRAPPER, "wrapper completed: ${name} returns ${rc_format} (errno: %s)\n", ${rc_value}, strerror(save_errno));
-#endif
+ /* not strerror(), which can hang on systems using locale data for it */
pseudo_debug(PDBGF_WRAPPER, "wrapper completed: ${name} returns ${rc_format} (errno: %d)\n", ${rc_value}, save_errno);
errno = save_errno;
PROFILE_DONE;
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 11/34] pseudo_util: Remove stale with_libpseudo() comment
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (9 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 10/34] Replace #if 0 strerror() debug messages with a comment Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 12/34] ports/common/subports: Show the unknown OS name in the error Mark Hatle
` (22 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
pseudo_setupenv() kept libdir_path until the end "because
with_libpseudo might need it". That was only true for the Darwin port,
which passed it to with_libpseudo(); the parameter was removed with the
port.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_util.c | 3 ---
1 file changed, 3 deletions(-)
diff --git a/pseudo_util.c b/pseudo_util.c
index 78c2e1b..351cd09 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -1123,9 +1123,6 @@ pseudo_setupenv() {
}
}
- /* we kept libdir path until now because with_libpseudo might
- * need it
- */
free(libdir_path);
}
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 12/34] ports/common/subports: Show the unknown OS name in the error
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (10 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 11/34] pseudo_util: Remove stale with_libpseudo() comment Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 13/34] ports/linux: Drop redundant PSEUDO_LINK_SYMLINK_BEHAVIOR override Mark Hatle
` (21 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
The error for an unsupported host used %(uname -s) instead of
$(uname -s), so it printed that text instead of the name of the OS.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/common/subports | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ports/common/subports b/ports/common/subports
index d5f9292..486c7a2 100755
--- a/ports/common/subports
+++ b/ports/common/subports
@@ -5,7 +5,7 @@
case $(uname -s) in
Linux) echo "linux";;
-*) echo >&2 "Unknown result from uname -s: %(uname -s). Aborting."
+*) echo >&2 "Unknown result from uname -s: $(uname -s). Aborting."
exit 1
;;
esac
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 13/34] ports/linux: Drop redundant PSEUDO_LINK_SYMLINK_BEHAVIOR override
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (11 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 12/34] ports/common/subports: Show the unknown OS name in the error Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 14/34] pseudo_server: Skip events for clients closed earlier in the batch Mark Hatle
` (20 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
ports/linux/portdefs.h redefined PSEUDO_LINK_SYMLINK_BEHAVIOR to 0,
the default already set in pseudo.h. Remove it, and move its note that
0 means not following symlinks, because linkat() rejects
AT_SYMLINK_NOFOLLOW, into the comment in pseudo.h.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/linux/portdefs.h | 9 ---------
pseudo.h | 3 ++-
2 files changed, 2 insertions(+), 10 deletions(-)
diff --git a/ports/linux/portdefs.h b/ports/linux/portdefs.h
index 3ac9466..97ddcb6 100644
--- a/ports/linux/portdefs.h
+++ b/ports/linux/portdefs.h
@@ -15,15 +15,6 @@
#define base_stat real_stat64
#define base_fstatat(dirfd, path, buf, flags) real___fxstatat64(_STAT_VER, dirfd, path, buf, flags)
#define PSEUDO_LINKPATH_SEPARATOR " "
-/* Linux NEVER follows symlinks for link(2)... except on old kernels
- * I don't care about.
- */
-#undef PSEUDO_LINK_SYMLINK_BEHAVIOR
-/* Note: 0, here, really means AT_SYMLINK_NOFOLLOW, but specifying that
- * causes errors; you have to leave it empty or specify AT_SYMLINK_FOLLOW.
- */
-#define PSEUDO_LINK_SYMLINK_BEHAVIOR 0
-
/* There were symbol changes that can cause the linker to request
* newer versions of glibc, which causes problems occasionally on
* older hosts if pseudo is built against a newer glibc and then
diff --git a/pseudo.h b/pseudo.h
index d83281c..13249bf 100644
--- a/pseudo.h
+++ b/pseudo.h
@@ -155,7 +155,8 @@ extern char *pseudo_version;
* would ever do that? Well, Linux did, and possibly as a result, linkat()
* does by default too; if you are on a host with the historical Unix
* behavior of following symlinks to find the link target, you will want
- * to set this to AT_SYMLINK_FOLLOW.
+ * to set this to AT_SYMLINK_FOLLOW. 0 means not following them; it can't
+ * be AT_SYMLINK_NOFOLLOW, as linkat() rejects that flag.
*/
#define PSEUDO_LINK_SYMLINK_BEHAVIOR 0
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 14/34] pseudo_server: Skip events for clients closed earlier in the batch
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (12 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 13/34] ports/linux: Drop redundant PSEUDO_LINK_SYMLINK_BEHAVIOR override Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 15/34] pseudo_server: Drop an idle client when out of file descriptors Mark Hatle
` (19 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
The epoll loop handles up to EPOLL_MAX_EVENTS events per wakeup. If a
client's slot was already closed earlier in the same batch, its event
was treated as an empty read, and close_client() was called again,
decrementing active_clients a second time. The server starts its idle
shutdown when active_clients reaches 1, so it could exit while clients
were still connected.
Skip events for slots that are no longer in use, as the select() loop
does.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_server.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/pseudo_server.c b/pseudo_server.c
index b983121..fc5c2c1 100644
--- a/pseudo_server.c
+++ b/pseudo_server.c
@@ -728,7 +728,9 @@ static void pseudo_server_loop_epoll(void)
pseudo_debug(PDBGF_SERVER, "Hit max open files.\n");
}
}
- } else {
+ } else if (clients[client_id].fd != -1) {
+ /* the client may have been closed earlier in
+ * this batch of events */
int n = 0;
ioctl(clients[client_id].fd, FIONREAD, &n);
if (n == 0) {
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 15/34] pseudo_server: Drop an idle client when out of file descriptors
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (13 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 14/34] pseudo_server: Skip events for clients closed earlier in the batch Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 16/34] pseudo_server: Always use epoll Mark Hatle
` (18 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
When accept() failed with EMFILE, the epoll loop only logged it. The
listening socket stays readable while the connection is pending, so
epoll_wait() returned immediately again, and the server spun until a
client went away.
Like the select() loop does, drop one client to make room: one that had
no event in the current batch, so it can't have one pending in it
either.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_server.c | 26 ++++++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/pseudo_server.c b/pseudo_server.c
index fc5c2c1..77d1c39 100644
--- a/pseudo_server.c
+++ b/pseudo_server.c
@@ -626,6 +626,7 @@ static void pseudo_server_loop_epoll(void)
struct sigaction eat_usr2 = {
.sa_handler = set_do_list_clients
};
+ int hitmaxfiles = 0;
clients = malloc(16 * sizeof(*clients));
if (!clients) {
@@ -724,8 +725,8 @@ static void pseudo_server_loop_epoll(void)
exit(PSEUDO_EXIT_EPOLL_CTL);
}
} else if (errno == EMFILE) {
- // select() loop would drop a client here, we do nothing (for now)
- pseudo_debug(PDBGF_SERVER, "Hit max open files.\n");
+ hitmaxfiles = 1;
+ pseudo_debug(PDBGF_SERVER, "Hit max open files, dropping a client.\n");
}
}
} else if (clients[client_id].fd != -1) {
@@ -742,6 +743,27 @@ static void pseudo_server_loop_epoll(void)
if (die_forcefully)
break;
}
+ /* The listening socket stays readable while accept()
+ * fails, so make room by dropping one client that had
+ * no event in this batch, and so can't have one pending
+ * in it either.
+ */
+ if (hitmaxfiles && !die_forcefully) {
+ hitmaxfiles = 0;
+ for (i = 1; i <= highest_client; ++i) {
+ int j;
+ if (clients[i].fd == -1)
+ continue;
+ for (j = 0; j < rc; ++j) {
+ if ((int) events[j].data.u64 == i)
+ break;
+ }
+ if (j == rc) {
+ close_client(i);
+ break;
+ }
+ }
+ }
pseudo_debug(PDBGF_SERVER, "server loop complete [%d clients left]\n", active_clients);
} else {
pseudo_error("epoll_wait failed: %s\n", strerror(errno));
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 16/34] pseudo_server: Always use epoll
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (14 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 15/34] pseudo_server: Drop an idle client when out of file descriptors Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 17/34] configure: Fix --disable-* options Mark Hatle
` (17 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
The server had two main loops: a select() based loop, used by default,
and an epoll loop, used with --enable-epoll. Every change to client
handling had to be made in both, and select() can't handle descriptors
numbered 1024 or higher, which a server with many clients can reach.
Now that Linux is the only port, always use the epoll loop, as
pseudo_server_loop(), and remove the select() loop and the
PSEUDO_EPOLL conditionals. pseudo_server.c fails with #error for a
port other than Linux.
configure accepts --enable-epoll for compatibility and ignores it, and
fails with an error if epoll is disabled.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
Makefile.in | 3 +-
configure | 18 +----
pseudo_server.c | 231 ++++----------------------------------------------------
3 files changed, 17 insertions(+), 235 deletions(-)
diff --git a/Makefile.in b/Makefile.in
index e875864..84e3ee8 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -14,7 +14,6 @@ SQLITE=@SQLITE@
SQLITE_LIB=@SQLITE_LIB@
SQLITE_MEMORY=@SQLITE_MEMORY@
FORCE_ASYNC=@FORCE_ASYNC@
-EPOLL=@EPOLL@
XATTR=@XATTR@
XATTRDB=@XATTRDB@
PROFILING=@PROFILING@
@@ -45,7 +44,7 @@ LOCALSTATEDIR=$(PREFIX)/$(LOCALSTATE)
CFLAGS_BASE=-pipe -std=gnu99 -Wall -W -Wextra -Wno-deprecated-declarations
CFLAGS_CODE=-fPIC -D_LARGEFILE64_SOURCE -D_ATFILE_SOURCE $(ARCH_FLAGS)
-CFLAGS_DEFS=-DPSEUDO_PREFIX='"$(PREFIX)"' -DPSEUDO_SUFFIX='"$(SUFFIX)"' -DPSEUDO_BINDIR='"$(BIN)"' -DPSEUDO_LIBDIR='"$(LIB)"' -DPSEUDO_LOCALSTATEDIR='"$(LOCALSTATE)"' -DPSEUDO_VERSION='"$(VERSION)"' $(SQLITE_MEMORY) $(FORCE_ASYNC) -DPSEUDO_PASSWD_FALLBACK='$(PASSWD_FALLBACK)' $(OPTDEFS) $(EPOLL)
+CFLAGS_DEFS=-DPSEUDO_PREFIX='"$(PREFIX)"' -DPSEUDO_SUFFIX='"$(SUFFIX)"' -DPSEUDO_BINDIR='"$(BIN)"' -DPSEUDO_LIBDIR='"$(LIB)"' -DPSEUDO_LOCALSTATEDIR='"$(LOCALSTATE)"' -DPSEUDO_VERSION='"$(VERSION)"' $(SQLITE_MEMORY) $(FORCE_ASYNC) -DPSEUDO_PASSWD_FALLBACK='$(PASSWD_FALLBACK)' $(OPTDEFS)
CFLAGS_DEBUG=-O2 -g
@DEFAULT_SQLITE@CFLAGS_SQL=-L$(SQLITE)/$(SQLITE_LIB) -I$(SQLITE)/include $(RPATH)
CFLAGS_PSEUDO=$(CFLAGS_BASE) $(CFLAGS_CODE) $(CFLAGS_DEFS) \
diff --git a/configure b/configure
index b43809e..315bcd0 100755
--- a/configure
+++ b/configure
@@ -15,7 +15,6 @@ opt_arch=x86
opt_bits=
opt_sqlite=/usr
opt_rpath=
-opt_epoll=
opt_memory=
opt_async=
opt_xattr=
@@ -35,7 +34,6 @@ usage()
echo >&2 " [--suffix=...]"
echo >&2 " [--target=...]"
echo >&2 " [--enable-memory-db]"
- echo >&2 " [--enable-epoll]"
echo >&2 " [--enable-xattr]"
echo >&2 " [--enable-xattrdb]"
echo >&2 " [--enable-profiling]"
@@ -110,10 +108,11 @@ do
opt_profiling=true
;;
--enable-epoll=no)
- opt_epoll=false
+ echo >&2 "Error: disabling epoll is no longer supported: the pseudo server always uses epoll."
+ exit 1
;;
--enable-epoll=yes | --enable-epoll)
- opt_epoll=true
+ # epoll is always used; accepted for compatibility
;;
--enable-xattr=no)
opt_xattr=false
@@ -261,10 +260,6 @@ if [ -z "$opt_async" ]; then
opt_async=false
fi
-if [ -z "$opt_epoll" ]; then
- opt_epoll=false
-fi
-
if $opt_async; then
FORCE_ASYNC="-DPSEUDO_FORCE_ASYNC"
else
@@ -293,12 +288,6 @@ if [ -z "$opt_xattr" ]; then
fi
fi
-if $opt_epoll; then
- EPOLL="-DPSEUDO_EPOLL"
-else
- EPOLL=""
-fi
-
if $opt_xattr || $opt_xattrdb; then
if ! $xattr_runs; then
echo >&2 "WARNING: getfattr doesn't work, but xattr-related features requested."
@@ -339,7 +328,6 @@ touch func_deps.mk
rm -f Makefile
sed -e '
s,@PREFIX@,'"$opt_prefix"',g
- s,@EPOLL@,'"$EPOLL"',g
s,@XATTR@,'"$opt_xattr"',g
s,@XATTRDB@,'"$opt_xattrdb"',g
s,@PROFILING@,'"$opt_profiling"',g
diff --git a/pseudo_server.c b/pseudo_server.c
index 77d1c39..c54c0d2 100644
--- a/pseudo_server.c
+++ b/pseudo_server.c
@@ -15,16 +15,12 @@
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
-#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/wait.h>
-#ifdef PSEUDO_EPOLL
-#include <sys/epoll.h>
-#endif
#include <signal.h>
#include "pseudo.h"
@@ -33,6 +29,11 @@
#include "pseudo_client.h"
#include "pseudo_db.h"
+#if !PSEUDO_PORT_LINUX
+#error "pseudo: the server uses epoll(7), which is only implemented for Linux"
+#endif
+#include <sys/epoll.h>
+
static int listen_fd = -1;
typedef struct {
@@ -80,11 +81,7 @@ set_do_list_clients(int sig) {
static int messages = 0, responses = 0;
static struct timeval message_time = { .tv_sec = 0 };
-#ifdef PSEUDO_EPOLL
-static void pseudo_server_loop_epoll(void);
-#else
static void pseudo_server_loop(void);
-#endif
/* helper function to make a directory, just like mkdir -p.
* Can't use system() because the child shell would end up trying
@@ -369,11 +366,7 @@ pseudo_server_start(int daemonize) {
kill(ppid, SIGUSR1);
}
}
-#ifdef PSEUDO_EPOLL
- pseudo_server_loop_epoll();
-#else
pseudo_server_loop();
-#endif
return 0;
}
@@ -589,8 +582,8 @@ serve_client(int i) {
}
free(response_path);
} else {
- /* this should not be happening, but the exceptions aren't
- * being detected in select() for some reason.
+ /* this should not be happening, as the server loop
+ * closes clients with nothing to read.
*/
pseudo_debug(PDBGF_SERVER, "client %d: no message\n", (int) clients[i].pid);
close_client(i);
@@ -612,9 +605,12 @@ serve_client(int i) {
return rc;
}
-#ifdef PSEUDO_EPOLL
-static void pseudo_server_loop_epoll(void)
-{
+/* get clients, handle messages, shut down.
+ * This doesn't actually do any work, it just calls a ton of things which
+ * do work.
+ */
+static void
+pseudo_server_loop(void) {
struct sockaddr_un client;
socklen_t len;
int i;
@@ -803,204 +799,3 @@ static void pseudo_server_loop_epoll(void)
}
}
-#else
-
-/* get clients, handle messages, shut down.
- * This doesn't actually do any work, it just calls a ton of things which
- * do work.
- */
-static void
-pseudo_server_loop(void) {
- struct sockaddr_un client;
- socklen_t len;
- fd_set reads, writes, events;
- int max_fd, current_clients;
- struct timeval timeout;
- int i;
- int rc;
- int fd;
- int loop_timeout = pseudo_server_timeout;
- struct sigaction eat_usr2 = {
- .sa_handler = set_do_list_clients
- };
- int hitmaxfiles;
-
- clients = malloc(16 * sizeof(*clients));
- if (!clients) {
- pseudo_error("out of memory allocating client table.\n");
- exit(PSEUDO_EXIT_LISTEN_FD);
- }
-
- sigaction(SIGUSR2, &eat_usr2, NULL);
-
- clients[0].fd = listen_fd;
- clients[0].pid = getpid();
-
- for (i = 1; i < 16; ++i) {
- clients[i].fd = -1;
- clients[i].pid = 0;
- clients[i].tag = NULL;
- clients[i].program = NULL;
- }
-
- active_clients = 1;
- max_clients = 16;
- highest_client = 0;
- hitmaxfiles = 0;
-
- pseudo_debug(PDBGF_SERVER, "server loop started.\n");
- if (listen_fd < 0) {
- pseudo_error("got into loop with no valid listen fd.\n");
- exit(PSEUDO_EXIT_LISTEN_FD);
- }
- pdb_log_msg(SEVERITY_INFO, NULL, NULL, NULL, "server started (pid %d)", getpid());
-
- FD_ZERO(&reads);
- FD_ZERO(&events);
- FD_ZERO(&writes);
- FD_SET(clients[0].fd, &reads);
- FD_SET(clients[0].fd, &events);
- max_fd = clients[0].fd;
- timeout = (struct timeval) { .tv_sec = LOOP_DELAY, .tv_usec = 0 };
-
- /* EINTR tends to come from profiling, so it is not a good reason to
- * exit; other signals are caught and set the flag causing a graceful
- * exit. */
- sigset_t maskusr2;
- sigemptyset(&maskusr2);
- sigaddset(&maskusr2, SIGUSR2);
- sigprocmask(SIG_BLOCK, &maskusr2, NULL);
- while ((rc = select(max_fd + 1, &reads, &writes, &events, &timeout)) >= 0 || (errno == EINTR)) {
- sigprocmask(SIG_UNBLOCK, &maskusr2, NULL);
- if (rc == 0 || (rc == -1 && errno == EINTR)) {
- /* If there's no clients, start timing out. If there
- * are active clients, never time out.
- */
- if (active_clients == 1) {
- loop_timeout -= LOOP_DELAY;
- /* maybe flush database to disk */
- pdb_maybe_backup();
- if (loop_timeout <= 0) {
- pseudo_debug(PDBGF_SERVER, "no more clients, got bored.\n");
- die_peacefully = 1;
- } else {
- /* display this if not exiting */
- pseudo_debug(PDBGF_SERVER | PDBGF_BENCHMARK, "%d messages handled in %.4f seconds, %d responses\n",
- messages,
- (double) message_time.tv_sec +
- (double) message_time.tv_usec / 1000000.0,
- responses);
- }
- }
- } else if (rc > 0) {
- loop_timeout = pseudo_server_timeout;
- for (i = 1; i <= highest_client; ++i) {
- if (clients[i].fd == -1) {
- continue;
- } else if (FD_ISSET(clients[i].fd, &reads)) {
- int n = 0;
- ioctl(clients[i].fd, FIONREAD, &n);
- if (n == 0) {
- close_client(i);
- } else {
- serve_client(i);
- }
- } else if (hitmaxfiles) {
- /* Only close one per loop iteration in the interests of caution */
- close_client(i);
- hitmaxfiles = 0;
- }
- if (die_forcefully)
- break;
- }
- hitmaxfiles = 0;
- if (!die_forcefully &&
- (FD_ISSET(clients[0].fd, &events) ||
- FD_ISSET(clients[0].fd, &reads))) {
- len = sizeof(client);
- if ((fd = accept(listen_fd, (struct sockaddr *) &client, &len)) != -1) {
- /* Don't allow clients to end up on fd 2, because glibc's
- * malloc debug uses that fd unconditionally.
- */
- if (fd == 2) {
- int newfd = fcntl(fd, F_DUPFD, 3);
- close(fd);
- fd = newfd;
- }
- pseudo_debug(PDBGF_SERVER, "new client fd %d\n", fd);
- open_client(fd);
- /* A new client implicitly cancels any
- * previous shutdown request, or a
- * shutdown for lack of clients.
- */
- pseudo_server_timeout = DEFAULT_PSEUDO_SERVER_TIMEOUT;
- die_peacefully = 0;
- } else if (errno == EMFILE) {
- hitmaxfiles = 1;
- pseudo_debug(PDBGF_SERVER, "Hit max open files, dropping a client.\n");
- }
- }
- pseudo_debug(PDBGF_SERVER, "server loop complete [%d clients left]\n", active_clients);
- }
- if (do_list_clients) {
- do_list_clients = 0;
- pseudo_info("listing clients [1 through %d]:\n", highest_client);
- for (i = 1; i <= highest_client; ++i) {
- if (clients[i].fd == -1) {
- pseudo_info("client %4d: inactive.\n", i);
- continue;
- }
- pseudo_info("client %4d: fd %4d, pid %5d, state %s, program %s\n",
- i, clients[i].fd, clients[i].pid,
- FD_ISSET(clients[i].fd, &reads) ? "R" : "-",
- clients[i].program ? clients[i].program : "<unspecified>");
- }
- pseudo_info("done.\n");
- }
- if (die_peacefully || die_forcefully) {
- pseudo_debug(PDBGF_SERVER, "quitting.\n");
- pseudo_debug(PDBGF_SERVER | PDBGF_BENCHMARK, "server %d exiting: handled %d messages in %.4f seconds\n",
- getpid(), messages,
- (double) message_time.tv_sec +
- (double) message_time.tv_usec / 1000000.0);
- pdb_log_msg(SEVERITY_INFO, NULL, NULL, NULL, "server %d exiting: handled %d messages in %.4f seconds",
- getpid(), messages,
- (double) message_time.tv_sec +
- (double) message_time.tv_usec / 1000000.0);
- /* and at this point, we'll start refusing connections */
- close(clients[0].fd);
- /* This is a good place to insert a delay for
- * debugging race conditions during startup. */
- /* usleep(300000); */
- exit(0);
- }
- FD_ZERO(&reads);
- FD_ZERO(&writes);
- FD_ZERO(&events);
- FD_SET(clients[0].fd, &reads);
- FD_SET(clients[0].fd, &events);
- max_fd = clients[0].fd;
- /* current_clients is a sanity check; note that for
- * purposes of select(), the server is one of the fds,
- * and thus, "a client".
- */
- current_clients = 1;
- for (i = 1; i <= highest_client; ++i) {
- if (clients[i].fd != -1) {
- ++current_clients;
- FD_SET(clients[i].fd, &reads);
- if (clients[i].fd > max_fd)
- max_fd = clients[i].fd;
- }
- }
- if (current_clients != active_clients) {
- pseudo_debug(PDBGF_SERVER, "miscount of current clients (%d) against active_clients (%d)?\n",
- current_clients, active_clients);
- }
- /* reinitialize timeout because Linux select alters it */
- timeout = (struct timeval) { .tv_sec = LOOP_DELAY, .tv_usec = 0 };
- sigprocmask(SIG_BLOCK, &maskusr2, NULL);
- }
- pseudo_error("select failed: %s\n", strerror(errno));
-}
-#endif /* this is the else of #ifdef PSEUDO_EPOLL */
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 17/34] configure: Fix --disable-* options
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (15 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 16/34] pseudo_server: Always use epoll Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 18/34] pseudo_server: Lower highest_client when the highest client closes Mark Hatle
` (16 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
configure turns --disable-<option> into --enable-<option>=no before
matching options, but used ${arg%--disable-}, which removes a suffix,
instead of ${arg#--disable-}, which removes the prefix. Every
--disable-* option became --enable---disable-<option>=no, which matched
nothing, so it was ignored with an "Unrecognized option" warning.
Remove the prefix, so --disable-force-async, --disable-profiling,
--disable-xattr, --disable-xattrdb and --disable-memory-db take effect,
and --disable-epoll fails with the intended error.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
configure | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/configure b/configure
index 315bcd0..e659370 100755
--- a/configure
+++ b/configure
@@ -58,7 +58,7 @@ for arg
do
case "$arg" in
--disable-*)
- arg="--enable-${arg%--disable-}=no"
+ arg="--enable-${arg#--disable-}=no"
;;
esac
case "$arg" in
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 18/34] pseudo_server: Lower highest_client when the highest client closes
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (16 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 17/34] configure: Fix --disable-* options Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 19/34] pseudo_server: Set highest_client to the new client when growing Mark Hatle
` (15 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
close_client() is meant to move highest_client down past closed slots
when the highest client goes away, but the loop condition was inverted:
it stopped at the first closed slot, which is always the one just
closed, so highest_client never went down.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_server.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pseudo_server.c b/pseudo_server.c
index c54c0d2..edcb23e 100644
--- a/pseudo_server.c
+++ b/pseudo_server.c
@@ -441,7 +441,7 @@ close_client(int client) {
clients[client].program = NULL;
--active_clients;
if (client == highest_client)
- while (clients[highest_client].fd != -1 && highest_client > 0)
+ while (clients[highest_client].fd == -1 && highest_client > 0)
--highest_client;
}
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 19/34] pseudo_server: Set highest_client to the new client when growing
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (17 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 18/34] pseudo_server: Lower highest_client when the highest client closes Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 20/34] pseudo_util: Store PSEUDO_SEVERITY as severity names Mark Hatle
` (14 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
When open_client() grows the client table, the new client goes in slot
max_clients, but highest_client was set to max_clients + 1, one past
it. This was harmless, as that slot exists and is unused, but the
server scanned it needlessly.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_server.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pseudo_server.c b/pseudo_server.c
index edcb23e..13caced 100644
--- a/pseudo_server.c
+++ b/pseudo_server.c
@@ -407,7 +407,7 @@ open_client(int fd) {
clients[max_clients].pid = 0;
clients[max_clients].tag = NULL;
clients[max_clients].program = NULL;
- highest_client = max_clients + 1;
+ highest_client = max_clients;
max_clients += 16;
++active_clients;
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 20/34] pseudo_util: Store PSEUDO_SEVERITY as severity names
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (18 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 19/34] pseudo_server: Set highest_client to the new client when growing Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 21/34] pseudo: Fix the -k severity option Mark Hatle
` (13 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Store the flags as a comma-separated list of severity names.
pseudo_severity_flags_finalize() stored the severity flags in
PSEUDO_SEVERITY using pseudo_flags_finalize(), which writes debug type
letters. The default "info,warn,error" became "foP", which
pseudo_severity_flags_in() doesn't understand, so every process started
under pseudo, including a server started by a client, ran with no
severity flags: pseudo_info() and pseudo_warning() messages were
silently dropped.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_util.c | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/pseudo_util.c b/pseudo_util.c
index 351cd09..631c3e9 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -443,7 +443,18 @@ pseudo_evlog_flags_finalize(void) {
void
pseudo_severity_flags_finalize(void) {
- pseudo_flags_finalize(pseudo_util_severity_flags, "PSEUDO_SEVERITY");
+ /* PSEUDO_SEVERITY holds severity names, not debug type letters */
+ char buf[128] = "";
+ size_t len = 0;
+
+ for (int i = SEVERITY_NONE + 1; i < SEVERITY_MAX; ++i) {
+ if (pseudo_util_severity_flags & (1UL << i)) {
+ snprintf(buf + len, sizeof(buf) - len, "%s%s",
+ len ? "," : "", pseudo_sev_name(i));
+ len = strlen(buf);
+ }
+ }
+ pseudo_set_value("PSEUDO_SEVERITY", buf);
}
static unsigned long
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 21/34] pseudo: Fix the -k severity option
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (19 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 20/34] pseudo_util: Store PSEUDO_SEVERITY as severity names Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 22/34] pseudo_util: Make -v and numeric PSEUDO_DEBUG add debug flags Mark Hatle
` (12 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
-k was declared to getopt() without an argument, so the severities that
followed it were taken as the command to run, and the flags were set
from a NULL string, turning off pseudo_info() and pseudo_warning().
It was also never stored in PSEUDO_SEVERITY, so programs started by
pseudo didn't inherit it, unlike -x and PSEUDO_DEBUG.
Make -k take an argument, store the result in PSEUDO_SEVERITY after
the options are processed, and list it in the usage message.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/pseudo.c b/pseudo.c
index 11112dd..4cd0993 100644
--- a/pseudo.c
+++ b/pseudo.c
@@ -47,7 +47,7 @@ static int pseudo_db_check(int fix);
void
usage(int status) {
FILE *f = status ? stderr : stdout;
- fputs("Usage: pseudo [-dflv] [-x flags] [-P prefix] [-rR root] [-t timeout] [command]\n", f);
+ fputs("Usage: pseudo [-dflv] [-x flags] [-k severities] [-P prefix] [-rR root] [-t timeout] [command]\n", f);
fputs(" pseudo -h\n", f);
fputs(" pseudo [-dflv] [-x flags] [-P prefix] [-BC] -i path\n", f);
fputs(" pseudo [-dflv] [-x flags] [-P prefix] [-BC] -m from -M to\n", f);
@@ -134,7 +134,7 @@ main(int argc, char *argv[]) {
* wrong. The + suppresses this annoying behavior, but may not
* be compatible with sane option libraries.
*/
- while ((o = getopt(argc, argv, "+BCdfhi:klm:M:p:P:r:R:St:vVx:")) != -1) {
+ while ((o = getopt(argc, argv, "+BCdfhi:k:lm:M:p:P:r:R:St:vVx:")) != -1) {
switch (o) {
case 'B': /* rebuild database */
opt_B = 1;
@@ -245,6 +245,7 @@ main(int argc, char *argv[]) {
}
}
pseudo_debug_flags_finalize();
+ pseudo_severity_flags_finalize();
/* Options are processed, preserve them... */
pseudo_set_value("PSEUDO_OPTS", opts);
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 22/34] pseudo_util: Make -v and numeric PSEUDO_DEBUG add debug flags
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (20 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 21/34] pseudo: Fix the -k severity option Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 23/34] AGENTS.md: Add to assist future developers Mark Hatle
` (11 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Since 2a7591e, pseudo_debug_set() replaces the debug flags, so each -v,
and each step of a numeric PSEUDO_DEBUG level, replaced the previous
flag instead of adding to it: -v -v -v gave "o" instead of "nfo". Add
the flag in pseudo_debug_verbose() instead. Also start a numeric level
from no flags, as pseudo_init_util() can run more than once in a
process and each run added another set.
Add test-debug-flags, covering -v, numeric PSEUDO_DEBUG, and how
PSEUDO_SEVERITY and -k reach programs run under pseudo.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_util.c | 12 ++++++----
test/test-debug-flags.sh | 59 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 67 insertions(+), 4 deletions(-)
create mode 100755 test/test-debug-flags.sh
diff --git a/pseudo_util.c b/pseudo_util.c
index 631c3e9..1eb90d5 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -96,6 +96,8 @@ static unsigned long pseudo_severity_flags_in(char *);
* best to handle that case.
*/
static int pseudo_util_initted = -1; /* Not yet run */
+/* number of debug flags turned on by pseudo_debug_verbose() */
+static int max_debug_level = 0;
/* bypass wrapper logic on path computations */
int (*pseudo_real_lstat)(const char *path, PSEUDO_STATBUF *buf) = NULL;
@@ -217,6 +219,10 @@ pseudo_init_util(void) {
int i;
int level = pseudo_atoi_wrapper(env);
if (level > 0) {
+ /* a level selects the first <level> flags, even if
+ * this runs more than once in a process */
+ pseudo_util_debug_flags = 0;
+ max_debug_level = 0;
for (i = 0; i < level; ++i) {
pseudo_debug_verbose();
}
@@ -379,9 +385,6 @@ with_libpseudo(char *list) {
char *pseudo_version = PSEUDO_VERSION;
-/* going away soon */
-static int max_debug_level = 0;
-
void
pseudo_debug_terse(void) {
char s[2] = { pseudo_debug_type_symbolic(max_debug_level) };
@@ -397,7 +400,8 @@ pseudo_debug_verbose(void) {
char s[2] = { pseudo_debug_type_symbolic(max_debug_level + 1) };
if (s[0]) {
- pseudo_debug_set(s);
+ /* add to the flags already set, unlike pseudo_debug_set() */
+ pseudo_util_debug_flags |= pseudo_debug_flags_in(s);
++max_debug_level;
}
}
diff --git a/test/test-debug-flags.sh b/test/test-debug-flags.sh
new file mode 100755
index 0000000..1cf6b14
--- /dev/null
+++ b/test/test-debug-flags.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Check how debugging and severity settings reach programs run under pseudo:
+# - each -v adds the next debugging flag to those already set;
+# - a numeric PSEUDO_DEBUG turns on exactly that many flags, even in a process
+# which initializes the client library more than once;
+# - PSEUDO_SEVERITY is passed on as severity names, from the default or -k.
+
+tmpdir="$(mktemp -d -p "$PWD" debug_flags_XXXXXX)"
+mkdir "$tmpdir/state"
+
+# Run a separate pseudo, with its own state directory, outside of the pseudo
+# running this test: PSEUDO_UNLOAD keeps libpseudo out of env(1), which then
+# starts pseudo with a clean environment.
+new_pseudo() {
+ PSEUDO_UNLOAD=1 env -u PSEUDO_UNLOAD -u PSEUDO_DEBUG -u PSEUDO_SEVERITY \
+ PSEUDO_LOCALSTATEDIR="$tmpdir/state" ./bin/pseudo "$@"
+}
+trap "new_pseudo -S >/dev/null 2>&1; rm -rf '$tmpdir'" EXIT TERM INT
+
+rc=0
+fail() {
+ echo "FAILED: $*"
+ rc=1
+}
+
+# value of a variable in the environment of a program run by a new pseudo
+var_under() {
+ local var=$1
+ shift
+ new_pseudo "$@" env | sed -n "s/^$var=//p"
+}
+
+check() {
+ local want=$1 got=$2
+ shift 2
+ [ "$got" = "$want" ] || fail "$*: got '$got', expected '$want'"
+}
+
+check nfo "$(var_under PSEUDO_DEBUG -v -v -v)" "pseudo -v -v -v"
+check no "$(var_under PSEUDO_DEBUG -x o -v)" "pseudo -x o -v"
+check info,warn,error "$(var_under PSEUDO_SEVERITY)" "default PSEUDO_SEVERITY"
+check warn,error "$(var_under PSEUDO_SEVERITY -k warn,error)" "pseudo -k warn,error"
+
+# A level turns on the first flags in order: o (operations, which logs
+# "exec" messages) is the third and P (process IDs, "new pid") the fourth.
+# The commands initialize the client library more than once.
+for level in 2 4; do
+ PSEUDO_DEBUG=$level PSEUDO_DEBUG_FILE="$tmpdir/debug.$level" \
+ sh -c '/bin/true; sh -c /bin/true' 2>/dev/null
+done
+grep -q "new pid:" "$tmpdir/debug.2" 2>/dev/null &&
+ fail "PSEUDO_DEBUG=2 turned on more than the first two debugging flags"
+grep -q "exec /" "$tmpdir/debug.4" 2>/dev/null ||
+ fail "PSEUDO_DEBUG=4 didn't turn on the first four debugging flags"
+
+exit $rc
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 23/34] AGENTS.md: Add to assist future developers
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (21 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 22/34] pseudo_util: Make -v and numeric PSEUDO_DEBUG add debug flags Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 24/34] pseudo_util: Fix the date format in the event log dump Mark Hatle
` (10 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Based partially off of the Yocto Project standards, this is my attempt
at documenting the configuration I use with agents during maintainer
and development activities.
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
AGENTS.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 85 insertions(+)
create mode 100644 AGENTS.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..c0d041a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,85 @@
+# AGENTS.md — pseudo
+
+Guidance for AI agents working in the `pseudo` repository.
+
+## Making changes
+
+- No web PR workflow. The project uses **patches on mailing lists**
+ (yocto-patches@lists.yoctoproject.org). Don't assume GitHub PR
+ conventions apply. Subject on the mailing list must include
+ '[pseudo]'.
+- One Git commit per logical change (e.g., one commit per fix, refactor or new test).
+- Split unrelated changes, even small ones found along the way: a bug
+ noticed during a refactor gets its own commit.
+- Keep every commit building cleanly, with tests passing, so the series
+ bisects. Put a new test after the fix it checks.
+- Commit message format:
+ - Summary line prefixed with the component or short file path,
+ e.g. `pseudo_util: Remove stale with_libpseudo() comment`.
+ Check `git log --oneline <path>` for the prefix convention already in use.
+ - Body explains what/why/how, and how it was tested.
+ - Prefer explaining *why* a change was made over restating what's
+ already visible in the diff, unless the diff is long/hard to follow
+ or the reason for the change isn't known — in those cases, summarize
+ what changed instead.
+ - Be succinct: use as few sentences and lines as possible while still
+ covering what/why/how. Cut restatements, filler, and any detail a
+ reviewer wouldn't need (e.g. exact test-run counts, step-by-step
+ narration). One short paragraph is usually enough; only add a second
+ if a distinct point (e.g. testing) needs it.
+ - The most relevant information about the change should be presented first.
+ - Reference bugs as `[YOCTO #1234]` when applicable.
+ - Use `Reported-by`, `Suggested-by`, `Tested-by`, `Reviewed-by`, `Cc` tags
+ where relevant.
+
+### AI-generated code
+
+- **Mandatory**: any AI-generated code/commit must add an `AI-Generated:`
+ tag line in the commit message, placed *before* `Signed-off-by` (if present),
+ naming the tool used, e.g.:
+
+ ```
+ component: Add the ability to ...
+
+ AI-Generated: Uses GitHub Copilot
+ ```
+
+- **Mandatory** Do not automatically add a `Signed-off-by`. This tag must
+ always be written by the human contributor, never by the AI tool — the
+ DCO/Developer's Statement of Origin still applies in full.
+- Contributor is responsible for confirming the AI tool's terms don't
+ conflict with the project's open source license/IP policy, and that
+ any third-party copyrighted material in the AI output is properly
+ licensed before contributing it. See the Linux Foundation generative AI
+ guidance: https://www.linuxfoundation.org/legal/generative-ai.
+- `Co-authored-by` should be omitted in favor of the `AI-Generated` tag.
+ Don't add other tool-specific trailers either (e.g. `Copilot-Session`).
+
+## Building and testing
+
+- Configure and build: `./configure --prefix=$(pwd)/test && make`
+- Full test suite: `make test-verbose` (or `make test`). Single tests:
+ `./run_tests.sh -v test-name`.
+- Tests are `test/test-*.sh`, with optional `test/test-*.c` programs that
+ the Makefile builds automatically. A script exits 0 to pass and 255 to
+ report a skip; C helpers conventionally exit 77 for "not supported"
+ (for example ENOSYS), which the script maps to 255. Create temporary
+ trees below $PWD so pseudo tracks them.
+- Changes to the server, locking or path handling: also run the
+ test-parallel-* tests several times, as they are timing dependent.
+
+## Code layout
+
+- Wrappers are generated by `makewrappers` from `ports/*/wrapfuncs.in`,
+ `templates/` and the guts in `ports/*/guts/`. Don't edit the generated
+ files (pseudo_wrapfuncs.*, pseudo_wrapper_table.c, pseudo_ports.h,
+ port_wrappers.c).
+- Linux is the only port. Put port-specific definitions in
+ `ports/<port>/portdefs.h`. Where shared code needs something only one
+ port provides, fail with `#error` for other ports rather than silently
+ falling back.
+- Keep pseudo.1 and README.security in step with behaviour changes:
+ options, environment variables, diagnostics, security model.
+
+When in doubt about a convention, grep `git log --oneline <path>` in the
+target repo for precedent before inventing a new style.
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 24/34] pseudo_util: Fix the date format in the event log dump
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (22 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 23/34] AGENTS.md: Add to assist future developers Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 25/34] pseudo: List -p in the usage message Mark Hatle
` (9 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
pseudo_evlog_dump() formatted the first and last entry times with
"%Y-%M-%D", which prints the minutes and a date in place of the
month and day. Use "%Y-%m-%d" instead.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_util.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pseudo_util.c b/pseudo_util.c
index 1eb90d5..e0d7306 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -560,8 +560,8 @@ pseudo_evlog_dump(void) {
}
localtime_r(&first, &first_tm);
localtime_r(&last, &last_tm);
- strftime(firstdate, 64, "%Y-%M-%D %H:%M:%S", &first_tm);
- strftime(lastdate, 64, "%Y-%M-%D %H:%M:%S", &last_tm);
+ strftime(firstdate, 64, "%Y-%m-%d %H:%M:%S", &first_tm);
+ strftime(lastdate, 64, "%Y-%m-%d %H:%M:%S", &last_tm);
len = snprintf(scratch, 256, "event log for pid %d [%d entries]\n",
getpid(), entries);
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 25/34] pseudo: List -p in the usage message
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (23 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 24/34] pseudo_util: Fix the date format in the event log dump Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 26/34] pseudo_util: Write the event log to PSEUDO_EVLOG_FILE Mark Hatle
` (8 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pseudo.c b/pseudo.c
index 4cd0993..26c7a36 100644
--- a/pseudo.c
+++ b/pseudo.c
@@ -47,7 +47,7 @@ static int pseudo_db_check(int fix);
void
usage(int status) {
FILE *f = status ? stderr : stdout;
- fputs("Usage: pseudo [-dflv] [-x flags] [-k severities] [-P prefix] [-rR root] [-t timeout] [command]\n", f);
+ fputs("Usage: pseudo [-dflv] [-x flags] [-k severities] [-p path] [-P prefix] [-rR root] [-t timeout] [command]\n", f);
fputs(" pseudo -h\n", f);
fputs(" pseudo [-dflv] [-x flags] [-P prefix] [-BC] -i path\n", f);
fputs(" pseudo [-dflv] [-x flags] [-P prefix] [-BC] -m from -M to\n", f);
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 26/34] pseudo_util: Write the event log to PSEUDO_EVLOG_FILE
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (24 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 25/34] pseudo: List -p in the usage message Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 27/34] pseudo.1: Update to match the current code Mark Hatle
` (7 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
PSEUDO_EVLOG_FILE was in the environment table, but
pseudo_evlog_logfile() was never called, so the event log was always
dumped to standard error, which may be the output of whatever program
was running. Call it when the client sets up its debug log file.
pseudo_logfile() also stored the descriptor it opened as the debug log
descriptor, which would have redirected debug output to the event log
file. When a program closes the descriptor used by either log, move
both logs that use it to a copy, as both are standard error by default.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
doc/program_flow | 4 ++--
pseudo.h | 1 +
pseudo_client.c | 9 +++++++--
pseudo_util.c | 1 -
pseudo_wrappers.c | 3 ++-
5 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/doc/program_flow b/doc/program_flow
index e987e4a..f7eb26c 100644
--- a/doc/program_flow
+++ b/doc/program_flow
@@ -9,10 +9,10 @@ libpseudo execution flow:
pseudo_wrappers.c: (constructor) _libpseudo_init()
pseudo_util.c: pseudo_init_util()
copy environment
- setup PSEUDO_DEBUG levels
+ setup PSEUDO_DEBUG, PSEUDO_EVLOG and PSEUDO_SEVERITY flags
pseudo_wrappers.c: pseudo_init_wrappers()
setup pseudo_functions
- setup pseudo_logfile
+ setup log files (PSEUDO_DEBUG_FILE, PSEUDO_EVLOG_FILE)
pseudo_client.c: pseudo_init_client()
setup PSEUDO_DISABLED
setup pseudo_localstate_dir_fd
diff --git a/pseudo.h b/pseudo.h
index 13249bf..6a7339b 100644
--- a/pseudo.h
+++ b/pseudo.h
@@ -100,6 +100,7 @@ extern char *pseudo_get_bindir(void);
extern char *pseudo_get_libdir(void);
extern char *pseudo_get_localstatedir(void);
extern int pseudo_debug_logfile(char *defname, int prefer_fd);
+extern int pseudo_evlog_logfile(char *defname, int prefer_fd);
extern ssize_t pseudo_sys_path_max(void);
extern ssize_t pseudo_path_max(void);
#define PSEUDO_PWD_MAX 4096
diff --git a/pseudo_client.c b/pseudo_client.c
index bbe0829..58e8810 100644
--- a/pseudo_client.c
+++ b/pseudo_client.c
@@ -2152,8 +2152,13 @@ pseudo_client_op(pseudo_op_t op, int access, int fd, int dirfd, const char *path
if (connect_fd == -1) {
pseudo_error("tried to close connection, couldn't dup: %s\n", strerror(errno));
}
- } else if (fd == pseudo_util_debug_fd) {
- pseudo_util_debug_fd = pseudo_fd(fd, COPY_FD);
+ } else if (fd == pseudo_util_debug_fd || fd == pseudo_util_evlog_fd) {
+ /* both are stderr by default */
+ int newfd = pseudo_fd(fd, COPY_FD);
+ if (fd == pseudo_util_debug_fd)
+ pseudo_util_debug_fd = newfd;
+ if (fd == pseudo_util_evlog_fd)
+ pseudo_util_evlog_fd = newfd;
} else if (fd == pseudo_localstate_dir_fd) {
pseudo_localstate_dir_fd = pseudo_fd(fd, COPY_FD);
} else if (fd == pseudo_pathlock_fd) {
diff --git a/pseudo_util.c b/pseudo_util.c
index e0d7306..1e29ba5 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -1738,7 +1738,6 @@ pseudo_logfile(char *filename, char *defname, int prefer_fd) {
fd = newfd;
}
}
- pseudo_util_debug_fd = fd;
}
free(pseudo_path);
return fd;
diff --git a/pseudo_wrappers.c b/pseudo_wrappers.c
index d073b1e..a4e9015 100644
--- a/pseudo_wrappers.c
+++ b/pseudo_wrappers.c
@@ -183,9 +183,10 @@ pseudo_init_wrappers(void) {
pseudo_real_execv = dlsym(RTLD_NEXT, "execv");
/* Once the wrappers are setup, we can now use open... so
- * setup the logfile, if necessary...
+ * setup the logfiles, if necessary...
*/
pseudo_debug_logfile(NULL, -1);
+ pseudo_evlog_logfile(NULL, -1);
pseudo_magic();
pseudo_droplock();
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 27/34] pseudo.1: Update to match the current code
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (25 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 26/34] pseudo_util: Write the event log to PSEUDO_EVLOG_FILE Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 28/34] ports/linux: Don't require <linux/mount.h> Mark Hatle
` (6 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Document the -k, -p and -P options, the full list of debugging flags,
-v and numeric PSEUDO_DEBUG levels, and the PSEUDO_SEVERITY,
PSEUDO_IGNORE_PATHS, PSEUDO_INCLUDE_PATHS, PSEUDO_EVLOG and
PSEUDO_EVLOG_FILE variables.
Rewrite DIAGNOSTICS for the severity flags and the event log, and BUGS
for pseudo's use of fcntl(2) locks, dropping the outdated promise of a
future fix. Add a SECURITY section that pseudo is a build tool, relies
on the cooperation of the programs it runs, and is not secure by design.
Fix PSEUDO_NOSYMLINKEXP, whose meaning was inverted: symlink expansion
is on when the variable is unset or 0, and disabled by any other value.
Note that PSEUDO_PASSWD may also be a colon-separated list of
directories, while -p accepts only a single directory.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo.1 | 318 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 281 insertions(+), 37 deletions(-)
diff --git a/pseudo.1 b/pseudo.1
index 40429aa..2ccf6bd 100644
--- a/pseudo.1
+++ b/pseudo.1
@@ -26,6 +26,14 @@ pseudo \- run a command in a virtual root environment
.I flags
]
[
+.B \-k
+.I severities
+]
+[
+.B \-p
+.I path
+]
+[
.B \-P
.I prefix
]
@@ -249,12 +257,53 @@ runs as a server, and does not try to start other commands. This mode
is useful for debugging.
.TP 8
+.BI \-k\ severities
+Select which
+.I pseudo
+diagnostic messages are shown, as a comma-separated list of severities.
+Leaving out
+.B info
+or
+.B warn
+suppresses informational messages or warnings; errors are always shown.
+The default is
+.IR info,warn,error .
+This sets the
+.B PSEUDO_SEVERITY
+environment variable, so it also applies to programs started by this
+invocation of
+.IR pseudo .
+
+.TP 8
.BI \-l\ (log)
Enable logging. The
.I pseudo
daemon will log every filesystem transaction in the log database.
.TP 8
+.BI \-p\ path
+Set the
+.B PSEUDO_PASSWD
+environment variable to the absolute path of
+.IR path :
+the directory containing the
+.I etc/passwd
+and
+.I etc/group
+files used to emulate password and group lookups.
+Only a single directory is accepted; to search several, set
+.B PSEUDO_PASSWD
+to a colon-separated list directly.
+
+.TP 8
+.BI \-P\ prefix
+Set the
+.B PSEUDO_PREFIX
+environment variable: the directory under which
+.I pseudo
+finds its server and keeps its database.
+
+.TP 8
.BI \-r\ root
.TP 8
.BI \-R\ root
@@ -282,23 +331,33 @@ new clients can restart the daemon if they need it.
.TP 8
.BI \-v\ (verbose)
-Increase the verbosity of the
+Enable debugging messages from the
.I pseudo
daemon, and the client library for any programs started by this
invocation of
.IR pseudo .
-This is equivalent to the numeric form of the
+Each
+.B \-v
+turns on the next debugging flag, in the order listed under
.B PSEUDO_DEBUG
-environment variable; multiple
+in
+.BR ENVIRONMENT ,
+in addition to those already on, so multiple
.B \-v
-options increase the debugging level.
+options increase the debugging level. This is equivalent to the numeric
+form of the
+.B PSEUDO_DEBUG
+environment variable. Use
+.B \-x
+to choose specific flags.
.TP 8
.BI \-x\ (debug)
-Set specific debugging flags (the
-.I pseudo
-utility's help message lists them). This is equivalent to the string
-form of the
+Set specific debugging flags, as letters (listed under
+.B PSEUDO_DEBUG
+in
+.BR ENVIRONMENT ).
+This sets the
.B PSEUDO_DEBUG
environment variable.
@@ -376,8 +435,11 @@ device/inode numbers.
.SH DIAGNOSTICS
Depending on invocation, diagnostic messages usually go either to standard
error or to the file
-.B PSEUDO_PREFIX
-.IR /var/pseudo/pseudo.log .
+.I pseudo.log
+in
+.B PSEUDO_LOCALSTATEDIR
+(by default,
+.BR PSEUDO_PREFIX /var/pseudo).
By default,
.I pseudo
daemon messages go into the log file, but messages generated by the client
@@ -385,21 +447,33 @@ code go to standard error. These can be changed using the
.B PSEUDO_DEBUG_FILE
environment variable, documented in
.BR ENVIRONMENT .
-At the default logging level, only critical
-messages are displayed. If you have raised the logging level (using the
-.I \-v
+If a client can't communicate with the server, it aborts, first writing
+its most recent events (see
+.BR PSEUDO_EVLOG )
+to standard error, or to the file named by
+.BR PSEUDO_EVLOG_FILE .
+
+Diagnostic messages are errors, warnings and informational messages.
+Errors are always shown. Warnings and informational messages are shown
+by default, and can be suppressed with the
+.B \-k
option or the
+.B PSEUDO_SEVERITY
+environment variable. Debugging messages are only shown if enabled with
+the
+.BR \-v\ or\ \-x
+options or the
.B PSEUDO_DEBUG
-environment variable), additional messages are displayed. Levels higher
-than 2 are very unlikely to be useful outside of
+environment variable, and are very unlikely to be useful outside of
.I pseudo
development.
-Diagnostic messages seen by default are those which are believed to indicate
-either a serious internal flaw in
+Errors indicate either a serious internal flaw in
.I pseudo
-or a completely unexpected failure from the underlying operating system. In
-normal use, you should see no diagnostic messages.
+or an unexpected failure from the underlying operating system. Warnings
+and informational messages report unusual conditions, such as a program
+calling a function the underlying C library doesn't provide. In normal
+use, you should see few diagnostic messages.
.SH ENVIRONMENT
The most significant environment variables for
@@ -434,14 +508,77 @@ path. Paths that are relative to this are treated as though they were
instead relative to the filesystem root.
.TP 8
.B PSEUDO_DEBUG
-This variable holds either a numeric "debug level" for
-.I pseudo
-to run at, or a set of specific debugging flags, generally letters.
-Use
-.B pseudo -h
-to see the available flags. In general, this is useful only for debugging
-.I pseudo
-itself.
+This variable holds either a set of debugging flags, as letters, or a
+number, which turns on that many flags in the order listed below, as
+with repeated
+.B \-v
+options. In general, this is useful only for debugging
+.I pseudo
+itself. The flags are:
+.RS
+.PD 0
+.TP 4
+.B n
+consistency checks
+.TP 4
+.B f
+file creation/deletion
+.TP 4
+.B o
+operations
+.TP 4
+.B P
+show process IDs
+.TP 4
+.B c
+client side startup/shutdown
+.TP 4
+.B v
+server side startup/shutdown
+.TP 4
+.B d
+database interactions
+.TP 4
+.B D
+xattr database
+.TP 4
+.B R
+profiling
+.TP 4
+.B y
+system calls
+.TP 4
+.B e
+environment manipulation
+.TP 4
+.B r
+chroot functionality
+.TP 4
+.B p
+path computations
+.TP 4
+.B s
+SQL query information
+.TP 4
+.B w
+wrapper functionality
+.TP 4
+.B i
+client/server interactions
+.TP 4
+.B k
+invocation and launching
+.TP 4
+.B b
+performance statistics
+.TP 4
+.B V
+extra detail, for the other flags given
+.TP 4
+.B x
+extended attributes
+.PD
+.RE
.TP 8
.B PSEUDO_DEBUG_FILE
The name of a file to use for debugging messages from the pseudo client;
@@ -501,6 +638,53 @@ no function outside of debugging
.I pseudo
itself.
.TP 8
+.B PSEUDO_EVLOG
+This variable holds debugging flags, as for
+.BR PSEUDO_DEBUG ,
+selecting events which the client library records in memory, keeping the
+most recent 250. Currently only the
+.B c
+flag (client side startup/shutdown) records events: starting the server,
+connecting to it, and retrying requests. If a client gives up because it
+can't communicate with the server, it writes these events to the
+.B PSEUDO_EVLOG_FILE
+file, or to standard error, before aborting. This is useful only for
+debugging
+.I pseudo
+itself.
+.TP 8
+.B PSEUDO_EVLOG_FILE
+The name of a file to write the client's event log to, instead of
+standard error (see
+.BR PSEUDO_EVLOG ).
+As with
+.BR PSEUDO_DEBUG_FILE ,
+a single
+.B %s
+is replaced with the short program name, and a single
+.B %d
+with the process ID.
+.TP 8
+.B PSEUDO_IGNORE_PATHS
+A comma-separated list of path prefixes. Operations on a path which
+starts with one of them are passed directly to the underlying C library,
+and are not recorded in the
+.I pseudo
+database. The prefixes are compared as strings, so
+.I /tmp
+also matches
+.IR /tmpfiles .
+Paths inside the current emulated
+.I chroot
+directory are never ignored.
+.TP 8
+.B PSEUDO_INCLUDE_PATHS
+A comma-separated list of path prefixes. If this variable is set,
+only operations on paths which start with one of them are tracked, and
+all other paths are ignored as for
+.BR PSEUDO_IGNORE_PATHS ,
+which is then not used.
+.TP 8
.B PSEUDO_LIBDIR
This directory holds the path to the
.I pseudo
@@ -527,10 +711,10 @@ prepends the chroot directory to
the paths used for absolute symlinks; this behavior ensures that
opening symlinks produces expected results in most cases. In some
cases you may want to suppress this. If this variable is unset, or
-set to any value other than 0,
+set to 0,
.I pseudo
-expands symlink paths like this. If this variable is set to 0,
-the behavior is disabled.
+expands symlink paths like this. If this variable is set to any other
+value, including an empty string, the behavior is disabled.
.TP 8
.BR PSEUDO_OPTS
This variable holds options to be passed to any new
@@ -553,6 +737,8 @@ directory containing files named
.I passwd
and
.IR group .
+It may also be a colon-separated list of such directories, which are
+tried in order.
When
.I pseudo
is emulating a
@@ -600,6 +786,18 @@ and recorded in the log database if any logging occurs related to a
specific client. Note that different clients may have different tags
associated with them; the tag value is per-client, not per-server.
.TP 8
+.B PSEUDO_SEVERITY
+A comma-separated list of severities selecting which diagnostic messages
+are shown, as for the
+.B \-k
+option. Leaving out
+.B info
+or
+.B warn
+suppresses informational messages or warnings; errors are always shown.
+The default is
+.IR info,warn,error .
+.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.
@@ -637,18 +835,17 @@ The
database is not particularly robust in the face of whole directory trees
being moved, or changes in the underlying device and inode numbers. It
has a reasonable chance of recovering if only the path or the device numbers
-have changed, but it is not particularly designed to address this. A future
-release is expected to have improved resilience in these cases.
+have changed, but it is not particularly designed to address this.
The filesystem on which
.I pseudo
keeps its database and files must at a minimum support UNIX domain sockets
-and reasonable file locking semantics. Note that
+and
+.I fcntl(2)
+record locks.
.I pseudo
-relies on
-.I flock(2)
-locking semantics; a lock has to persist into a child process. This should
-probably eventually be fixed.
+uses these locks for the server's lock file, and to keep clients from
+changing the same path at the same time.
The
.I pseudo
@@ -662,6 +859,53 @@ than it is otherwise. This is probably because nearly every operation
communication with the server, and probably some kind of database
activity.
+.SH SECURITY
+.I pseudo
+is intended for build environments, where ordinary users produce
+packages and filesystem images containing files that appear to be owned
+by root, device nodes, and special permissions and attributes. It is
+not a security boundary, is not secure by design, and must not be used
+to contain untrusted programs or in security sensitive applications.
+
+.I pseudo
+relies on the cooperation of the programs it runs. It only sees calls
+that go through
+.IR libpseudo.so ,
+so statically linked programs, setuid programs, programs that make
+system calls directly, and programs that remove it from
+.B LD_PRELOAD
+or set
+.B PSEUDO_UNLOAD
+or
+.B PSEUDO_DISABLED
+all reach the real filesystem with the privileges of the user running
+.IR pseudo .
+Its emulated
+.I chroot(2)
+does not confine anything.
+
+The ownership and permissions that programs see exist only in the
+.I pseudo
+database: the files themselves belong to the user running
+.IR pseudo ,
+who can read and write them whatever their emulated owner and mode.
+The database can fall out of step with the filesystem if the tree is
+changed outside of
+.IR pseudo ,
+and
+.I pseudo
+works with paths in ways that are open to races if other processes
+change the tree at the same time. Only use
+.I pseudo
+on trees that are private to the build, and check the ownership,
+permissions and attributes of security relevant files in the result.
+
+See
+.I README.security
+in the
+.I pseudo
+source tree for details.
+
.SH SEE ALSO
fakeroot(1), ld.so(8), pseudolog(1), sqlite3(1)
.SH FURTHER READING
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 28/34] ports/linux: Don't require <linux/mount.h>
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (26 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 27/34] pseudo.1: Update to match the current code Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 29/34] configure: Require GCC 5 or later Mark Hatle
` (5 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
<linux/mount.h> only exists in kernel headers from 5.2, so pseudo no
longer built on older hosts such as CentOS 7. pseudo only needs
OPEN_TREE_CLONE, which already has a fallback, and struct mount_attr,
which is only used through pointers, so declare it instead of including
the header. The forward declaration is also needed without the header,
or each prototype using the struct declares a different type.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/linux/portdefs.h | 7 +++++--
test/test-open_tree_attr-syscall.c | 1 -
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/ports/linux/portdefs.h b/ports/linux/portdefs.h
index 97ddcb6..255b2ec 100644
--- a/ports/linux/portdefs.h
+++ b/ports/linux/portdefs.h
@@ -34,7 +34,6 @@ GLIBC_COMPAT_SYMBOL(memcpy,2.0);
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <sys/mount.h>
-#include <linux/mount.h>
/* close_range()'s flags, and unshare(), are only declared by glibc under
* _GNU_SOURCE, which pseudo does not build with. <linux/close_range.h> is
@@ -93,11 +92,15 @@ extern int unshare(int flags);
#define SYS_open_tree __NR_open_tree
#endif
-/* OPEN_TREE_CLONE may be missing from older host headers.
+/* The new mount API is only in <sys/mount.h> from glibc 2.36, and
+ * <linux/mount.h> is missing from older kernel headers (before 5.2), so
+ * don't rely on either. OPEN_TREE_CLONE is kernel ABI, and struct
+ * mount_attr is only used through pointers, so it can stay incomplete.
*/
#ifndef OPEN_TREE_CLONE
#define OPEN_TREE_CLONE (1U << 0)
#endif
+struct mount_attr;
#ifndef SYS_open_tree_attr
diff --git a/test/test-open_tree_attr-syscall.c b/test/test-open_tree_attr-syscall.c
index e4e652e..aead9d4 100644
--- a/test/test-open_tree_attr-syscall.c
+++ b/test/test-open_tree_attr-syscall.c
@@ -5,7 +5,6 @@
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
-#include <linux/mount.h>
#include <sys/stat.h>
#ifndef SYS_open_tree_attr
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 29/34] configure: Require GCC 5 or later
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (27 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 28/34] ports/linux: Don't require <linux/mount.h> Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 30/34] doc: Update README and doc/ to match the code Mark Hatle
` (4 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
pseudo uses __has_include, which GCC only supports from version 5, so
older compilers such as GCC 4.8 on CentOS 7 failed part way through the
build.
clang reports itself as GCC 4.2.1, so it is only checked for
__has_include.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
README | 3 +++
configure | 25 +++++++++++++++++++++++++
2 files changed, 28 insertions(+)
diff --git a/README b/README
index 7808463..cbefa56 100644
--- a/README
+++ b/README
@@ -14,6 +14,9 @@ things necessary for creating distribution packages or filesystems.
pseudo is not a security boundary and must not be used in security
sensitive applications. See README.security for details.
+Building pseudo requires GCC 5 or later (or clang), which the configure
+script checks for.
+
To configure, run the provided configure script. Note that this is
NOT an autoconf script.
diff --git a/configure b/configure
index e659370..01ecd85 100755
--- a/configure
+++ b/configure
@@ -236,6 +236,31 @@ if [ -z "$opt_sqlite_lib" ]; then
opt_sqlite_lib=$opt_lib
fi
+# pseudo uses __has_include, which needs GCC 5 or later. clang reports
+# itself as GCC 4.2.1, so only check its support for __has_include.
+if ! cc_version=$(${CC:-cc} -dumpversion 2>/dev/null); then
+ echo >&2 "Error: can't run the C compiler '${CC:-cc}'."
+ exit 1
+fi
+cc_check=$(mktemp -d "${TMPDIR:-/tmp}/pseudo-configure.XXXXXX") || exit 1
+cat > "$cc_check/cc_check.c" <<EOF
+#if defined(__clang__)
+# if !defined(__has_include)
+# error clang without __has_include
+# endif
+#elif !defined(__GNUC__) || __GNUC__ < 5
+# error GCC older than 5
+#endif
+int pseudo_cc_check;
+EOF
+if ! cc_check_out=$(${CC:-cc} -c -o "$cc_check/cc_check.o" "$cc_check/cc_check.c" 2>&1); then
+ echo >&2 "Error: pseudo requires GCC 5 or later (${CC:-cc} reports version $cc_version)."
+ echo >&2 "$cc_check_out"
+ rm -rf "$cc_check"
+ exit 1
+fi
+rm -rf "$cc_check"
+
if [ ! -f "${opt_sqlite}/include/sqlite3.h" ]; then
echo >&2 "SQLite3 headers not found in at ${opt_sqlite}/include/sqlite3.h. Please check that SQLite3 and SQLite3 headers are installed."
exit 1
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 30/34] doc: Update README and doc/ to match the code
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (28 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 29/34] configure: Require GCC 5 or later Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 31/34] configure: Fix the --enable-profiling default Mark Hatle
` (3 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Several descriptions had drifted from the implementation: the README
configure option list had duplicates and was missing options, and the
database, IPC, overview, passwd, chroot, utils and program flow notes
described old schemas, message fields, shutdown timing and wrapper
behavior.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
README | 38 ++++++++++++++++++++--------
README.security | 2 +-
doc/chroot | 10 +++++---
doc/database | 75 ++++++++++++++++++++++++++++++++++++++------------------
doc/overview | 21 ++++++++--------
doc/passwd | 7 ++++--
doc/program_flow | 8 +++++-
doc/pseudo_ipc | 37 +++++++++++++++++++---------
doc/utils | 6 ++++-
9 files changed, 139 insertions(+), 65 deletions(-)
diff --git a/README b/README
index cbefa56..caf09b9 100644
--- a/README
+++ b/README
@@ -22,25 +22,43 @@ NOT an autoconf script.
Configure options:
--prefix=/path/to/install/dir
- --with-sqlite=/path/to/sqlite/dir
- --bits={32,64}
- --suffix=<text>
- --enable-static-sqlite
- --with-sqlite-lib
--libdir=...
+ --suffix=<text>
+ --target=<cpu>
--enable-memory-db
+ --enable-xattr
+ --enable-xattrdb
+ --enable-profiling
--enable-force-async
+ --with-sqlite=/path/to/sqlite/dir
--with-sqlite-lib=...
--enable-static-sqlite
--with-static-sqlite=...
--with-rpath=...|--without-rpath
+ --with-passwd-fallback=...|--without-passwd-fallback
--cflags=''
+ --bits={32,64}
+
+The --enable-* options also accept =yes and =no, and --disable-* is the
+same as --enable-*=no.
+
+There is no default prefix. The default for sqlite is /usr. If --bits
+is not given, it is guessed from /bin/sh, falling back to 32. SQLite 3.6
+or later is required.
+
+The --target option sets the CPU the wrappers are generated for (used to
+pick the right syscall numbers); it defaults to the build machine.
+
+Extended attribute support (--enable-xattr) is enabled by default if
+getfattr works on the build machine. --enable-xattrdb stores extended
+attributes in the database rather than on the files, and implies
+--enable-xattr. --enable-profiling builds the pseudo_profile utility and
+the client profiling support.
-There is no default prefix. The default for sqlite is /usr, and for
-bits is 32. You need a reasonably modern sqlite3 -- it has to have
-sqlite3_clear_bindings(), which at least one default sqlite3 install
-did not. (But that was dated 2006, so I'm not sure this will affect
-most readers.)
+--with-passwd-fallback sets the directory whose etc/passwd and etc/group
+are used when no chroot or PSEUDO_PASSWD file is found; it defaults to /
+(that is, the host's /etc). --without-passwd-fallback disables the
+fallback.
The suffix value can be used to append a particular text string to file
names (such as libpseudo<suffix>.so). This was used in the WR environment
diff --git a/README.security b/README.security
index 8817450..45533f2 100644
--- a/README.security
+++ b/README.security
@@ -91,7 +91,7 @@ that protection in several ways:
* Several wrappers check a path and then act on it by path in a second
step. For example, fchmodat (ports/unix/guts/fchmodat.c) stats the
- path and later calls the real chmod on the same path, and mknodat
+ path and later calls the real fchmodat() on the same path, and mknodat
(ports/linux/guts/__xmknodat.c) removes the path again by name if the
database update fails.
diff --git a/doc/chroot b/doc/chroot
index a0d1758..75b1d54 100644
--- a/doc/chroot
+++ b/doc/chroot
@@ -12,15 +12,17 @@ are invoked with absolute, canonical, paths.
A few calls are then modified. For instance, when readlink() returns
a link, it strips a leading prefix looking like the chroot directory;
when symlink() creates a link, if the link path is absolute, the chroot
-directory is prepended to it.
+directory is prepended to it. (Setting PSEUDO_NOSYMLINKEXP to a value
+other than 0 disables this symlink() behavior.)
This behavior, which may seem surprising, is an attempt to produce
path resolution equivalent to what would normally happen when
canonicalizing a path using real chroot; if you create a link to /tmp
while in a chroot environment, following that link should yield
-<chroot>/tmp. Arguably, now that we are intercepting nearly every
-incoming path, we should just prepend the chroot path when following
-absolute symlinks -- this may get revisited.
+<chroot>/tmp. Pseudo's own path canonicalization also resolves absolute
+symlink targets relative to the chroot directory (stripping the chroot
+prefix first if the target already has it), so links created without the
+prefix are still followed correctly inside the chroot.
Similarly, when glob() is called, the path to be globbed has the
chroot directory prepended, and then the results have any leading
diff --git a/doc/database b/doc/database
index 7003aff..924efd3 100644
--- a/doc/database
+++ b/doc/database
@@ -16,13 +16,13 @@ FILES:
rdev (integer)
deleting (integer)
-There are two indexes on the file database, one by path and one by device
-and inode. Earlier versions of pseudo ignored symlinks, but this turned
-out to create problems; specifically, if you had a symlink to a directory,
-and accessed a file through that, it could create unexpected results. Names
-are fully canonicalized by the client, except for functions which would
-operate directly on a symlink, in which case the last path component is not
-replaced.
+There are two indexes on the files table, one on path, device and inode,
+and one on device and inode. Earlier versions of pseudo ignored symlinks,
+but this turned out to create problems; specifically, if you had a symlink
+to a directory, and accessed a file through that, it could create
+unexpected results. Names are fully canonicalized by the client, except
+for functions which would operate directly on a symlink, in which case the
+last path component is not replaced.
It is not an error to have multiple entries with the same device and inode.
Updates to uid, gid, mode, or rdev are applied to every file with the same
@@ -31,21 +31,43 @@ to obtain the device and inode, then modifying all matching records.
If a file shows up with no name (this should VERY rarely happen), it is stored
in the database with the special name 'NAMELESS FILE'. This name can never
-be sent by the client (all names are sent as absolute paths). If a laterThe
+be sent by the client (all names are sent as absolute paths). If a later
request comes in with a valid name, the 'NAMELESS FILE' is renamed to it so
it can be unlinked later.
+XATTRS:
+ id (unique key)
+ dev (integer)
+ ino (integer)
+ name (varchar)
+ value (varchar)
+
+The xattrs table holds extended attributes, keyed by device and inode
+(indexed on dev, ino and on dev, ino, name), so they are shared by every
+name for the same file.
+
+MIGRATIONS:
+ id (unique key)
+ version (integer)
+ stamp (integer, seconds since epoch)
+ sql (varchar)
+
+Both databases have a migrations table recording which schema updates
+have been applied, so an older database can be brought up to date when
+it is opened.
+
The "deleting" field is used to track files which are in the midst of
being possibly-deleted. The issue is that if you issue an unlink database
operation only after removing a file, there is a window during which
another program can genuinely create a new file with the same name or
inode, and that request can reach the daemon before the unlink operation
-does. To address this, three operations are addded: may-unlink, did-unlink,
+does. To address this, three operations are added: may-unlink, did-unlink,
and cancel-unlink. The may-unlink operation tags a file for possible
-deletion, setting "deleting" to 1. A clash involving a file marked for
-deletion is resolved by deleting the existing database entry without
-complaint. A did-unlink operation deletes a file ONLY if it is marked
-for deletion. A cancel-unlink operation unmarks a file for deletion.
+deletion, setting "deleting" to the requesting client's ID. A clash
+involving a file marked for deletion is resolved by deleting the existing
+database entry without complaint. A did-unlink operation deletes a file
+ONLY if it is marked for deletion by the same client. A cancel-unlink
+operation unmarks a file for deletion.
You can still have a race condition here, but it seems a lot less likely.
The failure would be:
@@ -67,12 +89,13 @@ Rename operations use a pair of paths, separated by a null byte; the client
sends the total length of both names (plus the null byte), and the server
knows to split them around the null byte. The impact of a rename on things
contained within a directory is handled in SQL:
- UPDATE files SET path = replace(path, oldpath, newpath) WHERE
- path = oldpath;
- UPDATE files SET path = replace(path, oldpath, newpath) WHERE
- (path > oldpath || '/') && (path < oldpath || '0);
+ UPDATE files SET path = ?, deleting = 0 WHERE path = ?;
+ UPDATE files SET path = replace(path, ?, ?), deleting = 0
+ WHERE (path > (? || '/') AND path < (? || '0'));
+with oldpath and newpath bound to the parameters.
That is to say, anything which either starts with "oldpath/" or is exactly
-equal to oldpath gets renamed, with oldpath replaced by newpath... The
+equal to oldpath gets renamed, with oldpath replaced by newpath, and any
+pending deletion mark is cleared... The
unusual constructions are to address two key issues. One is that an "OR"
would prevent proper use of the index. The other is that a pattern,
such as "LIKE oldpath || '/%'", would prevent use of the index (at least
@@ -83,8 +106,9 @@ after it.
LOGS
id (unique key)
stamp (integer, seconds since epoch)
- operation (id from operations, can be null)
+ op (operation id, can be null)
client (integer identifier)
+ fd (integer)
dev (integer)
ino (integer)
mode (integer)
@@ -93,17 +117,20 @@ LOGS
severity (severity id)
text (anything else you wanted to say)
tag (identifier for operations)
+ uid (integer)
+ gid (integer)
access (integer)
+ program (varchar, name of the client program)
+ type (integer, message type)
The log database contains a primary table (logs). As of this writing it
is not indexed, because indexing is expensive during writes (common, for
the log database) and very few queries are usually run.
-The log database also contains, when created, tables of operations, result
-types, and severities. These exist so that queries can be run against
-a log database even if these values might have changed in a newer build
-of pseudo. The tables of operations and severities are just id->name pairs.
-No enforcement of the relation is currently provided.
+The op, result, severity and type fields hold the numeric values of
+pseudo's internal enumerations; pseudolog translates them back to names.
+No tables of these names are stored in the database, so a log database
+should be read with a matching build of pseudolog.
The log database "tag" field, added since the initial release of pseudo,
is available for tagging operations. When a client connects to the
diff --git a/doc/overview b/doc/overview
index 37d223a..96c883d 100644
--- a/doc/overview
+++ b/doc/overview
@@ -50,16 +50,17 @@ server sends back failure or success notices.
As a fairly typical example, the progress of a stat(2) call is:
-* The __xstat() wrapper is called. This wrapper checks the version argument
- against the _STAT_VER constant in case we some day run into a system where
- programs call stat with different versions of struct stat. (Hasn't happened
- yet.)
-* The __xstat() wrapper calls the __fxstatat() wrapper, which in turn calls
+* The stat() wrapper (or __xstat(), for programs built against older glibc)
+ is called.
+* That wrapper calls the __fxstatat() wrapper, which in turn calls
the __fxstatat64() wrapper (this allows us to have only one copy of the
logic shared among all the path-based stat syscalls).
* The __fxstatat64() wrapper calls the underlying __fxstatat64() function,
which has been mapped to the name real___fxstatat64(). (If this fails,
- the wrapper function returns immediately.)
+ the wrapper function returns immediately.) It then checks the version
+ argument against the _STAT_VER constant, in case we some day run into a
+ system where programs call stat with different versions of struct stat,
+ and returns the real result unmodified if it doesn't match.
* The __fxstatat64() wrapper passes the resulting stat buffer and path to the
client code and asks for a response.
* The client code converts the stat buffer into a pseudo_msg_t message
@@ -87,8 +88,6 @@ as mkstemp, fopen, and freopen. These are wrapped because, in glibc, they
call internal functions which make inline assembly syscalls, rather than
calling the syscall entry points. In each case, the wrapper makes the real
call without intervention, then snoops the results for a file descriptor to
-path mapping. (This would be done to opendir/fdopendir/closedir as well,
-but the DIR * is opaque and can't be snooped practically. This is why
-some versions of 'rm -r' can, at higher diagnostic levels, generate a slew
-of warnings about file descriptors being reopened when no close was
-observed.)
+path mapping. opendir() and closedir() are handled the same way, using
+dirfd() to find the descriptor behind the opaque DIR *. fdopendir() is not
+wrapped; it uses a descriptor pseudo already knows about.
diff --git a/doc/passwd b/doc/passwd
index 42d41c6..18a05d4 100644
--- a/doc/passwd
+++ b/doc/passwd
@@ -5,8 +5,11 @@ possibly pick a password file other than /etc/passwd, and likewise
for setgrent() and /etc/group. The logic is as follows:
* If a chroot directory is set, <chroot>/etc is tried first.
-* If PSEUDO_PASSWD is set, PSEUDO_PASSWD/etc is tried next.
-* Otherwise, fall back on /etc.
+* If PSEUDO_PASSWD is set, PSEUDO_PASSWD/etc is tried next. PSEUDO_PASSWD
+ may be a colon-separated list of directories, which are tried in order.
+* Otherwise, fall back on the directory set at build time with
+ configure's --with-passwd-fallback (default /, so /etc is used). If
+ pseudo was built with --without-passwd-fallback, there is no fallback.
In each case, failure to find a passwd or group file results in
going on to trying the next case.
diff --git a/doc/program_flow b/doc/program_flow
index f7eb26c..4137cf0 100644
--- a/doc/program_flow
+++ b/doc/program_flow
@@ -65,7 +65,13 @@ libpseudo execution flow:
pseudo_sigblock
pseudo_getlock
if antimagic rc = real_*()
- else rc = wrap_*()
+ else
+ fix paths (canonicalize path arguments)
+ if path is ignored rc = real_*()
+ else
+ take path lock (if path_lock is set)
+ rc = wrap_*()
+ drop path lock
variadic end (if necessary)
pseudo_droplock
unmask signals
diff --git a/doc/pseudo_ipc b/doc/pseudo_ipc
index 0892bb7..0d9d6cd 100644
--- a/doc/pseudo_ipc
+++ b/doc/pseudo_ipc
@@ -2,10 +2,11 @@ MESSAGE PASSING
typedef struct {
pseudo_msg_type_t type;
- op_id_t op;
- res_id_t result;
+ pseudo_op_t op;
+ pseudo_res_t result;
int access;
int client;
+ int fd;
dev_t dev;
unsigned long long ino;
uid_t uid;
@@ -14,6 +15,7 @@ typedef struct {
dev_t rdev;
unsigned int pathlen;
int nlink;
+ int deleting;
char path[];
} pseudo_msg_t;
@@ -22,12 +24,14 @@ server. The last field is optional (it's a C99ism called a flexible array
member, allowing a single allocation to hold both the structure and the
variable-length character data at the end).
-All messages contain items up through 'pathlen'. If pathlen is not zero,
-an additional pathlen bytes containing path are provided; path is
-null-terminated.
+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
-never really sends a path, currently, but maybe it will someday. Note that
+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
@@ -36,16 +40,19 @@ 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 takes no response, otherwise the
-server should always send ACK. When run with '-S', the pseudo program
+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.
+
+When run with '-S', the pseudo program
runs as a client, sending a SHUTDOWN message to a server -- but only if
it can find one, it does not start a new one. In this case, the server
could respond with a NAK, in which case it sends a message in which
"path" is a list of space-separated PIDs of currently-living clients,
for the program to print out in an error message. The server will
not shut down while there are living clients. (The request, though,
-causes it to shut down immediately when there are no more clients,
-rather than waiting for the timeout period.)
+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.
@@ -54,9 +61,13 @@ 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
debugging feature.)
+fd is the file descriptor involved in the operation, if any. It is
+recorded in the log database. In a NAK reply to SHUTDOWN, it holds the
+number of other live clients.
+
dev/ino/uid/gid/mode/rdev/path are information about the file. They should
all be provided on send if possible, but the server only generally changes
-uid/gid/mode/rdev on response, and never sends a path back. Dev and inode
+uid/gid/mode/rdev on response. Dev and inode
are currently changed by stat-by-path operations, but this may turn out to
be wrong.
@@ -69,6 +80,10 @@ nlink is used to forward the number of links. The server DOES NOT modify
this. Rather, nlink is used to provide better diagnostics when checking
paths against inodes.
+deleting carries the database's "deleting" mark (see doc/database) when
+the server looks up file entries. The server uses it internally; the
+client doesn't set or use it.
+
32/64 bit: This structure should have the same offsets for every element,
including path, on both 64-bit and 32-bit machines. (Check with 'offsets.c'.)
It is *not* an error if sizeof(pseudo_msg_t) is different; the padding
diff --git a/doc/utils b/doc/utils
index 35520d4..7b18af6 100644
--- a/doc/utils
+++ b/doc/utils
@@ -20,7 +20,7 @@ pseudo
is 30 seconds, or whatever you specified with "-t" or PSEUDO_OPTS
when starting it) or run "pseudo -S". The server will not exit
while clients are active, but requesting a shutdown sets the timeout
- to one second, so it will exit quickly after the last client
+ to 3 seconds, so it will exit quickly after the last client
disconnects.
libpseudo.so
@@ -31,3 +31,7 @@ libpseudo.so
pseudodb
allows browsing and modification of db (not implemented)
+
+pseudo_profile
+ Only built with --enable-profiling. Reads and summarizes the
+ profiling data written by clients when PSEUDO_PROFILE_PATH is set.
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 31/34] configure: Fix the --enable-profiling default
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (29 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 30/34] doc: Update README and doc/ to match the code Mark Hatle
@ 2026-11-11 4:20 ` Mark Hatle
2026-11-11 4:21 ` [pseudo][PATCH 32/34] pseudo_util: Skip unneeded lstat calls when canonicalizing paths Mark Hatle
` (2 subsequent siblings)
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:20 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
The default was assigned to opt_profile, while everything else uses
opt_profiling, so PROFILING was substituted as an empty string unless
the option was given. This only worked because the Makefile tests for
"true".
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
configure | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/configure b/configure
index 01ecd85..89b8f9c 100755
--- a/configure
+++ b/configure
@@ -19,7 +19,7 @@ opt_memory=
opt_async=
opt_xattr=
opt_xattrdb=false
-opt_profile=false
+opt_profiling=false
opt_passwd_fallback='""'
compile_x86_32=-m32
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 32/34] pseudo_util: Skip unneeded lstat calls when canonicalizing paths
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (30 preceding siblings ...)
2026-11-11 4:20 ` [pseudo][PATCH 31/34] configure: Fix the --enable-profiling default Mark Hatle
@ 2026-11-11 4:21 ` Mark Hatle
2026-11-11 4:21 ` [pseudo][PATCH 33/34] wrappers: Don't re-block signals in nested wrapper calls Mark Hatle
2026-11-11 4:21 ` [pseudo][PATCH 34/34] Wrap execvpe(), fexecve() and execveat() Mark Hatle
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:21 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
Path canonicalization called lstat on the base directory and on every
element, including a final element that is not followed, whose result
is never used. On a tar/chown/find/ls/rm workload of 40,000 files
this was 1.43M lstat calls.
Skip the lstat of a final element that is left alone, and only stat
the base when the first element is "", "." or "..", which are the only
cases where it matters whether the base is a directory. For any other
name, lstat of the element fails with ENOTDIR under a non-directory,
giving the same result. This drops the workload to 0.61M lstat calls.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
pseudo_util.c | 40 ++++++++++++++++++++++++++++------------
1 file changed, 28 insertions(+), 12 deletions(-)
diff --git a/pseudo_util.c b/pseudo_util.c
index 1e29ba5..d9312d0 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -726,9 +726,13 @@ pseudo_append_element(char *newpath, char *root, size_t allocated, char **pcurre
/* if we are not on the last element of a path and supposed to leave
* it alone (for SYMLINK_NOFOLLOW type cases), and we are not trying to
* go further under a regular file, we want to know whether this is a symlink.
- * either way, we want to update buf to show the correct state of the file.
+ * otherwise, update buf to show the correct state of the file. An
+ * element we leave alone is always the last one, so nothing uses
+ * its state and we skip the lstat.
*/
- if (!pseudo_real_lstat || (pseudo_real_lstat(newpath, buf) == -1)) {
+ if (leave_this) {
+ buf->st_mode = 0;
+ } else if (!pseudo_real_lstat || (pseudo_real_lstat(newpath, buf) == -1)) {
// if we can't stat it, zero mode so we don't think it's
// known to be a link or a regular file.
buf->st_mode = 0;
@@ -846,6 +850,7 @@ pseudo_append_elements(char *newpath, char *root, size_t allocated, char **curre
/* a shareable buffer so we can cache stat results while walking the path */
PSEUDO_STATBUF buf;
buf.st_mode = 0;
+ int stat_base = 0;
const char * start = path;
if (!newpath || !root ||
!current || !*current ||
@@ -861,16 +866,18 @@ pseudo_append_elements(char *newpath, char *root, size_t allocated, char **curre
* then get shared by anything we call.
*/
sbuf = &buf;
- if (*current > root) {
- if (!pseudo_real_lstat || (pseudo_real_lstat(newpath, sbuf) == -1)) {
- sbuf->st_mode = 0;
- }
- } else {
- /* Don't call lstat on an empty path, or at all when we
- * know that "root" is always directory-like.
- */
- sbuf->st_mode = S_IFDIR;
- }
+ /* Don't call lstat on an empty path, or at all when we
+ * know that "root" is always directory-like.
+ */
+ sbuf->st_mode = S_IFDIR;
+ /* The base is only stat'd if the first element is "", "."
+ * or "..", the only cases where it matters whether the base
+ * is really a directory. For any other name, lstat of the
+ * appended element fails with ENOTDIR under a non-directory,
+ * giving the same result as stat'ing the base.
+ */
+ if (*current > root)
+ stat_base = 1;
}
pseudo_debug(PDBGF_PATH | PDBGF_VERBOSE, "paes: newpath %s, element list <%.*s>\n",
newpath, (int) elen, path);
@@ -889,6 +896,15 @@ pseudo_append_elements(char *newpath, char *root, size_t allocated, char **curre
*/
pseudo_debug(PDBGF_PATH | PDBGF_VERBOSE, "element to add: '%.*s'\n",
(int) this_elen, path);
+ if (stat_base) {
+ stat_base = 0;
+ if (this_elen == 0 || (path[0] == '.' &&
+ (this_elen == 1 || (this_elen == 2 && path[1] == '.')))) {
+ if (!pseudo_real_lstat || (pseudo_real_lstat(newpath, sbuf) == -1)) {
+ sbuf->st_mode = 0;
+ }
+ }
+ }
if (pseudo_append_element(newpath, root, allocated, current, path, this_elen, sbuf, leave_this) == -1) {
retval = -1;
break;
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 33/34] wrappers: Don't re-block signals in nested wrapper calls
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (31 preceding siblings ...)
2026-11-11 4:21 ` [pseudo][PATCH 32/34] pseudo_util: Skip unneeded lstat calls when canonicalizing paths Mark Hatle
@ 2026-11-11 4:21 ` Mark Hatle
2026-11-11 4:21 ` [pseudo][PATCH 34/34] Wrap execvpe(), fexecve() and execveat() Mark Hatle
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:21 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
pseudo's own calls to wrapped functions, such as fcntl() for the path
lock, go through the full wrapper and block and restore signals again,
although the outer wrapper already blocked them. Skip that when this
thread already holds the pseudo lock, and keep the outer wrapper's
saved mask for exec*().
On a tar/chown/find/ls/rm workload of 40,000 files this halves the
rt_sigprocmask calls, from 3.1M to 1.5M.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
doc/program_flow | 4 ++--
pseudo_wrappers.c | 6 ++++++
templates/wrapfuncs.c | 21 ++++++++++++++++-----
3 files changed, 24 insertions(+), 7 deletions(-)
diff --git a/doc/program_flow b/doc/program_flow
index 4137cf0..5fbdb5d 100644
--- a/doc/program_flow
+++ b/doc/program_flow
@@ -62,7 +62,7 @@ libpseudo execution flow:
return enosys
variadic setup (if necessary)
if pseudo_disabled return real_*()
- pseudo_sigblock
+ pseudo_sigblock (unless this thread already holds the lock)
pseudo_getlock
if antimagic rc = real_*()
else
@@ -74,7 +74,7 @@ libpseudo execution flow:
drop path lock
variadic end (if necessary)
pseudo_droplock
- unmask signals
+ unmask signals (if they were blocked above)
return rc
pseudo execution flow:
diff --git a/pseudo_wrappers.c b/pseudo_wrappers.c
index a4e9015..13d7ede 100644
--- a/pseudo_wrappers.c
+++ b/pseudo_wrappers.c
@@ -58,6 +58,7 @@ static pthread_t pseudo_mutex_holder;
static int pseudo_mutex_recursion = 0;
static int pseudo_getlock(void);
static void pseudo_droplock(void);
+static int pseudo_lock_held(void);
static size_t pseudo_dechroot(char *, size_t);
static void pseudo_sigblock(sigset_t *);
@@ -226,6 +227,11 @@ pseudo_getlock(void) {
}
}
+static int
+pseudo_lock_held(void) {
+ return pthread_equal(pseudo_mutex_holder, pthread_self());
+}
+
static void
pseudo_droplock(void) {
if (--pseudo_mutex_recursion == 0) {
diff --git a/templates/wrapfuncs.c b/templates/wrapfuncs.c
index d7fb548..2fa4212 100644
--- a/templates/wrapfuncs.c
+++ b/templates/wrapfuncs.c
@@ -20,6 +20,7 @@ ${maybe_skip}
${type}
${name}(${decl_args}) {
sigset_t saved;
+ int nested;
${variadic_decl}
${rc_decl}
${paths_decl}
@@ -44,11 +45,17 @@ ${maybe_async_skip}
}
pseudo_debug(PDBGF_WRAPPER, "wrapper called: ${name}\n");
- pseudo_sigblock(&saved);
+ /* If this thread already holds the lock, an outer wrapper has
+ * already blocked signals, so don't block and restore them again.
+ */
+ nested = pseudo_lock_held();
+ if (!nested)
+ pseudo_sigblock(&saved);
pseudo_debug(PDBGF_WRAPPER | PDBGF_VERBOSE, "${name} - signals blocked, obtaining lock\n");
if (pseudo_getlock()) {
errno = EBUSY;
- sigprocmask(SIG_SETMASK, &saved, NULL);
+ if (!nested)
+ sigprocmask(SIG_SETMASK, &saved, NULL);
pseudo_debug(PDBGF_WRAPPER, "${name} failed to get lock, giving EBUSY.\n");
PROFILE_DONE;
${def_return}
@@ -66,8 +73,11 @@ ${maybe_async_skip}
pseudo_debug(PDBGF_SYSCALL, "${name} ignored path, calling real syscall.\n");
${rc_assign} (*real_${name})(${call_args});
} else {
- /* exec*() use this to restore the sig mask */
- pseudo_saved_sigmask = saved;
+ /* exec*() use this to restore the sig mask; a
+ * nested call keeps the outer wrapper's mask.
+ */
+ if (!nested)
+ pseudo_saved_sigmask = saved;
${path_lock_begin}${rc_assign} wrap_$name(${call_args});${path_lock_end}
}
}
@@ -76,7 +86,8 @@ quick_exit:
__attribute__((unused));
save_errno = errno;
pseudo_droplock();
- sigprocmask(SIG_SETMASK, &saved, NULL);
+ if (!nested)
+ sigprocmask(SIG_SETMASK, &saved, NULL);
pseudo_debug(PDBGF_WRAPPER | PDBGF_VERBOSE, "${name} - yielded lock, restored signals\n");
/* not strerror(), which can hang on systems using locale data for it */
pseudo_debug(PDBGF_WRAPPER, "wrapper completed: ${name} returns ${rc_format} (errno: %d)\n", ${rc_value}, save_errno);
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [pseudo][PATCH 34/34] Wrap execvpe(), fexecve() and execveat()
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
` (32 preceding siblings ...)
2026-11-11 4:21 ` [pseudo][PATCH 33/34] wrappers: Don't re-block signals in nested wrapper calls Mark Hatle
@ 2026-11-11 4:21 ` Mark Hatle
33 siblings, 0 replies; 35+ messages in thread
From: Mark Hatle @ 2026-11-11 4:21 UTC (permalink / raw)
To: yocto-patches
From: Mark Hatle <mark.hatle@amd.com>
These weren't wrapped, so a program that ran another one through them
with an environment lacking LD_PRELOAD or pseudo's variables, as
env -i does through execve(), ran it outside pseudo: its files were
created and changed as the real user, with nothing recorded.
Wrap them like execve(): restore pseudo's environment, honour
PSEUDO_UNLOAD, and log the exec when the server is logging (execveat()
only for absolute paths or AT_FDCWD). Rename test-execl to test-exec
and extend it to run env through each of them, checking the arguments
get through, that an empty environment gets libpseudo back in
LD_PRELOAD, and that PSEUDO_UNLOAD keeps it out.
AI-Generated: Uses GitHub Copilot
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
ports/common/guts/execveat.c | 29 +++++++++
ports/common/guts/execvpe.c | 29 +++++++++
ports/common/guts/fexecve.c | 21 ++++++
ports/common/pseudo_wrappers.c | 141 +++++++++++++++++++++++++++++++++++++++++
ports/common/wrapfuncs.in | 3 +
test/test-exec.c | 50 +++++++++++++++
test/test-exec.sh | 31 +++++++++
test/test-execl.c | 4 --
test/test-execl.sh | 13 ----
9 files changed, 304 insertions(+), 17 deletions(-)
create mode 100644 ports/common/guts/execveat.c
create mode 100644 ports/common/guts/execvpe.c
create mode 100644 ports/common/guts/fexecve.c
create mode 100644 test/test-exec.c
create mode 100755 test/test-exec.sh
delete mode 100644 test/test-execl.c
delete mode 100755 test/test-execl.sh
diff --git a/ports/common/guts/execveat.c b/ports/common/guts/execveat.c
new file mode 100644
index 0000000..10e0d52
--- /dev/null
+++ b/ports/common/guts/execveat.c
@@ -0,0 +1,29 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * static int
+ * wrap_execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int flags) {
+ * int rc = -1;
+ */
+ char **new_environ;
+
+ /* as for execve(), the path is not canonicalized or redirected
+ * into the chroot environment
+ */
+ if (antimagic == 0 && path && (path[0] == '/' || dirfd == AT_FDCWD)) {
+ const char *path_guess = pseudo_exec_path(path, 0);
+ pseudo_client_op(OP_EXEC, PSA_EXEC, -1, -1, path_guess, 0);
+ }
+
+ new_environ = pseudo_setupenvp(envp);
+ if (pseudo_has_unload(new_environ))
+ new_environ = pseudo_dropenvp(new_environ);
+
+ sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
+ rc = real_execveat(dirfd, path, argv, new_environ, flags);
+
+ pseudo_free_envp(new_environ);
+
+/* return rc;
+ * }
+ */
diff --git a/ports/common/guts/execvpe.c b/ports/common/guts/execvpe.c
new file mode 100644
index 0000000..37e4927
--- /dev/null
+++ b/ports/common/guts/execvpe.c
@@ -0,0 +1,29 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * static int
+ * wrap_execvpe(const char *file, char *const *argv, char *const *envp) {
+ * int rc = -1;
+ */
+ char **new_environ;
+
+ /* as for execvp(), the file is not canonicalized or redirected
+ * into the chroot environment
+ */
+ if (antimagic == 0) {
+ const char *path_guess = pseudo_exec_path(file, 1);
+ pseudo_client_op(OP_EXEC, PSA_EXEC, -1, -1, path_guess, 0);
+ }
+
+ new_environ = pseudo_setupenvp(envp);
+ if (pseudo_has_unload(new_environ))
+ new_environ = pseudo_dropenvp(new_environ);
+
+ sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
+ rc = real_execvpe(file, argv, new_environ);
+
+ pseudo_free_envp(new_environ);
+
+/* return rc;
+ * }
+ */
diff --git a/ports/common/guts/fexecve.c b/ports/common/guts/fexecve.c
new file mode 100644
index 0000000..a0162d3
--- /dev/null
+++ b/ports/common/guts/fexecve.c
@@ -0,0 +1,21 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * static int
+ * wrap_fexecve(int fd, char *const *argv, char *const *envp) {
+ * int rc = -1;
+ */
+ char **new_environ;
+
+ new_environ = pseudo_setupenvp(envp);
+ if (pseudo_has_unload(new_environ))
+ new_environ = pseudo_dropenvp(new_environ);
+
+ sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
+ rc = real_fexecve(fd, argv, new_environ);
+
+ pseudo_free_envp(new_environ);
+
+/* return rc;
+ * }
+ */
diff --git a/ports/common/pseudo_wrappers.c b/ports/common/pseudo_wrappers.c
index 2cbc946..2ad0814 100644
--- a/ports/common/pseudo_wrappers.c
+++ b/ports/common/pseudo_wrappers.c
@@ -331,6 +331,120 @@ execvp(const char *file, char *const *argv) {
return rc;
}
+int
+execvpe(const char *file, char *const *argv, char *const *envp) {
+ sigset_t saved;
+
+ int rc = -1;
+ PROFILE_START;
+
+ if (!pseudo_check_wrappers() || !real_execvpe) {
+ /* rc was initialized to the "failure" value */
+ pseudo_enosys("execvpe");
+ PROFILE_DONE;
+ return rc;
+ }
+
+ pseudo_debug(PDBGF_WRAPPER, "called: execvpe\n");
+ pseudo_sigblock(&saved);
+ if (pseudo_getlock()) {
+ errno = EBUSY;
+ sigprocmask(SIG_SETMASK, &saved, NULL);
+ PROFILE_DONE;
+ return -1;
+ }
+
+ int save_errno;
+
+ /* exec*() use this to restore the sig mask */
+ pseudo_saved_sigmask = saved;
+ rc = wrap_execvpe(file, argv, envp);
+
+ save_errno = errno;
+ pseudo_droplock();
+ sigprocmask(SIG_SETMASK, &saved, NULL);
+ pseudo_debug(PDBGF_WRAPPER, "completed: execvpe\n");
+ errno = save_errno;
+ PROFILE_DONE;
+ return rc;
+}
+
+int
+fexecve(int fd, char *const *argv, char *const *envp) {
+ sigset_t saved;
+
+ int rc = -1;
+ PROFILE_START;
+
+ if (!pseudo_check_wrappers() || !real_fexecve) {
+ /* rc was initialized to the "failure" value */
+ pseudo_enosys("fexecve");
+ PROFILE_DONE;
+ return rc;
+ }
+
+ pseudo_debug(PDBGF_WRAPPER, "called: fexecve\n");
+ pseudo_sigblock(&saved);
+ if (pseudo_getlock()) {
+ errno = EBUSY;
+ sigprocmask(SIG_SETMASK, &saved, NULL);
+ PROFILE_DONE;
+ return -1;
+ }
+
+ int save_errno;
+
+ /* exec*() use this to restore the sig mask */
+ pseudo_saved_sigmask = saved;
+ rc = wrap_fexecve(fd, argv, envp);
+
+ save_errno = errno;
+ pseudo_droplock();
+ sigprocmask(SIG_SETMASK, &saved, NULL);
+ pseudo_debug(PDBGF_WRAPPER, "completed: fexecve\n");
+ errno = save_errno;
+ PROFILE_DONE;
+ return rc;
+}
+
+int
+execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int flags) {
+ sigset_t saved;
+
+ int rc = -1;
+ PROFILE_START;
+
+ if (!pseudo_check_wrappers() || !real_execveat) {
+ /* rc was initialized to the "failure" value */
+ pseudo_enosys("execveat");
+ PROFILE_DONE;
+ return rc;
+ }
+
+ pseudo_debug(PDBGF_WRAPPER, "called: execveat\n");
+ pseudo_sigblock(&saved);
+ if (pseudo_getlock()) {
+ errno = EBUSY;
+ sigprocmask(SIG_SETMASK, &saved, NULL);
+ PROFILE_DONE;
+ return -1;
+ }
+
+ int save_errno;
+
+ /* exec*() use this to restore the sig mask */
+ pseudo_saved_sigmask = saved;
+ rc = wrap_execveat(dirfd, path, argv, envp, flags);
+
+ save_errno = errno;
+ pseudo_droplock();
+ sigprocmask(SIG_SETMASK, &saved, NULL);
+ pseudo_debug(PDBGF_WRAPPER, "completed: execveat\n");
+ errno = save_errno;
+ PROFILE_DONE;
+ return rc;
+}
+
/* no profiling in fork because it wouldn't work anyway
* half the time
*/
@@ -403,6 +517,33 @@ wrap_execvp(const char *file, char *const *argv) {
}
static int
+wrap_execvpe(const char *file, char *const *argv, char *const *envp) {
+ int rc = -1;
+
+#include "guts/execvpe.c"
+
+ return rc;
+}
+
+static int
+wrap_fexecve(int fd, char *const *argv, char *const *envp) {
+ int rc = -1;
+
+#include "guts/fexecve.c"
+
+ return rc;
+}
+
+static int
+wrap_execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int flags) {
+ int rc = -1;
+
+#include "guts/execveat.c"
+
+ return rc;
+}
+
+static int
wrap_fork(void) {
int rc = -1;
diff --git a/ports/common/wrapfuncs.in b/ports/common/wrapfuncs.in
index 9a44ac1..9b01bf2 100644
--- a/ports/common/wrapfuncs.in
+++ b/ports/common/wrapfuncs.in
@@ -7,3 +7,6 @@ int execvp(const char *file, char *const *argv); /* hand_wrapped=1 */
int fork(void); /* hand_wrapped=1 */
int posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const *argv, char *const *envp); /* hand_wrapped=1 */
int posix_spawnp(pid_t *pid, const char *file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const *argv, char *const *envp); /* hand_wrapped=1 */
+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 */
diff --git a/test/test-exec.c b/test/test-exec.c
new file mode 100644
index 0000000..82ceddf
--- /dev/null
+++ b/test/test-exec.c
@@ -0,0 +1,50 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * Run "/usr/bin/env A=A B=B C=C" through an exec function.
+ *
+ * usage: test-exec [execl|execvpe|fexecve|execveat [VAR=value...]]
+ *
+ * execl() keeps the caller's environment; the others pass only the
+ * VAR=value arguments given, so pseudo has to add its own variables back
+ * (or leave them out, if PSEUDO_UNLOAD is among them).
+ */
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+int main(int argc, char **argv) {
+ char *args[] = { "env", "A=A", "B=B", "C=C", NULL };
+ /* argv[2] onwards, followed by argv's terminating NULL */
+ char **envp = argc > 2 ? argv + 2 : argv + argc;
+ const char *fn = argc > 1 ? argv[1] : "execl";
+ int fd;
+
+ if (!strcmp(fn, "execl")) {
+ return execl("/usr/bin/env", "/usr/bin/env", "A=A", "B=B", "C=C", NULL);
+ } else if (!strcmp(fn, "execvpe")) {
+ /* searched for in the caller's PATH */
+ execvpe("env", args, envp);
+ } else if (!strcmp(fn, "fexecve")) {
+ fd = open("/usr/bin/env", O_RDONLY | O_CLOEXEC);
+ if (fd == -1) {
+ perror("/usr/bin/env");
+ return 2;
+ }
+ fexecve(fd, args, envp);
+ } else if (!strcmp(fn, "execveat")) {
+ fd = open("/usr/bin", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd == -1) {
+ perror("/usr/bin");
+ return 2;
+ }
+ execveat(fd, "env", args, envp, 0);
+ } else {
+ fprintf(stderr, "unknown function '%s'\n", fn);
+ return 2;
+ }
+ perror(fn);
+ return 2;
+}
diff --git a/test/test-exec.sh b/test/test-exec.sh
new file mode 100755
index 0000000..4308631
--- /dev/null
+++ b/test/test-exec.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Run env through each exec function pseudo wraps. The arguments must
+# get through, and for the functions given an explicit environment, an
+# empty one must get libpseudo back in LD_PRELOAD (as with env -i), and
+# one with PSEUDO_UNLOAD must not.
+
+test_exec="$(dirname "$0")/test-exec"
+
+"$test_exec" | grep -q "C=C" || { echo "FAILED: execl"; exit 1; }
+
+rc=0
+for fn in execvpe fexecve execveat; do
+ out=$("$test_exec" $fn)
+ if ! grep -q "C=C" <<< "$out"; then
+ echo "FAILED: $fn: arguments not passed"
+ rc=1
+ fi
+ if ! grep -q "^LD_PRELOAD=.*libpseudo" <<< "$out"; then
+ echo "FAILED: $fn: pseudo not restored to LD_PRELOAD"
+ rc=1
+ fi
+ out=$("$test_exec" $fn PSEUDO_UNLOAD=1)
+ if grep -q "^LD_PRELOAD=.*libpseudo" <<< "$out"; then
+ echo "FAILED: $fn: PSEUDO_UNLOAD did not drop pseudo"
+ rc=1
+ fi
+done
+exit $rc
diff --git a/test/test-execl.c b/test/test-execl.c
deleted file mode 100644
index 31b803c..0000000
--- a/test/test-execl.c
+++ /dev/null
@@ -1,4 +0,0 @@
-#include <unistd.h>
-int main() {
- return execl("/usr/bin/env", "/usr/bin/env", "A=A", "B=B", "C=C", NULL);
-}
diff --git a/test/test-execl.sh b/test/test-execl.sh
deleted file mode 100755
index 8715822..0000000
--- a/test/test-execl.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-#
-# SPDX-License-Identifier: LGPL-2.1-only
-#
-$(dirname "$0")/test-execl | grep -q "C=C"
-
-if [ "$?" = "0" ]
-then
- #echo "Passed."
- exit 0
-fi
-#echo "Failed"
-exit 1
--
1.8.3.1
^ permalink raw reply related [flat|nested] 35+ messages in thread
end of thread, other threads:[~2026-11-11 4:21 UTC | newest]
Thread overview: 35+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-11 4:20 [pseudo][PATCH 00/34] Various fixes Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 01/34] pseudo_client: serialize path changes across clients Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 02/34] ports: hold the path lock in create and metadata wrappers Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 03/34] ports: check created files under the path lock before recording them Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 04/34] test-parallel-ops: Add a parallel create/rename/link/chmod test Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 05/34] ports: Remove PSEUDO_NO_REAL_AT_FUNCTIONS Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 06/34] ports/unix: Remove guts overridden by the Linux port Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 07/34] ports: Let the port define how pseudo stats files Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 08/34] patches/yocto-older-glibc-symbols.patch: Update to latest YP version Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 09/34] Remove unused #if 0 debug code Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 10/34] Replace #if 0 strerror() debug messages with a comment Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 11/34] pseudo_util: Remove stale with_libpseudo() comment Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 12/34] ports/common/subports: Show the unknown OS name in the error Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 13/34] ports/linux: Drop redundant PSEUDO_LINK_SYMLINK_BEHAVIOR override Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 14/34] pseudo_server: Skip events for clients closed earlier in the batch Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 15/34] pseudo_server: Drop an idle client when out of file descriptors Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 16/34] pseudo_server: Always use epoll Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 17/34] configure: Fix --disable-* options Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 18/34] pseudo_server: Lower highest_client when the highest client closes Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 19/34] pseudo_server: Set highest_client to the new client when growing Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 20/34] pseudo_util: Store PSEUDO_SEVERITY as severity names Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 21/34] pseudo: Fix the -k severity option Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 22/34] pseudo_util: Make -v and numeric PSEUDO_DEBUG add debug flags Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 23/34] AGENTS.md: Add to assist future developers Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 24/34] pseudo_util: Fix the date format in the event log dump Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 25/34] pseudo: List -p in the usage message Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 26/34] pseudo_util: Write the event log to PSEUDO_EVLOG_FILE Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 27/34] pseudo.1: Update to match the current code Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 28/34] ports/linux: Don't require <linux/mount.h> Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 29/34] configure: Require GCC 5 or later Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 30/34] doc: Update README and doc/ to match the code Mark Hatle
2026-11-11 4:20 ` [pseudo][PATCH 31/34] configure: Fix the --enable-profiling default Mark Hatle
2026-11-11 4:21 ` [pseudo][PATCH 32/34] pseudo_util: Skip unneeded lstat calls when canonicalizing paths Mark Hatle
2026-11-11 4:21 ` [pseudo][PATCH 33/34] wrappers: Don't re-block signals in nested wrapper calls Mark Hatle
2026-11-11 4:21 ` [pseudo][PATCH 34/34] Wrap execvpe(), fexecve() and execveat() Mark Hatle
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.