diff --git a/xen/arch/arm/traps.c b/xen/arch/arm/traps.c
index a3b961bd06..44b027951f 100644
--- a/xen/arch/arm/traps.c
+++ b/xen/arch/arm/traps.c
@@ -1401,6 +1401,7 @@ static arm_hypercall_t arm_hypercall_table[] = {
 #ifdef CONFIG_ARGO
     HYPERCALL(argo_op, 5),
 #endif
+    HYPERCALL(filesystem_op, 5),
 };
 
 #ifndef NDEBUG
diff --git a/xen/arch/x86/hvm/hypercall.c b/xen/arch/x86/hvm/hypercall.c
index 33dd2d99d2..936ad10add 100644
--- a/xen/arch/x86/hvm/hypercall.c
+++ b/xen/arch/x86/hvm/hypercall.c
@@ -144,6 +144,7 @@ static const hypercall_table_t hvm_hypercall_table[] = {
 #endif
     HYPERCALL(xenpmu_op),
     COMPAT_CALL(dm_op),
+    HYPERCALL(filesystem_op),
     HYPERCALL(arch_1)
 };
 
diff --git a/xen/arch/x86/hypercall.c b/xen/arch/x86/hypercall.c
index d483dbaa6b..640347e514 100644
--- a/xen/arch/x86/hypercall.c
+++ b/xen/arch/x86/hypercall.c
@@ -73,6 +73,7 @@ const hypercall_args_t hypercall_args_table[NR_hypercalls] =
     ARGS(hvm_op, 2),
     ARGS(dm_op, 3),
 #endif
+    ARGS(filesystem_op, 5),
     ARGS(mca, 1),
     ARGS(arch_1, 1),
 };
diff --git a/xen/arch/x86/pv/hypercall.c b/xen/arch/x86/pv/hypercall.c
index 0c84c0b3a0..27c8e893c6 100644
--- a/xen/arch/x86/pv/hypercall.c
+++ b/xen/arch/x86/pv/hypercall.c
@@ -84,6 +84,7 @@ const hypercall_table_t pv_hypercall_table[] = {
     HYPERCALL(hvm_op),
     COMPAT_CALL(dm_op),
 #endif
+    HYPERCALL(filesystem_op),
     HYPERCALL(mca),
     HYPERCALL(arch_1),
 };
diff --git a/xen/common/Makefile b/xen/common/Makefile
index 62b34e69e9..be43757dff 100644
--- a/xen/common/Makefile
+++ b/xen/common/Makefile
@@ -11,6 +11,7 @@ obj-y += domain.o
 obj-y += event_2l.o
 obj-y += event_channel.o
 obj-y += event_fifo.o
+obj-y += filesystem.o
 obj-$(CONFIG_CRASH_DEBUG) += gdbstub.o
 obj-$(CONFIG_GRANT_TABLE) += grant_table.o
 obj-y += guestcopy.o
