/* * Copyright (C) 2020 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "cockpitcontrolmessages.h" #include "common/cockpitfdpassing.h" #include "common/cockpitjsonprint.h" #include "cockpitmemfdread.h" #include "cockpitsocket.h" #include "testlib/cockpittest.h" #include #include #include #include #include #include #include #include static int memfd_create_noexec (const char *name, unsigned int flags) { /* current kernels moan about not specifying exec mode */ #ifdef MFD_NOEXEC_SEAL int fd = memfd_create (name, flags | MFD_NOEXEC_SEAL); /* fallback for older kernels */ if (fd != -1 || errno != EINVAL) return fd; #endif return memfd_create (name, flags); } /* --- testing of reading --- */ static void test_memfd_simple (void) { FILE *stream; stream = cockpit_json_print_open_memfd ("test", 1); gint fd = cockpit_json_print_finish_memfd (&stream); g_autoptr(GError) error = NULL; g_autofree gchar *content = cockpit_memfd_read (fd, &error); g_assert_no_error (error); close (fd); g_assert_cmpstr (content, ==, "{\"version\": 1}"); } static void test_memfd_error_cases (void) { g_autoptr(GError) error = NULL; g_autofree gchar *content = NULL; FILE *stream; gint fd; gint r; /* not a memfd */ fd = open ("/dev/null", O_RDONLY); content = cockpit_memfd_read (fd, &error); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*not memfd?*"); g_assert (content == NULL); g_clear_error (&error); close (fd); /* memfd is not properly sealed */ fd = memfd_create_noexec ("xyz", MFD_CLOEXEC); content = cockpit_memfd_read (fd, &error); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*incorrect seals set*"); g_assert (content == NULL); g_clear_error (&error); close (fd); /* memfd is empty */ fd = memfd_create_noexec ("xyz", MFD_ALLOW_SEALING | MFD_CLOEXEC); r = fcntl (fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE); g_assert (r == 0); content = cockpit_memfd_read (fd, &error); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*empty*"); g_assert (content == NULL); g_clear_error (&error); close (fd); /* memfd is too big */ stream = cockpit_json_print_open_memfd ("xyz", 1); fprintf (stream, "%20000s", ""); fd = cockpit_json_print_finish_memfd (&stream); content = cockpit_memfd_read (fd, &error); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*unreasonably large*"); g_assert (content == NULL); g_clear_error (&error); close (fd); /* memfd can't be read */ stream = cockpit_json_print_open_memfd ("xyz", 1); int tmpfd = cockpit_json_print_finish_memfd (&stream); gchar procfile[80]; snprintf (procfile, sizeof procfile, "/proc/self/fd/%d", tmpfd); fd = open (procfile, O_WRONLY); g_assert_cmpint (fd, !=, -1); r = close (tmpfd); g_assert_cmpint (r, ==, 0); content = cockpit_memfd_read (fd, &error); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_BADF, "*failed to read*"); g_assert (content == NULL); g_clear_error (&error); close (fd); /* memfd contains a nul */ stream = cockpit_json_print_open_memfd ("xyz", 1); fputc (0, stream); fd = cockpit_json_print_finish_memfd (&stream); content = cockpit_memfd_read (fd, &error); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*contains nul*"); g_assert (content == NULL); g_clear_error (&error); close (fd); /* memfd contains non-ascii */ stream = cockpit_json_print_open_memfd ("xyz", 1); fputc (0xcc, stream); fd = cockpit_json_print_finish_memfd (&stream); content = cockpit_memfd_read (fd, &error); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*contains non-ASCII*"); g_assert (content == NULL); g_clear_error (&error); close (fd); } static void test_memfd_json (void) { FILE *stream; stream = cockpit_json_print_open_memfd ("test", 1); cockpit_json_print_string_property (stream, "hello", "world", -1); cockpit_json_print_integer_property (stream, "size", 200); cockpit_json_print_bool_property (stream, "truth", true); cockpit_json_print_bool_property (stream, "falsth", false); gint fd = cockpit_json_print_finish_memfd (&stream); g_autoptr(GError) error = NULL; g_autoptr(JsonObject) object = cockpit_memfd_read_json (fd, &error); g_assert_no_error (error); close (fd); g_assert_cmpint (json_object_get_int_member (object, "version"), ==, 1); g_assert_cmpstr (json_object_get_string_member (object, "hello"), ==, "world"); g_assert_cmpint (json_object_get_int_member (object, "size"), ==, 200); g_assert_cmpint (json_object_get_boolean_member (object, "truth"), ==, TRUE); g_assert_cmpint (json_object_get_boolean_member (object, "falsth"), ==, FALSE); } static void test_memfd_json_error_cases (void) { g_autoptr(GError) error = NULL; g_autoptr(JsonObject) object = NULL; gint fd; gint r; /* invalid json */ fd = memfd_create_noexec ("xyz", MFD_CLOEXEC | MFD_ALLOW_SEALING); g_assert_cmpint (write (fd, "beh", 3), ==, 3); r = fcntl (fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE); g_assert (r == 0); object = cockpit_memfd_read_json (fd, &error); g_assert (object == NULL); cockpit_assert_error_matches (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_INVALID_BAREWORD, "*unexpected identifier*"); g_clear_error (&error); close (fd); /* valid json, but not an object */ fd = memfd_create_noexec ("xyz", MFD_CLOEXEC | MFD_ALLOW_SEALING); g_assert_cmpint (write (fd, "[]", 2), ==, 2); r = fcntl (fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE); g_assert (r == 0); object = cockpit_memfd_read_json (fd, &error); g_assert (object == NULL); cockpit_assert_error_matches (error, JSON_PARSER_ERROR, -1, "*Not a JSON object*"); close (fd); } /* --- unix socket testing --- */ static GSList *live_control_messages; static void assert_live_control_messages (gint expected) { g_assert_cmpint (g_slist_length (live_control_messages), ==, expected); } static void remove_message_from_list (gpointer data, GObject *where_the_object_was) { for (GSList **n = &live_control_messages; *n; n = &(*n)->next) if ((*n)->data == where_the_object_was) { *n = g_slist_delete_link (*n, *n); return; } g_error ("Couldn't find control message %p in list", where_the_object_was); } static void receive_cmsgs (GSocket *socket, CockpitControlMessages *ccm) { gchar buffer[1]; GInputVector vector = { buffer, sizeof buffer }; GError *error = NULL; g_socket_receive_message (socket, NULL, /* address */ &vector, 1, &ccm->messages, &ccm->n_messages, NULL, NULL, &error); /* Use this to make sure all messages are getting properly freed */ for (gint i = 0; i < ccm->n_messages; i++) { live_control_messages = g_slist_prepend (live_control_messages, ccm->messages[i]); g_object_weak_ref (G_OBJECT (ccm->messages[i]), remove_message_from_list, NULL); } g_assert_no_error (error); } static void receive_nothing (GSocket *socket) { g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT; receive_cmsgs (socket, &ccm); g_assert (cockpit_control_messages_empty (&ccm)); } static gint * receive_fds (GSocket *socket, gint *out_nfds, GError **error) { g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT; receive_cmsgs (socket, &ccm); int n_fds; const gint *fds = cockpit_control_messages_peek_fd_list (&ccm, &n_fds, error); if (fds == NULL) return NULL; gint *result = g_new (int, n_fds + 1); for (gint i = 0; i < n_fds; i++) result[i] = dup (fds[i]); result[n_fds] = -1; *out_nfds = n_fds; return result; } static void free_fds (gint **inout_fds, gint *inout_nfds) { gint *fds = *inout_fds; gint nfds = *inout_nfds; for (gint i = 0; i < nfds; i++) { g_assert (fds[i] != -1); int r = close (fds[i]); g_assert (r == 0); } g_assert (fds[nfds] == -1); g_free (fds); *inout_fds = NULL; *inout_nfds = 0; } static gint receive_fd (GSocket *socket, GError **error) { g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT; receive_cmsgs (socket, &ccm); int fd = cockpit_control_messages_peek_single_fd (&ccm, error); if (fd == -1) return -1; return dup (fd); } static void send_cmsgs (GSocket *socket, GSocketControlMessage **messages, gint n_messages, gint n_bytes) { const gchar buffer[100] = ""; g_assert_cmpint(n_bytes, <=, sizeof buffer); GOutputVector vector = { buffer, n_bytes }; GError *error = NULL; g_socket_send_message (socket, NULL, /* address */ &vector, 1, messages, n_messages, 0, NULL, &error); g_assert_no_error (error); } static void send_nothing (GSocket *socket, gint n_bytes) { send_cmsgs (socket, NULL, 0, n_bytes); } static GSocketControlMessage * make_fd_message (const gint *fds, gint n_fds) { g_autoptr(GUnixFDList) fdl = g_unix_fd_list_new (); for (gint i = 0; i < n_fds; i++) { GError *error = NULL; g_unix_fd_list_append (fdl, fds[i], &error); g_assert_no_error (error); } return g_unix_fd_message_new_with_fd_list (fdl); } static void send_fds (GSocket *socket, const gint *fds, gint n_fds) { g_autoptr(GSocketControlMessage) fdm = make_fd_message (fds, n_fds); send_cmsgs (socket, &fdm, 1, 1); } static void send_fd (GSocket *socket, gint fd) { send_fds (socket, &fd, 1); } static void assert_base_state (GSocket *one, GSocket *two) { assert_live_control_messages (0); g_assert (g_socket_condition_check (one, G_IO_IN | G_IO_OUT) == G_IO_OUT); g_assert (g_socket_condition_check (two, G_IO_IN | G_IO_OUT) == G_IO_OUT); } static void test_unix_socket_simple (void) { g_autoptr(GSocket) one, two; cockpit_socket_socketpair (&one, &two); assert_base_state (one, two); /* boring */ send_nothing (one, 1); receive_nothing (two); assert_base_state (one, two); send_nothing (two, 1); receive_nothing (one); assert_base_state (one, two); /* try a single fd */ send_fd (one, 1); GError *error = NULL; gint fd = receive_fd (two, &error); g_assert_no_error (error); g_assert (fd != -1); close (fd); assert_base_state (one, two); /* try multiple fds */ send_fds (one, (gint []){ 0, 1, 2}, 3); gint n_fds = 0; /* gcc is unhappy without this... */ gint *fds = receive_fds (two, &n_fds, &error); g_assert_no_error (error); g_assert (fds != NULL); g_assert_cmpint (n_fds, ==, 3); free_fds (&fds, &n_fds); assert_base_state (one, two); /* mix-and-match with cockpitfdpassing */ int two_fd = g_socket_get_fd (two); g_unix_set_fd_nonblocking (two_fd, FALSE, &error); g_assert_no_error (error); /* one -> two */ send_fd (one, 1); int r = cockpit_socket_receive_fd (two_fd, &fd); g_assert_cmpint (r, ==, 1); g_assert (fd != -1); close (fd); /* two -> one */ cockpit_socket_send_fd (two_fd, 1); fd = receive_fd (one, &error); g_assert_no_error (error); g_assert (fd != -1); close (fd); assert_base_state (one, two); } static void test_unix_socket_partial_read (void) { g_autoptr(GSocket) one, two; cockpit_socket_socketpair (&one, &two); assert_base_state (one, two); /* test unspecified behaviour, which we rely on: the cmsg should be * read with the first byte of the message with which it was sent. * * we depend on this because we start on the cockpit-ws side by * reading a single byte, but we will send the json blob as part of * the first full packet from cockpit-tls. */ send_nothing (one, 10); int fd1 = 1; g_autoptr(GSocketControlMessage) fdm = make_fd_message (&fd1, 1); send_cmsgs (one, &fdm, 1, 10); for (gint i = 0; i < 20; i++) { g_autoptr(GError) error = NULL; gint fd = receive_fd (two, &error); if (fd != -1) { /* we expect to get this at the 11th try */ g_assert_cmpint (i, ==, 10); close (fd); } else { cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*0 control message*"); } } } static void test_unix_socket_error_cases (void) { g_autoptr(GSocket) one, two; cockpit_socket_socketpair (&one, &two); assert_base_state (one, two); /* try receiving an fd when nothing was sent */ send_nothing (one, 1); GError *error = NULL; int fd = receive_fd (two, &error); g_assert (fd == -1); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*0 control message*"); g_clear_error (&error); assert_base_state (one, two); /* see what happens if we send more fds than expected */ send_fds (one, (const gint []){ 0, 1, 2}, 3); fd = receive_fd (two, &error); g_assert (fd == -1); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*received 3*1 expected*"); g_clear_error (&error); assert_base_state (one, two); /* The remaining tests rely on receiving SCM_CREDENTIALS. We need to * enable SO_PASSCRED for that. */ int truth = 1; int r = setsockopt (g_socket_get_fd (two), SOL_SOCKET, SO_PASSCRED, &truth, sizeof truth); g_assert (r == 0); /* see what happens if we send the wrong message type */ g_autoptr(GSocketControlMessage) creds = g_unix_credentials_message_new (); send_cmsgs (one, &creds, 1, 1); fd = receive_fd (two, &error); g_assert (fd == -1); cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*GUnixCredentialsMessage*GUnixFDMessage expected*"); g_clear_error (&error); assert_base_state (one, two); /* see what happens if we send too many messages */ g_autoptr(GUnixFDList) fdl = g_unix_fd_list_new (); g_unix_fd_list_append (fdl, 1, &error); g_assert_no_error (error); g_autoptr(GSocketControlMessage) fdm = g_unix_fd_message_new_with_fd_list (fdl); GSocketControlMessage *messages[] = { creds, fdm }; send_cmsgs (one, messages, G_N_ELEMENTS (messages), 1); fd = receive_fd (two, &error); g_assert (fd == -1); g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); g_assert (strstr (error->message, "2 control messages (one message")); g_clear_error (&error); assert_base_state (one, two); } /* --- putting it all together (unix sockets) --- */ static void test_unix_socket_combined (void) { g_autoptr(GSocket) one, two; cockpit_socket_socketpair (&one, &two); assert_base_state (one, two); FILE *stream = cockpit_json_print_open_memfd ("xyz", 1); cockpit_json_print_string_property (stream, "test", "it worked!", -1); gint fd = cockpit_json_print_finish_memfd (&stream); send_fd (one, fd); close (fd); g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT; receive_cmsgs (two, &ccm); g_autoptr(GError) error = NULL; g_autoptr(JsonObject) json = cockpit_memfd_read_json_from_control_messages (&ccm, &error); g_assert_no_error (error); g_assert_cmpint (json_object_get_int_member (json, "version"), ==, 1); g_assert_cmpstr (json_object_get_string_member (json, "test"), ==, "it worked!"); } int main (int argc, char **argv) { cockpit_test_init (&argc, &argv); g_test_add_func ("/json/fd/memfd/simple", test_memfd_simple); g_test_add_func ("/json/fd/memfd/error-cases", test_memfd_error_cases); g_test_add_func ("/json/fd/memfd/json", test_memfd_json); g_test_add_func ("/json/fd/memfd/json/error-cases", test_memfd_json_error_cases); g_test_add_func ("/json/fd/unix-socket/simple", test_unix_socket_simple); g_test_add_func ("/json/fd/unix-socket/partial-read", test_unix_socket_partial_read); g_test_add_func ("/json/fd/unix-socket/error-cases", test_unix_socket_error_cases); g_test_add_func ("/json/fd/unix-socket/combined", test_unix_socket_combined); return g_test_run (); }