diff --git a/ports/unix/guts/fts_children.c b/ports/unix/guts/fts_children.c
new file mode 100644
index 0000000..f5d1890
--- /dev/null
+++ b/ports/unix/guts/fts_children.c
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2026 Yocto Project; see
+ * guts/COPYRIGHT for information.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * static FTSENT *
+ * wrap_fts_children(FTS *ftsp, int options) {
+ *	FTSENT * rc = NULL;
+ */
+
+	rc = real_fts_children(ftsp, options);
+
+	/* glibc's fts_children calls fstatat internally using glibc-private
+	 * symbols, which bypass pseudo's LD_PRELOAD wrappers. We need to
+	 * re-stat each entry through pseudo so that pseudo-tracked
+	 * ownership and permissions are visible to callers.
+	 */
+	FTSENT *p;
+	for (p = rc; p != NULL; p = p->fts_link) {
+		if (p->fts_statp && p->fts_path) {
+			pseudo_msg_t *msg;
+			int save_errno = errno;
+			const char *fts_rpath;
+			PSEUDO_STATBUF buf64;
+
+			switch (p->fts_info) {
+			case FTS_F:
+			case FTS_D:
+			case FTS_SL:
+			case FTS_SLNONE:
+			case FTS_DEFAULT:
+				/* See fts_read.c: fts_path may already contain
+				 * the chroot prefix from fts_open's path resolution.
+				 * Use it directly in that case.
+				 */
+				if (p->fts_path[0] == '/' && pseudo_chroot_len &&
+				    !memcmp(p->fts_path, pseudo_chroot, pseudo_chroot_len) &&
+				    (p->fts_path[pseudo_chroot_len] == '/' ||
+				     p->fts_path[pseudo_chroot_len] == '\0')) {
+					fts_rpath = p->fts_path;
+				} else {
+					fts_rpath = PSEUDO_ROOT_PATH(AT_FDCWD, p->fts_path, AT_SYMLINK_NOFOLLOW);
+				}
+				if (fts_rpath) {
+					pseudo_stat64_from32(&buf64, p->fts_statp);
+					msg = pseudo_client_op(OP_STAT, 0, -1, -1, fts_rpath, &buf64);
+					if (msg && msg->result == RESULT_SUCCEED) {
+						pseudo_stat_msg(&buf64, msg);
+						pseudo_stat32_from64(p->fts_statp, &buf64);
+					}
+				}
+				break;
+			default:
+				break;
+			}
+
+			errno = save_errno;
+		}
+	}
+
+/*	return rc;
+ * }
+ */
diff --git a/ports/unix/guts/fts_read.c b/ports/unix/guts/fts_read.c
new file mode 100644
index 0000000..654b982
--- /dev/null
+++ b/ports/unix/guts/fts_read.c
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2026 Yocto Project; see
+ * guts/COPYRIGHT for information.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * static FTSENT *
+ * wrap_fts_read(FTS *ftsp) {
+ *	FTSENT * rc = NULL;
+ */
+
+	rc = real_fts_read(ftsp);
+
+	/* glibc's fts_read calls fstatat internally using glibc-private
+	 * symbols, which bypass pseudo's LD_PRELOAD wrappers. We need to
+	 * re-stat the entry through pseudo so that pseudo-tracked
+	 * ownership and permissions are visible to callers.
+	 */
+	if (rc && rc->fts_statp && rc->fts_accpath) {
+		pseudo_msg_t *msg;
+		int save_errno = errno;
+		const char *fts_rpath;
+		PSEUDO_STATBUF buf64;
+
+		switch (rc->fts_info) {
+		case FTS_F:
+		case FTS_D:
+		case FTS_DP:
+		case FTS_SL:
+		case FTS_SLNONE:
+		case FTS_DEFAULT:
+			/* fts_open passes real (chroot-resolved) paths to real_fts_open,
+			 * so fts_path is already a real filesystem path. We must use
+			 * it directly without PSEUDO_ROOT_PATH, which would
+			 * incorrectly prepend the chroot prefix a second time.
+			 *
+			 * However, for the non-chroot case (or when fts was given
+			 * relative paths that are still relative in fts_path),
+			 * we still need PSEUDO_ROOT_PATH to make them absolute.
+			 */
+			if (rc->fts_path[0] == '/' && pseudo_chroot_len &&
+			    !memcmp(rc->fts_path, pseudo_chroot, pseudo_chroot_len) &&
+			    (rc->fts_path[pseudo_chroot_len] == '/' ||
+			     rc->fts_path[pseudo_chroot_len] == '\0')) {
+				/* Already a real path with chroot prefix */
+				fts_rpath = rc->fts_path;
+			} else {
+				fts_rpath = PSEUDO_ROOT_PATH(AT_FDCWD, rc->fts_path, AT_SYMLINK_NOFOLLOW);
+			}
+			if (fts_rpath) {
+				pseudo_stat64_from32(&buf64, rc->fts_statp);
+				msg = pseudo_client_op(OP_STAT, 0, -1, -1, fts_rpath, &buf64);
+				if (msg && msg->result == RESULT_SUCCEED) {
+					pseudo_stat_msg(&buf64, msg);
+					pseudo_stat32_from64(rc->fts_statp, &buf64);
+				}
+			}
+			break;
+		default:
+			break;
+		}
+
+		errno = save_errno;
+	}
+
+/*	return rc;
+ * }
+ */
diff --git a/ports/unix/wrapfuncs.in b/ports/unix/wrapfuncs.in
index 7724fc7..5665735 100644
--- a/ports/unix/wrapfuncs.in
+++ b/ports/unix/wrapfuncs.in
@@ -13,6 +13,8 @@ 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 **)); /* inode64=1 */
+FTSENT *fts_read(FTS *ftsp); /* noignore_path=1, inode64=1 */
+FTSENT *fts_children(FTS *ftsp, int options); /* noignore_path=1, inode64=1 */
 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);
