protocol-native: add security context API

Add a new extension that can create a server on a user provided socket
with user provided security properties.

This is mainly used in flatpaks that want to create and bind a pipewire
socket with specific permissions for the flatpak app.

The flatpak will also provide an fd that will be closed when the server
can be removed.
This commit is contained in:
Wim Taymans 2024-02-08 10:04:21 +01:00
parent c5e8da7247
commit e7846fc12a
8 changed files with 580 additions and 1 deletions

View file

@ -331,6 +331,7 @@ pipewire_module_protocol_native = shared_library('pipewire-module-protocol-nativ
'module-protocol-native/protocol-native.c',
'module-protocol-native/v0/protocol-native.c',
'module-protocol-native/protocol-footer.c',
'module-protocol-native/security-context.c',
'module-protocol-native/connection.c' ],
include_directories : [configinc],
install : true,

View file

@ -175,6 +175,7 @@ static const struct spa_dict_item module_props[] = {
void pw_protocol_native_init(struct pw_protocol *protocol);
void pw_protocol_native0_init(struct pw_protocol *protocol);
int protocol_native_security_context_init(struct pw_impl_module *module, struct pw_protocol *protocol);
struct protocol_data {
struct pw_impl_module *module;
@ -233,6 +234,7 @@ struct server {
struct pw_loop *loop;
struct spa_source *source;
struct spa_source *resume;
struct spa_source *close;
unsigned int activated:1;
};
@ -809,6 +811,17 @@ socket_data(void *data, int fd, uint32_t mask)
}
}
static void
close_data(void *data, int fd, uint32_t mask)
{
struct server *s = data;
if (mask & (SPA_IO_HUP | SPA_IO_ERR)) {
pw_log_info("server %p: closed socket %d %08x", s, fd, mask);
pw_protocol_server_destroy(&s->this);
}
}
static int write_socket_address(struct server *s)
{
long v;
@ -1337,6 +1350,8 @@ static void destroy_server(struct pw_protocol_server *server)
pw_loop_destroy_source(s->loop, s->source);
if (s->resume)
pw_loop_destroy_source(s->loop, s->resume);
if (s->close)
pw_loop_destroy_source(s->loop, s->close);
if (s->addr.sun_path[0] && !s->activated)
unlink(s->addr.sun_path);
if (s->lock_addr[0])
@ -1464,10 +1479,58 @@ impl_add_server(struct pw_protocol *protocol,
return add_server(protocol, core, props, NULL);
}
static struct pw_protocol_server *
impl_add_fd_server(struct pw_protocol *protocol,
struct pw_impl_core *core,
int listen_fd, int close_fd,
const struct spa_dict *props)
{
struct pw_protocol_server *this;
struct server *s;
int res;
if ((s = create_server(protocol, core, props)) == NULL)
return NULL;
this = &s->this;
pw_properties_setf(s->props, PW_KEY_SEC_SOCKET, "pipewire-fd-%d", listen_fd);
s->loop = pw_context_get_main_loop(protocol->context);
if (s->loop == NULL) {
res = -errno;
goto error;
}
s->source = pw_loop_add_io(s->loop, listen_fd, SPA_IO_IN, true, socket_data, s);
if (s->source == NULL) {
res = -errno;
goto error;
}
s->close = pw_loop_add_io(s->loop, close_fd, 0, true, close_data, s);
if (s->close == NULL) {
res = -errno;
goto error;
}
if ((s->resume = pw_loop_add_event(s->loop, do_resume, s)) == NULL) {
res = -errno;
goto error;
}
pw_log_info("%p: Listening on fd:%d", protocol, listen_fd);
return this;
error:
destroy_server(this);
errno = -res;
return NULL;
}
static const struct pw_protocol_implementation protocol_impl = {
PW_VERSION_PROTOCOL_IMPLEMENTATION,
.new_client = impl_new_client,
.add_server = impl_add_server,
.add_fd_server = impl_add_fd_server,
};
static struct spa_pod_builder *
@ -1753,6 +1816,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args_str)
goto error_cleanup;
}
protocol_native_security_context_init(module, this);
props = pw_context_get_properties(context);
pw_properties_update_keys(d->props, &props->dict, keys);

View file