diff --git a/xen/common/filesystem.c b/xen/common/filesystem.c
new file mode 100644
index 0000000000..d833992ab1
--- /dev/null
+++ b/xen/common/filesystem.c
@@ -0,0 +1,270 @@
+/******************************************************************************
+ *
+ * filesystem.c
+ *
+ * Simple sysfs-like file system for the hypervisor.
+ */
+
+#include <xen/lib.h>
+#include <xen/filesystem.h>
+#include <xen/guest_access.h>
+#include <xen/hypercall.h>
+#include <public/filesystem.h>
+
+static DEFINE_SPINLOCK(fs_lock);
+
+struct fs_dir fs_root = {
+    .list = LIST_HEAD_INIT(fs_root.list),
+};
+
+static struct fs_entry fs_root_entry = {
+    .type = fs_type_dir,
+    .name = "",
+    .list = LIST_HEAD_INIT(fs_root_entry.list),
+    .parent = &fs_root,
+    .dir = &fs_root,
+};
+
+static int fs_add_entry(struct fs_dir *parent, struct fs_entry *new)
+{
+    int ret = -ENOENT;
+    struct list_head *l;
+
+    if ( !new->content )
+        return -EINVAL;
+
+    spin_lock(&fs_lock);
+
+    list_for_each ( l, &parent->list )
+    {
+        struct fs_entry *e = list_entry(l, struct fs_entry, list);
+        int cmp = strcmp(e->name, new->name);
+
+        if ( cmp < 0 )
+        {
+            ret = 0;
+            list_add_tail(&new->list, l);
+            break;
+        }
+        if ( cmp == 0 )
+        {
+            ret = -EEXIST;
+            break;
+        }
+    }
+
+    if ( ret == -ENOENT )
+    {
+        ret = 0;
+        list_add_tail(&new->list, &parent->list);
+    }
+
+    if ( !ret )
+    {
+        unsigned int sz = strlen(new->name) + 1;
+
+        parent->content_size += sizeof(struct xen_fs_direntry) + ROUNDUP(sz, 4);
+        new->parent = parent;
+    }
+
+    spin_unlock(&fs_lock);
+
+    return ret;
+}
+
+int fs_new_entry_any(struct fs_dir *parent, const char *name,
+                     enum fs_entry_type type, void *content)
+{
+    int ret = -ENOMEM;
+    struct fs_entry *new = xzalloc(struct fs_entry);
+
+    if ( !new )
+        return ret;
+
+    new->name = name;
+    new->type = type;
+    new->content = content;
+
+    ret = fs_add_entry(parent, new);
+
+    if ( ret )
+        xfree(new);
+
+    return ret;
+}
+
+int fs_new_entry(struct fs_dir *parent, const char *name, const char *val)
+{
+    return fs_new_entry_any(parent, name, fs_type_string, (void *)val);
+}
+
+int fs_new_dir(struct fs_dir *parent, const char *name, struct fs_dir *dir)
+{
+    if ( !dir )
+        dir = xzalloc(struct fs_dir);
+
+    return fs_new_entry_any(parent, name, fs_type_dir, dir);
+}
+
+static int fs_get_path_user(char *buf, XEN_GUEST_HANDLE_PARAM(void) uaddr,
+                            unsigned long len)
+{
+    if ( len > XEN_FS_MAX_PATHLEN )
+        return -EINVAL;
+
+    if ( copy_from_guest(buf, uaddr, len) )
+        return -EFAULT;
+
+    buf[len - 1] = 0;
+
+    return 0;
+}
+
+static struct fs_entry *fs_get_entry_rel(struct fs_entry *dir, char *path)
+{
+    char *slash;
+    struct fs_entry *entry;
+    struct list_head *l;
+    unsigned int name_len;
+
+    if ( *path == 0 )
+        return dir;
+
+    if ( dir->type != fs_type_dir )
+        return NULL;
+
+    slash = strchr(path, '/');
+    if ( !slash )
+        slash = strchr(path, '\0');
+    name_len = slash - path;
+
+    list_for_each ( l, &dir->dir->list )
+    {
+        int cmp;
+
+        entry = list_entry(l, struct fs_entry, list);
+        cmp = strncmp(path, entry->name, name_len);
+        if ( cmp < 0 )
+            return NULL;
+        if ( cmp > 0 )
+            continue;
+        if ( strlen(entry->name) == name_len )
+            return *slash ? fs_get_entry_rel(entry, slash + 1) : entry;
+    }
+
+    return NULL;
+}
+
+struct fs_entry *fs_get_entry(char *path)
+{
+    if ( path[0] != '/' )
+        return NULL;
+
+    return fs_get_entry_rel(&fs_root_entry, path + 1);
+}
+
+long do_filesystem_op(unsigned int cmd,
+                      XEN_GUEST_HANDLE_PARAM(void) arg1, unsigned long arg2,
+                      XEN_GUEST_HANDLE_PARAM(void) arg3, unsigned long arg4)
+{
+    int ret;
+    struct fs_entry *entry;
+    unsigned int len;
+    static char path[XEN_FS_MAX_PATHLEN];
+
+    if ( !is_control_domain(current->domain) &&
+         !is_hardware_domain(current->domain) )
+        return -EPERM;
+
+    spin_lock(&fs_lock);
+
+    ret = fs_get_path_user(path, arg1, arg2);
+    if ( ret )
+        goto out;
+
+    entry = fs_get_entry(path);
+    if ( !entry )
+    {
+        ret = -ENOENT;
+        goto out;
+    }
+
+    switch ( cmd )
+    {
+    case XEN_FS_OP_read_contents:
+        if ( entry->type == fs_type_dir )
+        {
+            ret = -EISDIR;
+            break;
+        }
+
+        len = strlen(entry->val) + 1;
+        if ( len > arg4 )
+        {
+            ret = len;
+            break;
+        }
+
+        if ( copy_to_guest(arg3, entry->val, len) )
+            ret = -EFAULT;
+
+        break;
+
+    case XEN_FS_OP_read_dir:
+    {
+        struct list_head *l;
+
+        if ( entry->type != fs_type_dir )
+        {
+            ret = -ENOTDIR;
+            break;
+        }
+
+        len = entry->dir->content_size;
+        if ( len > arg4 )
+        {
+            ret = len;
+            break;
+        }
+
+        list_for_each ( l, &entry->dir->list )
+        {
+            struct xen_fs_direntry direntry;
+            struct fs_entry *e = list_entry(l, struct fs_entry, list);
+            unsigned int e_len = strlen(e->name) + 1;
+
+            e_len = sizeof(direntry) + ROUNDUP(e_len, 4);
+            direntry.flags = (e->type == fs_type_dir) ? XEN_FS_ISDIR : 0;
+            direntry.off_next = list_is_last(l, &entry->dir->list) ? 0 : e_len;
+            direntry.content_len = (e->type == fs_type_dir)
+                                       ? e->dir->content_size
+                                       : strlen(e->val) + 1;
+            if ( copy_to_guest(arg3, &direntry, 1) )
+            {
+                ret = -EFAULT;
+                goto out;
+            }
+
+            if ( copy_to_guest_offset(arg3, sizeof(direntry), e->name,
+                                      strlen(e->name) + 1) )
+            {
+                ret = -EFAULT;
+                goto out;
+            }
+
+            guest_handle_add_offset(arg3, e_len);
+        }
+
+        break;
+    }
+
+    default:
+        ret = -ENOSYS;
+        break;
+    }
+
+ out:
+    spin_unlock(&fs_lock);
+
+    return ret;
+}
diff --git a/xen/include/public/errno.h b/xen/include/public/errno.h
index e1d02fcddf..5c53af6af9 100644
--- a/xen/include/public/errno.h
+++ b/xen/include/public/errno.h
@@ -78,6 +78,7 @@ XEN_ERRNO(EBUSY,	16)	/* Device or resource busy */
 XEN_ERRNO(EEXIST,	17)	/* File exists */
 XEN_ERRNO(EXDEV,	18)	/* Cross-device link */
 XEN_ERRNO(ENODEV,	19)	/* No such device */
