diff --git a/fs/pkernfs/Makefile b/fs/pkernfs/Makefile
index 17258cb77f58..0a66e98bda07 100644
--- a/fs/pkernfs/Makefile
+++ b/fs/pkernfs/Makefile
@@ -3,4 +3,4 @@
 # Makefile for persistent kernel filesystem
 #
 
-obj-$(CONFIG_PKERNFS_FS) += pkernfs.o
+obj-$(CONFIG_PKERNFS_FS) += pkernfs.o inode.o dir.o
diff --git a/fs/pkernfs/dir.c b/fs/pkernfs/dir.c
new file mode 100644
index 000000000000..b10ce745f19d
--- /dev/null
+++ b/fs/pkernfs/dir.c
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include "pkernfs.h"
+
+static int pkernfs_dir_iterate(struct file *dir, struct dir_context *ctx)
+{
+	struct pkernfs_inode *pkernfs_inode;
+	struct super_block *sb = dir->f_inode->i_sb;
+
+	/* Indication from previous invoke that there's no more to iterate. */
+	if (ctx->pos == -1)
+		return 0;
+
+	if (!dir_emit_dots(dir, ctx))
+		return 0;
+
+	/*
+	 * Just emitted this dir; go to dir contents. Use pos to smuggle
+	 * the next inode number to emit across iterations.
+	 * -1 indicates no valid inode. Can't use 0 because first loop has pos=0
+	 */
+	if (ctx->pos == 2) {
+		ctx->pos = pkernfs_get_persisted_inode(sb, dir->f_inode->i_ino)->child_ino;
+		/* Empty dir case. */
+		if (ctx->pos == 0)
+			ctx->pos = -1;
+	}
+
+	while (ctx->pos > 1) {
+		pkernfs_inode = pkernfs_get_persisted_inode(sb, ctx->pos);
+		dir_emit(ctx, pkernfs_inode->filename, PKERNFS_FILENAME_LEN,
+				ctx->pos, DT_UNKNOWN);
+		ctx->pos = pkernfs_inode->sibling_ino;
+		if (!ctx->pos)
+			ctx->pos = -1;
+	}
+	return 0;
+}
+
+const struct file_operations pkernfs_dir_fops = {
+	.owner = THIS_MODULE,
+	.iterate_shared = pkernfs_dir_iterate,
+};
diff --git a/fs/pkernfs/inode.c b/fs/pkernfs/inode.c
new file mode 100644
index 000000000000..f6584c8b8804
--- /dev/null
+++ b/fs/pkernfs/inode.c
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include "pkernfs.h"
+#include <linux/fs.h>
+
+const struct inode_operations pkernfs_dir_inode_operations;
+
+struct pkernfs_inode *pkernfs_get_persisted_inode(struct super_block *sb, int ino)
+{
+	/*
+	 * Inode index starts at 1, so -1 to get memory index.
+	 */
+	return ((struct pkernfs_inode *) (pkernfs_mem + PMD_SIZE)) + ino - 1;
+}
+
+struct inode *pkernfs_inode_get(struct super_block *sb, unsigned long ino)
+{
+	struct inode *inode = iget_locked(sb, ino);
+
+	/* If this inode is cached it is already populated; just return */
+	if (!(inode->i_state & I_NEW))
+		return inode;
+	inode->i_op = &pkernfs_dir_inode_operations;
+	inode->i_sb = sb;
+	inode->i_mode = S_IFREG;
+	unlock_new_inode(inode);
+	return inode;
+}
+
+static unsigned long pkernfs_allocate_inode(struct super_block *sb)
+{
+
+	unsigned long next_free_ino;
+	struct pkernfs_sb *psb = (struct pkernfs_sb *) pkernfs_mem;
+
+	next_free_ino = psb->next_free_ino;
+	if (!next_free_ino)
+		return -ENOMEM;
+	psb->next_free_ino =
+		pkernfs_get_persisted_inode(sb, next_free_ino)->sibling_ino;
+	return next_free_ino;
+}
+
+/*
+ * Zeroes the inode and makes it the head of the free list.
+ */
+static void pkernfs_free_inode(struct super_block *sb, unsigned long ino)
+{
+	struct pkernfs_sb *psb = (struct pkernfs_sb *) pkernfs_mem;
+	struct pkernfs_inode *inode = pkernfs_get_persisted_inode(sb, ino);
+
+	memset(inode, 0, sizeof(struct pkernfs_inode));
+	inode->sibling_ino = psb->next_free_ino;
+	psb->next_free_ino = ino;
+}
+
+void pkernfs_initialise_inode_store(struct super_block *sb)
+{
+	/* Inode store is a PMD sized (ie: 2 MiB) page */
+	memset(pkernfs_get_persisted_inode(sb, 1), 0, PMD_SIZE);
+	/* Point each inode for the next one; linked-list initialisation. */
+	for (unsigned long ino = 2; ino * sizeof(struct pkernfs_inode) < PMD_SIZE; ino++)
+		pkernfs_get_persisted_inode(sb, ino - 1)->sibling_ino = ino;
+}
+
+static int pkernfs_create(struct mnt_idmap *id, struct inode *dir,
+			  struct dentry *dentry, umode_t mode, bool excl)
+{
+	unsigned long free_inode;
+	struct pkernfs_inode *pkernfs_inode;
+	struct inode *vfs_inode;
+
+	free_inode = pkernfs_allocate_inode(dir->i_sb);
+	if (free_inode <= 0)
+		return -ENOMEM;
+
+	pkernfs_inode = pkernfs_get_persisted_inode(dir->i_sb, free_inode);
+	pkernfs_inode->sibling_ino = pkernfs_get_persisted_inode(dir->i_sb, dir->i_ino)->child_ino;
+	pkernfs_get_persisted_inode(dir->i_sb, dir->i_ino)->child_ino = free_inode;
+	strscpy(pkernfs_inode->filename, dentry->d_name.name, PKERNFS_FILENAME_LEN);
+	pkernfs_inode->flags = PKERNFS_INODE_FLAG_FILE;
+
+	vfs_inode = pkernfs_inode_get(dir->i_sb, free_inode);
+	d_instantiate(dentry, vfs_inode);
+	return 0;
+}
+
+static struct dentry *pkernfs_lookup(struct inode *dir,
+		struct dentry *dentry,
+		unsigned int flags)
+{
+	struct pkernfs_inode *pkernfs_inode;
+	unsigned long ino;
+
+	pkernfs_inode = pkernfs_get_persisted_inode(dir->i_sb, dir->i_ino);
+	ino = pkernfs_inode->child_ino;
+	while (ino) {
+		pkernfs_inode = pkernfs_get_persisted_inode(dir->i_sb, ino);
+		if (!strncmp(pkernfs_inode->filename, dentry->d_name.name, PKERNFS_FILENAME_LEN)) {
+			d_add(dentry, pkernfs_inode_get(dir->i_sb, ino));
+			break;
+		}
+		ino = pkernfs_inode->sibling_ino;
+	}
+	return NULL;
+}
+
+static int pkernfs_unlink(struct inode *dir, struct dentry *dentry)
+{
+	unsigned long ino;
+	struct pkernfs_inode *inode;
+
+	ino = pkernfs_get_persisted_inode(dir->i_sb, dir->i_ino)->child_ino;
+
+	/* Special case for first file in dir */
+	if (ino == dentry->d_inode->i_ino) {
+		pkernfs_get_persisted_inode(dir->i_sb, dir->i_ino)->child_ino =
+			pkernfs_get_persisted_inode(dir->i_sb, dentry->d_inode->i_ino)->sibling_ino;
+		pkernfs_free_inode(dir->i_sb, ino);
+		return 0;
+	}
+
+	/*
+	 * Although we know exactly the inode to free, because we maintain only
+	 * a singly linked list we need to scan for it to find the previous
+	 * element so it's "next" pointer can be updated.
+	 */
+	while (ino) {
+		inode = pkernfs_get_persisted_inode(dir->i_sb, ino);
+		/* We've found the one pointing to the one we want to delete */
+		if (inode->sibling_ino == dentry->d_inode->i_ino) {
+			inode->sibling_ino =
+				pkernfs_get_persisted_inode(dir->i_sb,
+						dentry->d_inode->i_ino)->sibling_ino;
+			pkernfs_free_inode(dir->i_sb, dentry->d_inode->i_ino);
+			break;
+		}
+		ino = pkernfs_get_persisted_inode(dir->i_sb, ino)->sibling_ino;
+	}
+
+	return 0;
+}
+
+const struct inode_operations pkernfs_dir_inode_operations = {
+	.create		= pkernfs_create,
+	.lookup		= pkernfs_lookup,
+	.unlink		= pkernfs_unlink,
+};
diff --git a/fs/pkernfs/pkernfs.c b/fs/pkernfs/pkernfs.c
index 4c476ddc35b6..518c610e3877 100644
--- a/fs/pkernfs/pkernfs.c
+++ b/fs/pkernfs/pkernfs.c
@@ -8,7 +8,7 @@
 #include <linux/io.h>
 
 static phys_addr_t pkernfs_base, pkernfs_size;