@ -11,6 +11,7 @@
#include <pipewire/impl.h>
#include <pipewire/extensions/protocol-native.h>
#include <pipewire/extensions/security-context.h>
#include "connection.h"
@ -1879,6 +1880,66 @@ static int registry_marshal_destroy(void *object, uint32_t id)
return pw_protocol_native_end_proxy(proxy, b);
}
static int security_context_method_marshal_add_listener(void *object,
struct spa_hook *listener,
const struct pw_security_context_events *events,
void *data)
{
struct pw_proxy *proxy = object;
pw_proxy_add_object_listener(proxy, listener, events, data);
return 0;
}
static int security_context_marshal_create(void *object, const char *engine_name,
int listen_fd, int close_fd, const struct spa_dict *props)
{
struct pw_proxy *proxy = object;
struct spa_pod_builder *b;
struct spa_pod_frame f;
b = pw_protocol_native_begin_proxy(proxy, PW_SECURITY_CONTEXT_METHOD_CREATE, NULL);
spa_pod_builder_push_struct(b, &f);
spa_pod_builder_add(b,
SPA_POD_String(engine_name),
SPA_POD_Fd(pw_protocol_native_add_proxy_fd(proxy, listen_fd)),
SPA_POD_Fd(pw_protocol_native_add_proxy_fd(proxy, close_fd)),
NULL);
push_dict(b, props);
spa_pod_builder_pop(b, &f);
return pw_protocol_native_end_proxy(proxy, b);
}
static int security_context_demarshal_create(void *object, const struct pw_protocol_native_message *msg)
{
struct pw_resource *resource = object;
struct spa_dict props = SPA_DICT_INIT(NULL, 0);
struct spa_pod_parser prs;
struct spa_pod_frame f[2];
char *engine_name;
int64_t listen_idx, close_idx;
int listen_fd, close_fd;
spa_pod_parser_init(&prs, msg->data, msg->size);
if (spa_pod_parser_push_struct(&prs, &f[0]) < 0)
return -EINVAL;
if (spa_pod_parser_get(&prs,
SPA_POD_String(&engine_name),
SPA_POD_Fd(&listen_idx),
SPA_POD_Fd(&close_idx),
NULL) < 0)
return -EINVAL;
parse_dict_struct(&prs, &f[1], &props);
listen_fd = pw_protocol_native_get_resource_fd(resource, listen_idx);
close_fd = pw_protocol_native_get_resource_fd(resource, close_idx);
return pw_resource_notify(resource, struct pw_security_context_methods, create, 0,
engine_name, listen_fd, close_fd, &props);
}
static const struct pw_core_methods pw_protocol_native_core_method_marshal = {
PW_VERSION_CORE_METHODS,
.add_listener = &core_method_marshal_add_listener,
@ -2253,6 +2314,40 @@ static const struct pw_protocol_marshal pw_protocol_native_link_marshal = {
.client_demarshal = pw_protocol_native_link_event_demarshal,
};
static const struct pw_security_context_methods pw_protocol_native_security_context_method_marshal = {
PW_VERSION_LINK_METHODS,
.add_listener = &security_context_method_marshal_add_listener,
.create = &security_context_marshal_create,
};
static const struct pw_protocol_native_demarshal
pw_protocol_native_security_context_method_demarshal[PW_SECURITY_CONTEXT_METHOD_NUM] =
{
[PW_SECURITY_CONTEXT_METHOD_ADD_LISTENER] = { NULL, 0, },
[PW_SECURITY_CONTEXT_METHOD_CREATE] = { &security_context_demarshal_create, 0, },
};
static const struct pw_security_context_events pw_protocol_native_security_context_event_marshal = {
PW_VERSION_LINK_EVENTS,
};
static const struct pw_protocol_native_demarshal
pw_protocol_native_security_context_event_demarshal[PW_SECURITY_CONTEXT_EVENT_NUM] =
{
};
static const struct pw_protocol_marshal pw_protocol_native_security_context_marshal = {
PW_TYPE_INTERFACE_SecurityContext,
PW_VERSION_SECURITY_CONTEXT,
0,
PW_SECURITY_CONTEXT_METHOD_NUM,
PW_SECURITY_CONTEXT_EVENT_NUM,
.client_marshal = &pw_protocol_native_security_context_method_marshal,
.server_demarshal = pw_protocol_native_security_context_method_demarshal,
.server_marshal = &pw_protocol_native_security_context_event_marshal,
.client_demarshal = pw_protocol_native_security_context_event_demarshal,
};
void pw_protocol_native_init(struct pw_protocol *protocol)
{
pw_protocol_add_marshal(protocol, &pw_protocol_native_core_marshal);
@ -2264,4 +2359,5 @@ void pw_protocol_native_init(struct pw_protocol *protocol)
pw_protocol_add_marshal(protocol, &pw_protocol_native_factory_marshal);
pw_protocol_add_marshal(protocol, &pw_protocol_native_client_marshal);
pw_protocol_add_marshal(protocol, &pw_protocol_native_link_marshal);
pw_protocol_add_marshal(protocol, &pw_protocol_native_security_context_marshal);
}

View file

@ -0,0 +1,132 @@
/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */
/* SPDX-License-Identifier: MIT */
#include <pipewire/pipewire.h>
#include <pipewire/impl.h>
#include <pipewire/private.h>
#include <pipewire/extensions/security-context.h>
PW_LOG_TOPIC_EXTERN(mod_topic);
#define PW_LOG_TOPIC_DEFAULT mod_topic
PW_LOG_TOPIC_EXTERN(mod_topic_connection);
struct impl {
struct pw_context *context;
struct pw_global *global;
struct pw_protocol *protocol;
};
struct resource_data {
struct impl *impl;
struct pw_resource *resource;
struct spa_hook resource_listener;
struct spa_hook object_listener;
};
static int security_context_create(void *object,
const char *engine_name,
int listen_fd,
int close_fd,
const struct spa_dict *props)
{
struct resource_data *d = object;
struct impl *impl = d->impl;
pw_protocol_add_fd_server(impl->protocol, impl->context->core,
listen_fd, close_fd, props);
return 0;
}
static const struct pw_security_context_methods security_context_methods = {
PW_VERSION_SECURITY_CONTEXT_METHODS,
.create = security_context_create,
};
static void global_unbind(void *data)
{
struct resource_data *d = data;
if (d->resource) {
spa_hook_remove(&d->resource_listener);
}
}
static const struct pw_resource_events resource_events = {
PW_VERSION_RESOURCE_EVENTS,
.destroy = global_unbind,
};
static int
global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
uint32_t version, uint32_t id)
{
struct impl *impl = object;
struct pw_resource *resource;
struct resource_data *data;
resource = pw_resource_new(client, id, permissions,
PW_TYPE_INTERFACE_SecurityContext,
version, sizeof(*data));
if (resource == NULL)
return -errno;
data = pw_resource_get_user_data(resource);
data->impl = impl;
data->resource = resource;
pw_global_add_resource(impl->global, resource);
/* listen for when the resource goes away */
pw_resource_add_listener(resource,
&data->resource_listener,
&resource_events, data);
/* resource methods -> implementation */
pw_resource_add_object_listener(resource,
&data->object_listener,
&security_context_methods, data);
return 0;
}
int protocol_native_security_context_init(struct pw_impl_module *module, struct pw_protocol *protocol)
{
struct pw_context *context = pw_impl_module_get_context(module);
struct impl *impl;
char serial_str[32];
struct spa_dict_item items[1] = {
SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
};
struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
static const char * const keys[] = {
PW_KEY_OBJECT_SERIAL,
NULL
};
impl = calloc(1, sizeof(struct impl));
if (impl == NULL)
return -errno;
impl->context = context;
impl->protocol = protocol;
impl->global = pw_global_new(context,
PW_TYPE_INTERFACE_SecurityContext,
PW_VERSION_SECURITY_CONTEXT,
PW_SECURITY_CONTEXT_PERM_MASK,
NULL,
global_bind, impl);
if (impl->global == NULL) {
free(impl);
return -errno;
}
spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
pw_global_get_serial(impl->global));
pw_global_update_keys(impl->global, &extra_props, keys);
pw_global_register(impl->global);
return 0;
}