+XEN_ERRNO(ENOTDIR,	20)	/* Not a directory */
 XEN_ERRNO(EISDIR,	21)	/* Is a directory */
 XEN_ERRNO(EINVAL,	22)	/* Invalid argument */
 XEN_ERRNO(ENFILE,	23)	/* File table overflow */
diff --git a/xen/include/public/filesystem.h b/xen/include/public/filesystem.h
new file mode 100644
index 0000000000..cc790ae490
--- /dev/null
+++ b/xen/include/public/filesystem.h
@@ -0,0 +1,98 @@
+/******************************************************************************
+ * Xen Hypervisor Filesystem
+ *
+ * Copyright (c) 2019, SUSE Software Solutions Germany GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+#ifndef __XEN_PUBLIC_FILESYSTEM_H__
+#define __XEN_PUBLIC_FILESYSTEM_H__
+
+#include "xen.h"
+
+/*
+ * Definitions for the __HYPERVISOR_filesystem_op hypercall.
+ */
+
+/* Maximum length of a path in the filesystem. */
+#define XEN_FS_MAX_PATHLEN 1024
+
+struct xen_fs_direntry {
+    uint16_t flags;
+#define XEN_FS_ISDIR   0x0001
+    /* Offset in bytes to next entry (0 == this is the last entry). */
+    uint16_t off_next;
+    uint32_t content_len;
+    char name[XEN_FLEX_ARRAY_DIM];
+};
+
+/*
+ * Hypercall operations.
+ */
+
+/*
+ * XEN_FS_OP_read_contents
+ *
+ * Read contents of a filesystem entry.
+ *
+ * Returns the contents of an entry in the buffer supplied by the caller.
+ * Only text data with a trailing zero byte is returned.
+ *
+ * arg1: XEN_GUEST_HANDLE(path name)
+ * arg2: length of path name (including trailing zero byte)
+ * arg3: XEN_GUEST_HANDLE(content buffer)
+ * arg4: content buffer size
+ *
+ * Possible return values:
+ * 0: success
+ * -EPERM:   operation not permitted
+ * -ENOENT:  entry not found
+ * -EACCESS: access to entry not permitted
+ * -EISDIR:  entry is a directory
+ * -EINVAL:  invalid parameter
+ * positive value: content buffer was too small, returned value is needed size
+ */
+#define XEN_FS_OP_read_contents     1
+
+/*
+ * XEN_FS_OP_read_dir
+ *
+ * Read directory entries of a directory.
+ *
+ * Returns a struct xen_fs_direntry for each entry in a directory.
+ *
+ * arg1: XEN_GUEST_HANDLE(path name)
+ * arg2: length of path name (including trailing zero byte)
+ * arg3: XEN_GUEST_HANDLE(content buffer)
+ * arg4: content buffer size
+ *
+ * Possible return values:
+ * 0: success
+ * -EPERM:   operation not permitted
+ * -ENOENT:  entry not found
+ * -EACCESS: access to entry not permitted
+ * -ENOTDIR: entry is not a directory
+ * -EINVAL:  invalid parameter
+ * positive value: content buffer was too small, returned value is needed size
+ */
+#define XEN_FS_OP_read_dir          2
+
+#endif /* __XEN_PUBLIC_FILESYSTEM_H__ */
diff --git a/xen/include/public/xen.h b/xen/include/public/xen.h
index cef65c38e7..232589a95b 100644
--- a/xen/include/public/xen.h
+++ b/xen/include/public/xen.h
@@ -130,6 +130,7 @@ DEFINE_XEN_GUEST_HANDLE(xen_ulong_t);
 #define __HYPERVISOR_argo_op              39
 #define __HYPERVISOR_xenpmu_op            40
 #define __HYPERVISOR_dm_op                41