-static void *pkernfs_mem;
+void *pkernfs_mem;
 static const struct super_operations pkernfs_super_ops = { };
 
 static int pkernfs_fill_super(struct super_block *sb, struct fs_context *fc)
@@ -24,23 +24,26 @@ static int pkernfs_fill_super(struct super_block *sb, struct fs_context *fc)
 		pr_info("pkernfs: Restoring from super block\n");
 	} else {
 		pr_info("pkernfs: Clean super block; initialising\n");
+		pkernfs_initialise_inode_store(sb);
 		psb->magic_number = PKERNFS_MAGIC_NUMBER;
+		pkernfs_get_persisted_inode(sb, 1)->flags = PKERNFS_INODE_FLAG_DIR;
+		strscpy(pkernfs_get_persisted_inode(sb, 1)->filename, ".", PKERNFS_FILENAME_LEN);
+		psb->next_free_ino = 2;
 	}
 
 	sb->s_op = &pkernfs_super_ops;
 
-	inode = new_inode(sb);
+	inode = pkernfs_inode_get(sb, 1);
 	if (!inode)
 		return -ENOMEM;
 
-	inode->i_ino = 1;
 	inode->i_mode = S_IFDIR;
-	inode->i_op = &simple_dir_inode_operations;
-	inode->i_fop = &simple_dir_operations;
+	inode->i_fop = &pkernfs_dir_fops;
 	inode->i_atime = inode->i_mtime = current_time(inode);
 	inode_set_ctime_current(inode);
 	/* directory inodes start off with i_nlink == 2 (for "." entry) */
 	inc_nlink(inode);
