diff --git a/shared/os-compatibility.c b/shared/os-compatibility.c index aeea780f..5e1ce479 100644 --- a/shared/os-compatibility.c +++ b/shared/os-compatibility.c @@ -33,6 +33,7 @@ #include #include #include +#include #ifdef HAVE_MEMFD_CREATE #include @@ -40,6 +41,8 @@ #include "os-compatibility.h" +#define READONLY_SEALS (F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) + int os_fd_set_cloexec(int fd) { @@ -233,3 +236,172 @@ strchrnul(const char *s, int c) return (char *)s; } #endif + +struct ro_anonymous_file { + int fd; + size_t size; +}; + +/** Create a new anonymous read-only file of the given size and the given data + * + * \param size The size of \p data. + * \param data The data of the file with the size \p size. + * \return A new \c ro_anonymous_file, or NULL on failure. + * + * The intended use-case is for sending mid-sized data from the compositor + * to clients. + * If the function fails errno is set. + */ +struct ro_anonymous_file * +os_ro_anonymous_file_create(size_t size, + const char *data) +{ + struct ro_anonymous_file *file; + void *map; + + file = zalloc(sizeof *file); + if (!file) { + errno = ENOMEM; + return NULL; + } + + file->size = size; + file->fd = os_create_anonymous_file(size); + if (file->fd == -1) + goto err_free; + + map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, file->fd, 0); + if (map == MAP_FAILED) + goto err_close; + + memcpy(map, data, size); + + munmap(map, size); + +#ifdef HAVE_MEMFD_CREATE + /* try to put seals on the file to make it read-only so that we can + * return the fd later directly when support_shared is not set. + * os_ro_anonymous_file_get_fd can handle the fd even if it is not + * sealed read-only and will instead create a new anonymous file on + * each invocation. + */ + fcntl(file->fd, F_ADD_SEALS, READONLY_SEALS); +#endif + + return file; + +err_close: + close(file->fd); +err_free: + free(file); + return NULL; +} + +/** Destroy an anonymous read-only file + * + * \param file The file to destroy. + */ +void +os_ro_anonymous_file_destroy(struct ro_anonymous_file *file) +{ + close(file->fd); + free(file); +} + +/** Get the size of an anonymous read-only file + * + * \param file The file to get the size of. + * \return The size of the file. + */ +size_t +os_ro_anonymous_file_size(struct ro_anonymous_file *file) +{ + return file->size; +} + +/** Returns a file descriptor for the given file, ready to be send to a client. + * + * \param file The file for which to get a file descriptor. + * \param mapmode Describes the ways in which the returned file descriptor can + * be used with mmap. + * \return A file descriptor for the given file that can be send to a client + * or -1 on failure. + * + * The returned file descriptor must not be shared between multiple clients. + * When \p mapmode is RO_ANONYMOUS_FILE_MAPMODE_PRIVATE the file descriptor is + * only guaranteed to be mmapable with \c MAP_PRIVATE, when \p mapmode is + * RO_ANONYMOUS_FILE_MAPMODE_SHARED the file descriptor can be mmaped with + * either MAP_PRIVATE or MAP_SHARED. + * When you're done with the fd you must call \c os_ro_anonymous_file_put_fd + * instead of calling \c close. + * If the function fails errno is set. + */ +int +os_ro_anonymous_file_get_fd(struct ro_anonymous_file *file, + enum ro_anonymous_file_mapmode mapmode) +{ + void *src, *dst; + int seals, fd; + + seals = fcntl(file->fd, F_GET_SEALS); + + /* file was sealed for read-only and we don't have to support MAP_SHARED + * so we can simply pass the memfd fd + */ + if (seals != -1 && mapmode == RO_ANONYMOUS_FILE_MAPMODE_PRIVATE && + (seals & READONLY_SEALS) == READONLY_SEALS) + return file->fd; + + /* for all other cases we create a new anonymous file that can be mapped + * with MAP_SHARED and copy the contents to it and return that instead + */ + fd = os_create_anonymous_file(file->size); + if (fd == -1) + return fd; + + src = mmap(NULL, file->size, PROT_READ, MAP_PRIVATE, file->fd, 0); + if (src == MAP_FAILED) { + close(fd); + return -1; + } + + dst = mmap(NULL, file->size, PROT_WRITE, MAP_SHARED, fd, 0); + if (dst == MAP_FAILED) { + close(fd); + munmap(src, file->size); + return -1; + } + + memcpy(dst, src, file->size); + munmap(src, file->size); + munmap(dst, file->size); + + return fd; +} + +/** Release a file descriptor returned by \c os_ro_anonymous_file_get_fd + * + * \param fd A file descriptor returned by \c os_ro_anonymous_file_get_fd. + * \return 0 on success, or -1 on failure. + * + * This function must be called for every file descriptor created with + * \c os_ro_anonymous_file_get_fd to not leake any resources. + * If the function fails errno is set. + */ +int +os_ro_anonymous_file_put_fd(int fd) +{ + int seals = fcntl(fd, F_GET_SEALS); + if (seals == -1 && errno != EINVAL) + return -1; + + /* If the fd cannot be sealed seals is -1 at this point + * or the file can be sealed but has not been sealed for writing. + * In both cases we created a new anonymous file that we have to + * close. + */ + if (seals == -1 || !(seals & F_SEAL_WRITE)) + close(fd); + + return 0; +} diff --git a/shared/os-compatibility.h b/shared/os-compatibility.h index 35e3a78d..6aaa2688 100644 --- a/shared/os-compatibility.h +++ b/shared/os-compatibility.h @@ -47,4 +47,28 @@ char * strchrnul(const char *s, int c); #endif +struct ro_anonymous_file; + +enum ro_anonymous_file_mapmode { + RO_ANONYMOUS_FILE_MAPMODE_PRIVATE, + RO_ANONYMOUS_FILE_MAPMODE_SHARED, +}; + +struct ro_anonymous_file * +os_ro_anonymous_file_create(size_t size, + const char *data); + +void +os_ro_anonymous_file_destroy(struct ro_anonymous_file *file); + +size_t +os_ro_anonymous_file_size(struct ro_anonymous_file *file); + +int +os_ro_anonymous_file_get_fd(struct ro_anonymous_file *file, + enum ro_anonymous_file_mapmode mapmode); + +int +os_ro_anonymous_file_put_fd(int fd); + #endif /* OS_COMPATIBILITY_H */