+#define __HYPERVISOR_filesystem_op        42
 
 /* Architecture-specific hypercall definitions. */
 #define __HYPERVISOR_arch_0               48
diff --git a/xen/include/xen/filesystem.h b/xen/include/xen/filesystem.h
new file mode 100644
index 0000000000..24e0e2c62d
--- /dev/null
+++ b/xen/include/xen/filesystem.h
@@ -0,0 +1,34 @@
+#ifndef __XEN_FILESYSTEM_H__
+#define __XEN_FILESYSTEM_H__
+
+#include <xen/list.h>
+
+struct fs_dir {
+    unsigned int content_size;
+    struct list_head list;
+};
+
+enum fs_entry_type {
+    fs_type_dir,
+    fs_type_string
+};
+
+struct fs_entry {
+    enum fs_entry_type type;
+    const char *name;
+    struct list_head list;
+    struct fs_dir *parent;
+    union {
+        void *content;
+        struct fs_dir *dir;
+        const char *val;
+    };
+};
+
+extern struct fs_dir fs_root;
+
+int fs_new_dir(struct fs_dir *parent, const char *name, struct fs_dir *dir);
+int fs_new_entry(struct fs_dir *parent, const char *name, const char *val);
+struct fs_entry *fs_get_entry(char *path);
+
+#endif /* __XEN_FILESYSTEM_H__ */
diff --git a/xen/include/xen/hypercall.h b/xen/include/xen/hypercall.h
index ad8ad27b23..56129ef82f 100644
--- a/xen/include/xen/hypercall.h
+++ b/xen/include/xen/hypercall.h
@@ -150,6 +150,14 @@ do_dm_op(
     unsigned int nr_bufs,
     XEN_GUEST_HANDLE_PARAM(xen_dm_op_buf_t) bufs);
 
+extern long
+do_filesystem_op(
+    unsigned int cmd,
+    XEN_GUEST_HANDLE_PARAM(void) arg1,
+    unsigned long arg2,
+    XEN_GUEST_HANDLE_PARAM(void) arg3,
+    unsigned long arg4);
+
 #ifdef CONFIG_COMPAT
 
 extern int