+	inode_init_owner(&nop_mnt_idmap, inode, NULL, inode->i_mode);
 
 	dentry = d_make_root(inode);
 	if (!dentry)
diff --git a/fs/pkernfs/pkernfs.h b/fs/pkernfs/pkernfs.h
index bd1e2a6fd336..192e089b3151 100644
--- a/fs/pkernfs/pkernfs.h
+++ b/fs/pkernfs/pkernfs.h
@@ -1,6 +1,40 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 
+#include <linux/fs.h>
+
 #define PKERNFS_MAGIC_NUMBER 0x706b65726e6673
+#define PKERNFS_FILENAME_LEN 255
+
+extern void *pkernfs_mem;
+
 struct pkernfs_sb {
 	unsigned long magic_number;
+	/* Inode number */
+	unsigned long next_free_ino;
 };
+
+// If neither of these are set the inode is not in use.
+#define PKERNFS_INODE_FLAG_FILE (1 << 0)
+#define PKERNFS_INODE_FLAG_DIR (1 << 1)
+struct pkernfs_inode {
+	int flags;
+	/*
+	 * Points to next inode in the same directory, or
+	 * 0 if last file in directory.
+	 */
+	unsigned long sibling_ino;
+	/*
+	 * If this inode is a directory, this points to the
+	 * first inode *in* that directory.
+	 */
+	unsigned long child_ino;
+	char filename[PKERNFS_FILENAME_LEN];
+	int mappings_block;
+	int num_mappings;
+};
+
+void pkernfs_initialise_inode_store(struct super_block *sb);
+struct inode *pkernfs_inode_get(struct super_block *sb, unsigned long ino);
+struct pkernfs_inode *pkernfs_get_persisted_inode(struct super_block *sb, int ino);
+
+extern const struct file_operations pkernfs_dir_fops;