View file

@ -0,0 +1,111 @@
/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */
/* SPDX-License-Identifier: MIT */
#ifndef PIPEWIRE_EXT_SECURITY_CONTEXT_H
#define PIPEWIRE_EXT_SECURITY_CONTEXT_H
#ifdef __cplusplus
extern "C" {
#endif
#include <spa/utils/defs.h>
/** \defgroup pw_security_context Security Context
* Security Context interface
*/
/**
* \addtogroup pw_security_context
* \{
*/
#define PW_TYPE_INTERFACE_SecurityContext PW_TYPE_INFO_INTERFACE_BASE "SecurityContext"
#define PW_SECURITY_CONTEXT_PERM_MASK PW_PERM_RWX
#define PW_VERSION_SECURITY_CONTEXT 3
struct pw_security_context;
#define PW_EXTENSION_MODULE_SECURITY_CONTEXT PIPEWIRE_MODULE_PREFIX "module-security-context"
#define PW_SECURITY_CONTEXT_EVENT_NUM 0
/** \ref pw_security_context events */
struct pw_security_context_events {
#define PW_VERSION_SECURITY_CONTEXT_EVENTS 0
uint32_t version;
};
#define PW_SECURITY_CONTEXT_METHOD_ADD_LISTENER 0
#define PW_SECURITY_CONTEXT_METHOD_CREATE 1
#define PW_SECURITY_CONTEXT_METHOD_NUM 2
/** \ref pw_security_context methods */
struct pw_security_context_methods {
#define PW_VERSION_SECURITY_CONTEXT_METHODS 0
uint32_t version;
int (*add_listener) (void *object,
struct spa_hook *listener,
const struct pw_security_context_events *events,
void *data);
/**
* Create a new security context
*
* Creates a new security context with a socket listening FD.
* PipeWire will accept new client connections on listen_fd.
*
* listen_fd must be ready to accept new connections when this request is
* sent by the client. In other words, the client must call bind(2) and
* listen(2) before sending the FD.
*
* close_fd is a FD closed by the client when PipeWire should stop
* accepting new connections on listen_fd.
*
* PipeWire must continue to accept connections on listen_fd when
* the client which created the security context disconnects.
*
* After sending this request, closing listen_fd and close_fd remains the
* only valid operation on them.
*
* \param engine_name a unique sandbox engine name.
* \param listen_fd the fd to listen on for new connections
* \param close_fd the fd used to stop listening
* \param props extra (engine_name specific) properties.
*
* See https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md
* For a list of engine_names and the properties to set.
*
* This requires X and W permissions on the security_context.
*/
int (*create) (void *object,
const char *engine_name,
int listen_fd,
int close_fd,
const struct spa_dict *props);
};
#define pw_security_context_method(o,method,version,...) \
({ \
int _res = -ENOTSUP; \
spa_interface_call_res((struct spa_interface*)o, \
struct pw_security_context_methods, _res, \
method, version, ##__VA_ARGS__); \
_res; \
})
#define pw_security_context_add_listener(c,...) pw_security_context_method(c,add_listener,0,__VA_ARGS__)
#define pw_security_context_create(c,...) pw_security_context_method(c,create,0,__VA_ARGS__)
/**
* \}
*/
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* PIPEWIRE_EXT_SECURITY_CONTEXT_H */

View file

@ -81,7 +81,7 @@ struct pw_protocol_marshal {
};
struct pw_protocol_implementation {
#define PW_VERSION_PROTOCOL_IMPLEMENTATION 0
#define PW_VERSION_PROTOCOL_IMPLEMENTATION 1
uint32_t version;
struct pw_protocol_client * (*new_client) (struct pw_protocol *protocol,
@ -90,6 +90,10 @@ struct pw_protocol_implementation {
struct pw_protocol_server * (*add_server) (struct pw_protocol *protocol,
struct pw_impl_core *core,
const struct spa_dict *props);
struct pw_protocol_server * (*add_fd_server) (struct pw_protocol *protocol,
struct pw_impl_core *core,
int listen_fd, int close_fd,
const struct spa_dict *props);
};
struct pw_protocol_events {
@ -101,6 +105,7 @@ struct pw_protocol_events {
#define pw_protocol_new_client(p,...) (pw_protocol_get_implementation(p)->new_client(p,__VA_ARGS__))
#define pw_protocol_add_server(p,...) (pw_protocol_get_implementation(p)->add_server(p,__VA_ARGS__))
#define pw_protocol_add_fd_server(p,...) (pw_protocol_get_implementation(p)->add_fd_server(p,__VA_ARGS__))
#define pw_protocol_ext(p,type,method,...) (((type*)pw_protocol_get_extension(p))->method( __VA_ARGS__))
struct pw_protocol *pw_protocol_new(struct pw_context *context, const char *name, size_t user_data_size);

View file

@ -4,6 +4,7 @@ test_apps = [
# 'test-remote',
'test-stream',
'test-filter',
'test-security-context',
]
foreach a : test_apps

View file

@ -0,0 +1,168 @@
/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */
/* SPDX-License-Identifier: MIT */
#include <stdlib.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <limits.h>
#include <pipewire/pipewire.h>
#include <pipewire/main-loop.h>
#include <pipewire/extensions/security-context.h>
#include <spa/utils/string.h>
#define TEST_FUNC(a,b,func) \
do { \
a.func = b.func; \
spa_assert_se(SPA_PTRDIFF(&a.func, &a) == SPA_PTRDIFF(&b.func, &b)); \
} while(0)
static void test_abi(void)
{
static const struct {
uint32_t version;
} test = { PW_VERSION_SECURITY_CONTEXT_EVENTS, };
struct pw_security_context_events ev;
spa_assert_se(PW_VERSION_SECURITY_CONTEXT_EVENTS == 0);
spa_assert_se(sizeof(ev) == sizeof(test));
}
struct roundtrip_data
{
struct pw_main_loop *loop;
int pending;
int done;
};
static void core_event_done(void *object, uint32_t id, int seq)
{
struct roundtrip_data *data = object;
if (id == PW_ID_CORE && seq == data->pending) {
data->done = 1;
pw_main_loop_quit(data->loop);
}
}
static int roundtrip(struct pw_core *core, struct pw_main_loop *loop)
{
struct spa_hook core_listener;
struct roundtrip_data data = { .loop = loop };
const struct pw_core_events core_events = {
PW_VERSION_CORE_EVENTS,
.done = core_event_done,
};
spa_zero(core_listener);
pw_core_add_listener(core, &core_listener,
&core_events, &data);
data.pending = pw_core_sync(core, PW_ID_CORE, 0);
while (!data.done)
pw_main_loop_run(loop);
spa_hook_remove(&core_listener);
return 0;
}
struct registry_info {
struct pw_registry *registry;
struct pw_security_context *sec;
};
static void registry_global(void *data, uint32_t id,
uint32_t permissions, const char *type, uint32_t version,
const struct spa_dict *props)
{
struct registry_info *info = data;
if (spa_streq(type, PW_TYPE_INTERFACE_SecurityContext)) {
info->sec = pw_registry_bind(info->registry, id, type, version, 0);
}
}
static const struct pw_registry_events registry_events = {
PW_VERSION_REGISTRY_EVENTS,
.global = registry_global
};
static void test_create(void)
{
struct pw_main_loop *loop;
struct pw_context *context;
struct pw_core *core;
struct registry_info info;
struct spa_hook listener;
int res, listen_fd, close_fd[2];
char temp[PATH_MAX] = "/tmp/pipewire-XXXXXX";
struct sockaddr_un sockaddr = {0};
loop = pw_main_loop_new(NULL);
context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
spa_assert_se(context != NULL);
core = pw_context_connect(context, NULL, 0);
spa_assert_se(core != NULL);
spa_zero(info);
info.registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0);
spa_assert_se(info.registry != NULL);
pw_registry_add_listener(info.registry, &listener, &registry_events, &info);
roundtrip(core, loop);
spa_assert_se(info.sec != NULL);
res = mkstemp(temp);
spa_assert_se(res >= 0);
close(res);
unlink(temp);
listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
spa_assert_se(listen_fd >= 0);
sockaddr.sun_family = AF_UNIX;
snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", temp);
if (bind(listen_fd, (struct sockaddr *) &sockaddr, sizeof (sockaddr)) != 0)
spa_assert_not_reached();
if (listen(listen_fd, 0) != 0)
spa_assert_not_reached();
res = pipe2(close_fd, O_CLOEXEC);
spa_assert_se(res >= 0);
static const struct spa_dict_item items[] = {
{ "pipewire.foo.bar", "baz" },
{ "pipewire.access", "restricted" },
};
pw_security_context_create(info.sec, "org.flatpak",
listen_fd, close_fd[1],
&SPA_DICT_INIT_ARRAY(items));
roundtrip(core, loop);
pw_main_loop_run(loop);
pw_proxy_destroy((struct pw_proxy*)info.sec);
pw_proxy_destroy((struct pw_proxy*)info.registry);
pw_context_destroy(context);
pw_main_loop_destroy(loop);
}
int main(int argc, char *argv[])
{
pw_init(&argc, &argv);
test_abi();
test_create();
pw_deinit();
return 0;
}