diff --git a/src/modules/meson.build b/src/modules/meson.build index 40d5662de..8a5635f59 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -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, diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c index e30dd2e32..786319f6a 100644 --- a/src/modules/module-protocol-native.c +++ b/src/modules/module-protocol-native.c @@ -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); diff --git a/src/modules/module-protocol-native/protocol-native.c b/src/modules/module-protocol-native/protocol-native.c index 1681514eb..79a5bd362 100644 --- a/src/modules/module-protocol-native/protocol-native.c +++ b/src/modules/module-protocol-native/protocol-native.c @@ -11,6 +11,7 @@ #include #include +#include #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); } diff --git a/src/modules/module-protocol-native/security-context.c b/src/modules/module-protocol-native/security-context.c new file mode 100644 index 000000000..709f13721 --- /dev/null +++ b/src/modules/module-protocol-native/security-context.c @@ -0,0 +1,132 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + + +#include +#include +#include +#include + +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; +} diff --git a/src/pipewire/extensions/security-context.h b/src/pipewire/extensions/security-context.h new file mode 100644 index 000000000..211ec400c --- /dev/null +++ b/src/pipewire/extensions/security-context.h @@ -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 + +/** \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 */ diff --git a/src/pipewire/protocol.h b/src/pipewire/protocol.h index 8cd9c7bcf..60a029033 100644 --- a/src/pipewire/protocol.h +++ b/src/pipewire/protocol.h @@ -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); diff --git a/src/tests/meson.build b/src/tests/meson.build index f7c54f2b7..e66e36f3f 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -4,6 +4,7 @@ test_apps = [ # 'test-remote', 'test-stream', 'test-filter', + 'test-security-context', ] foreach a : test_apps diff --git a/src/tests/test-security-context.c b/src/tests/test-security-context.c new file mode 100644 index 000000000..7d413f562 --- /dev/null +++ b/src/tests/test-security-context.c @@ -0,0 +1,168 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#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, ®istry_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; +}