diff --git a/xen/arch/arm/traps.c b/xen/arch/arm/traps.c
index a3b961bd06..4b1acd67a2 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(hypfs_op, 5),
 };
 
 #ifndef NDEBUG
diff --git a/xen/arch/x86/hvm/hypercall.c b/xen/arch/x86/hvm/hypercall.c
index 33dd2d99d2..210dda4f38 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(hypfs_op),
     HYPERCALL(arch_1)
 };
 
diff --git a/xen/arch/x86/hypercall.c b/xen/arch/x86/hypercall.c
index d483dbaa6b..8a8f223481 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(hypfs_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..e6860247cc 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(hypfs_op),
     HYPERCALL(mca),
     HYPERCALL(arch_1),
 };
diff --git a/xen/common/Makefile b/xen/common/Makefile
index 62b34e69e9..a3f66aa0c0 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 += hypfs.o
 obj-$(CONFIG_CRASH_DEBUG) += gdbstub.o
 obj-$(CONFIG_GRANT_TABLE) += grant_table.o
 obj-y += guestcopy.o
diff --git a/xen/common/hypfs.c b/xen/common/hypfs.c
new file mode 100644
index 0000000000..7d9595dfa7
--- /dev/null
+++ b/xen/common/hypfs.c
@@ -0,0 +1,314 @@
+/******************************************************************************
+ *
+ * hypfs.c
+ *
+ * Simple sysfs-like file system for the hypervisor.
+ */
+
+#include <xen/lib.h>
+#include <xen/hypfs.h>
+#include <xen/guest_access.h>
+#include <xen/hypercall.h>
+#include <public/hypfs.h>
+
+static DEFINE_SPINLOCK(hypfs_lock);
+
+struct hypfs_dir hypfs_root = {
+    .list = LIST_HEAD_INIT(hypfs_root.list),
+};
+
+static struct hypfs_entry hypfs_root_entry = {
+    .type = hypfs_type_dir,
+    .name = "",
+    .list = LIST_HEAD_INIT(hypfs_root_entry.list),
+    .parent = &hypfs_root,
+    .dir = &hypfs_root,
+};
+
+static int hypfs_add_entry(struct hypfs_dir *parent, struct hypfs_entry *new)
+{
+    int ret = -ENOENT;
+    struct list_head *l;
+
+    if ( !new->content )
+        return -EINVAL;
+
+    spin_lock(&hypfs_lock);
+
+    list_for_each ( l, &parent->list )
+    {
+        struct hypfs_entry *e = list_entry(l, struct hypfs_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_hypfs_direntry) +
+                                ROUNDUP(sz, 4);
+        new->parent = parent;
+    }
+
+    spin_unlock(&hypfs_lock);
+
+    return ret;
+}
+
+int hypfs_new_entry_any(struct hypfs_dir *parent, const char *name,
+                        enum hypfs_entry_type type, void *content)
+{
+    int ret = -ENOMEM;
+    struct hypfs_entry *new = xzalloc(struct hypfs_entry);
+
+    if ( !new )
+        return ret;
+
+    new->name = name;
+    new->type = type;
+    new->content = content;
+
+    ret = hypfs_add_entry(parent, new);
+
+    if ( ret )
+        xfree(new);
+
+    return ret;
+}
+
+int hypfs_new_entry_string(struct hypfs_dir *parent, const char *name,
+                           char *val)
+{
+    return hypfs_new_entry_any(parent, name, hypfs_type_string, val);
+}
+
+int hypfs_new_entry_uint(struct hypfs_dir *parent, const char *name,
+                         unsigned int *val)
+{
+    return hypfs_new_entry_any(parent, name, hypfs_type_uint, val);
+}
+
+int hypfs_new_dir(struct hypfs_dir *parent, const char *name,
+                  struct hypfs_dir *dir)
+{
+    if ( !dir )
+        dir = xzalloc(struct hypfs_dir);
+
+    return hypfs_new_entry_any(parent, name, hypfs_type_dir, dir);
+}
+
+static int hypfs_get_path_user(char *buf, XEN_GUEST_HANDLE_PARAM(void) uaddr,
+                               unsigned long len)
+{
+    if ( len > XEN_HYPFS_MAX_PATHLEN )
+        return -EINVAL;
+
+    if ( copy_from_guest(buf, uaddr, len) )
+        return -EFAULT;
+
+    buf[len - 1] = 0;
+
+    return 0;
+}
+
+static struct hypfs_entry *hypfs_get_entry_rel(struct hypfs_entry *dir,
+                                               char *path)
+{
+    char *slash;
+    struct hypfs_entry *entry;
+    struct list_head *l;
+    unsigned int name_len;
+
+    if ( *path == 0 )
+        return dir;
+
+    if ( dir->type != hypfs_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 hypfs_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 ? hypfs_get_entry_rel(entry, slash + 1) : entry;
+    }
+
+    return NULL;
+}
+
+struct hypfs_entry *hypfs_get_entry(char *path)
+{
+    if ( path[0] != '/' )
+        return NULL;
+
+    return hypfs_get_entry_rel(&hypfs_root_entry, path + 1);
+}
+
+static unsigned int hypfs_get_entry_len(struct hypfs_entry *entry)
+{
+    unsigned int len = 0;
+
+    switch ( entry->type )
+    {
+    case hypfs_type_dir:
+        len = entry->dir->content_size;
+        break;
+    case hypfs_type_string:
+        len = strlen(entry->str_val) + 1;
+        break;
+    case hypfs_type_uint:
+        len = 11;      /* longest possible printed value + 1 */
+        break;
+    }
+
+    return len;
+}
+
+long do_hypfs_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 hypfs_entry *entry;
+    unsigned int len;
+    static char path[XEN_HYPFS_MAX_PATHLEN];
+
+    if ( !is_control_domain(current->domain) &&
+         !is_hardware_domain(current->domain) )
+        return -EPERM;
+
+    spin_lock(&hypfs_lock);
+
+    ret = hypfs_get_path_user(path, arg1, arg2);
+    if ( ret )
+        goto out;
+
+    entry = hypfs_get_entry(path);
+    if ( !entry )
+    {
+        ret = -ENOENT;
+        goto out;
+    }
+
+    switch ( cmd )
+    {
+    case XEN_HYPFS_OP_read_contents:
+    {
+        char buf[12];
+        char *val = buf;
+
+        len = hypfs_get_entry_len(entry);
+        if ( len > arg4 )
+        {
+            ret = len;
+            break;
+        }
+
+        switch ( entry->type )
+        {
+        case hypfs_type_dir:
+            ret = -EISDIR;
+            break;
+        case hypfs_type_string:
+            val = entry->str_val;
+            break;
+        case hypfs_type_uint:
+            len = snprintf(buf, sizeof(buf), "%u", *entry->uint_val) + 1;
+            break;
+        }
+
+        if ( !ret && copy_to_guest(arg3, val, len) )
+            ret = -EFAULT;
+
+        break;
+    }
+
+    case XEN_HYPFS_OP_read_dir:
+    {
+        struct list_head *l;
+
+        if ( entry->type != hypfs_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_hypfs_direntry direntry;
+            struct hypfs_entry *e = list_entry(l, struct hypfs_entry, list);
+            unsigned int e_len = strlen(e->name) + 1;
+
+            e_len = sizeof(direntry) + ROUNDUP(e_len, 4);
+            direntry.flags = (e->type == hypfs_type_dir) ? XEN_HYPFS_ISDIR : 0;
+            direntry.off_next = list_is_last(l, &entry->dir->list) ? 0 : e_len;
+            direntry.content_len = hypfs_get_entry_len(e);
+            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;
+    }
+
+    case XEN_HYPFS_OP_write_contents:
+        ret = -EACCES;
+        break;
+
+    default:
+        ret = -ENOSYS;
+        break;
+    }
+
+ out:
+    spin_unlock(&hypfs_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/hypfs.h b/xen/include/public/hypfs.h
new file mode 100644
index 0000000000..8822e2fb15
--- /dev/null
+++ b/xen/include/public/hypfs.h
@@ -0,0 +1,123 @@
+/******************************************************************************
+ * 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_HYPFS_H__
+#define __XEN_PUBLIC_HYPFS_H__
+
+#include "xen.h"
+
+/*
+ * Definitions for the __HYPERVISOR_hypfs_op hypercall.
+ */
+
+/* Maximum length of a path in the filesystem. */
+#define XEN_HYPFS_MAX_PATHLEN 1024
+
+struct xen_hypfs_direntry {
+    uint16_t flags;
+#define XEN_HYPFS_ISDIR      0x0001
+#define XEN_HYPFS_WRITEABLE  0x0002
+    /* 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_HYPFS_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_HYPFS_OP_read_contents     1
+
+/*
+ * XEN_HYPFS_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_HYPFS_OP_read_dir          2
+
+/*
+ * XEN_HYPFS_OP_read_contents
+ *
+ * Write contents of a filesystem entry.
+ *
+ * Writes an entry with the contents of a buffer supplied by the caller.
+ * Only text data with a trailing zero byte can be written.
+ *
+ * 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
+ * -ENOMEM:  memory shortage in the hypervisor
+ */
+#define XEN_HYPFS_OP_write_contents    3
+
+#endif /* __XEN_PUBLIC_HYPFS_H__ */
diff --git a/xen/include/public/xen.h b/xen/include/public/xen.h
index d2198dffad..bf80f1da8c 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_hypfs_op             42
 
 /* Architecture-specific hypercall definitions. */
 #define __HYPERVISOR_arch_0               48
diff --git a/xen/include/xen/hypercall.h b/xen/include/xen/hypercall.h
index ad8ad27b23..349a0f6487 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_hypfs_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
diff --git a/xen/include/xen/hypfs.h b/xen/include/xen/hypfs.h
new file mode 100644
index 0000000000..d2537211f6
--- /dev/null
+++ b/xen/include/xen/hypfs.h
@@ -0,0 +1,40 @@
+#ifndef __XEN_HYPFS_H__
+#define __XEN_HYPFS_H__
+
+#include <xen/list.h>
+
+struct hypfs_dir {
+    unsigned int content_size;
+    struct list_head list;
+};
+
+enum hypfs_entry_type {
+    hypfs_type_dir,
+    hypfs_type_string,
+    hypfs_type_uint
+};
+
+struct hypfs_entry {
+    enum hypfs_entry_type type;
+    const char *name;
+    struct list_head list;
+    struct hypfs_dir *parent;
+    union {
+        void *content;
+        struct hypfs_dir *dir;
+        char *str_val;
+        unsigned int *uint_val;
+    };
+};
+
+extern struct hypfs_dir hypfs_root;
+
+int hypfs_new_dir(struct hypfs_dir *parent, const char *name,
+                  struct hypfs_dir *dir);
+int hypfs_new_entry_string(struct hypfs_dir *parent, const char *name,
+                           char *val);
+int hypfs_new_entry_uint(struct hypfs_dir *parent, const char *name,
+                         unsigned int *val);
+struct hypfs_entry *hypfs_get_entry(char *path);
+
+#endif /* __XEN_HYPFS_H__ */
