blob: 8452216a865774862859ce725648855c68a7f99b [file] [edit]
/*
* Copyright © 2026 Canonical Ltd.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "in_process_server.h"
#include "helpers.h"
#include "version_specifier.h"
#include "expect_protocol_error.h"
#include <gmock/gmock.h>
#include <boost/throw_exception.hpp>
#include <cerrno>
#include <sys/mman.h>
#include <unistd.h>
#include <system_error>
#include <vector>
using namespace testing;
namespace
{
struct ShmTest : wlcs::StartedInProcessServer
{
wlcs::Client client{the_server()};
};
// A separate fixture, identical to ShmTest, used to spin up a second server
// instance (see truncated_shm_file_is_an_error_on_a_second_server_instance).
struct SecondShmTest : wlcs::StartedInProcessServer
{
wlcs::Client client{the_server()};
};
/// Binds a fresh wl_shm and collects the format events the server sends on bind.
std::vector<uint32_t> collect_advertised_formats(wlcs::Client& client)
{
static std::vector<uint32_t> formats;
formats.clear();
static wl_shm_listener const listener{
[](void*, wl_shm*, uint32_t format)
{
formats.push_back(format);
}};
auto shm = static_cast<wl_shm*>(
client.bind_if_supported(wl_shm_interface, wlcs::AnyVersion));
wl_shm_add_listener(shm, &listener, nullptr);
// Format events are delivered in response to the bind request.
client.roundtrip();
wl_shm_destroy(shm);
return formats;
}
// Creates a wl_buffer whose backing file is then truncated to a tiny size, so
// that the compositor will access it out-of-bounds (and hit SIGBUS) if it does
// not validate the buffer. Used to check the server handles this gracefully.
struct wl_buffer* create_bad_shm_buffer(wlcs::Client& client, int width, int height)
{
struct wl_shm* shm = client.shm();
int stride = width * 4;
int size = stride * height;
int fd = wlcs::helpers::create_anonymous_file(size);
auto pool = wl_shm_create_pool(shm, fd, size);
auto buffer = wl_shm_pool_create_buffer(
pool,
0,
width,
height,
stride,
WL_SHM_FORMAT_ARGB8888);
wl_shm_pool_destroy(pool);
// Truncate the file to a small size, so that the compositor will access it
// out-of-bounds, and hit SIGBUS.
if (ftruncate(fd, 12) == -1)
{
close(fd);
BOOST_THROW_EXCEPTION(
std::system_error(errno, std::system_category(), "Failed to truncate temporary file"));
}
close(fd);
return buffer;
}
}
// Creating a pool from a file descriptor that cannot be mapped must be rejected.
// A pipe is not mappable, so it serves as a conveniently-invalid fd.
TEST_F(ShmTest, create_pool_with_invalid_fd_is_an_error)
{
int pipe_fds[2];
ASSERT_THAT(pipe(pipe_fds), Eq(0));
EXPECT_PROTOCOL_ERROR(
{
auto pool = wl_shm_create_pool(client.shm(), pipe_fds[0], 4);
wl_shm_pool_destroy(pool);
client.roundtrip();
},
&wl_shm_interface,
WL_SHM_ERROR_INVALID_FD);
close(pipe_fds[0]);
close(pipe_fds[1]);
}
// Creating a pool with a non-positive size must be rejected.
TEST_F(ShmTest, create_pool_with_negative_size_is_an_error)
{
auto fd = wlcs::helpers::create_anonymous_file(64);
EXPECT_PROTOCOL_ERROR(
{
auto pool = wl_shm_create_pool(client.shm(), fd, -1);
wl_shm_pool_destroy(pool);
client.roundtrip();
},
&wl_shm_interface,
WL_SHM_ERROR_INVALID_STRIDE);
close(fd);
}
// The wl_shm protocol mandates that ARGB8888 and XRGB8888 are always supported.
TEST_F(ShmTest, advertises_mandatory_formats)
{
auto const formats = collect_advertised_formats(client);
EXPECT_THAT(formats, Contains(Eq(WL_SHM_FORMAT_ARGB8888)));
EXPECT_THAT(formats, Contains(Eq(WL_SHM_FORMAT_XRGB8888)));
}
// Requesting a buffer with a format the server never advertised must be rejected.
TEST_F(ShmTest, create_buffer_with_unknown_format_is_an_error)
{
int const width = 4;
int const height = 4;
int const stride = width * 4;
int const size = stride * height;
auto const bogus_format = 0xDEADBEEF;
auto const fd = wlcs::helpers::create_anonymous_file(size);
auto const pool = wl_shm_create_pool(client.shm(), fd, size);
close(fd);
EXPECT_PROTOCOL_ERROR(
{
wl_shm_pool_create_buffer(
pool, 0, width, height, stride, bogus_format);
client.roundtrip();
},
&wl_shm_pool_interface,
WL_SHM_ERROR_INVALID_FORMAT);
wl_shm_pool_destroy(pool);
}
// A buffer that extends beyond the bounds of its pool must be rejected.
TEST_F(ShmTest, create_buffer_exceeding_pool_size_is_an_error)
{
int const width = 4, height = 4;
int const stride = width * 4;
int const size = stride * height;
auto fd = wlcs::helpers::create_anonymous_file(size);
auto pool = wl_shm_create_pool(client.shm(), fd, size);
close(fd);
EXPECT_PROTOCOL_ERROR(
{
// Offsetting by a full row pushes the buffer past the end of the pool.
int const offset = stride;
wl_shm_pool_create_buffer(
pool, offset, width, height, stride, WL_SHM_FORMAT_ARGB8888);
client.roundtrip();
},
&wl_shm_pool_interface,
WL_SHM_ERROR_INVALID_STRIDE);
wl_shm_pool_destroy(pool);
}
// Growing a pool must allow buffers to be created in the newly-available space.
TEST_F(ShmTest, pool_can_be_resized_larger)
{
int const width = 4, height = 4;
int const stride = width * 4;
int const buffer_size = stride * height;
auto fd = wlcs::helpers::create_anonymous_file(buffer_size * 2);
auto pool = wl_shm_create_pool(client.shm(), fd, buffer_size);
close(fd);
auto first = wl_shm_pool_create_buffer(
pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888);
wl_shm_pool_resize(pool, buffer_size * 2);
auto second = wl_shm_pool_create_buffer(
pool, buffer_size, width, height, stride, WL_SHM_FORMAT_ARGB8888);
// No protocol error should be raised for a valid resize and second buffer.
client.roundtrip();
wl_buffer_destroy(first);
wl_buffer_destroy(second);
wl_shm_pool_destroy(pool);
}
// A single pool may back several independent buffers simultaneously.
TEST_F(ShmTest, multiple_buffers_can_share_a_pool)
{
int const width = 4, height = 4;
int const stride = width * 4;
int const buffer_size = stride * height;
int const buffer_count = 3;
int const pool_size = buffer_size * buffer_count;
auto fd = wlcs::helpers::create_anonymous_file(pool_size);
auto pool = wl_shm_create_pool(client.shm(), fd, pool_size);
close(fd);
std::vector<wl_buffer*> buffers;
for (int i = 0; i < buffer_count; ++i)
{
buffers.push_back(wl_shm_pool_create_buffer(
pool,
i * buffer_size,
width, height,
stride,
WL_SHM_FORMAT_ARGB8888));
}
// Destroying the pool must not invalidate the buffers created from it.
wl_shm_pool_destroy(pool);
client.roundtrip();
for (auto buffer : buffers)
wl_buffer_destroy(buffer);
}
// Attaching a buffer whose backing file has been truncated must not crash the
// compositor; it should raise a protocol error instead.
TEST_F(ShmTest, truncated_shm_file_is_an_error)
{
auto surface = client.create_visible_surface(200, 200);
wl_buffer* bad_buffer = create_bad_shm_buffer(client, 200, 200);
wl_surface_attach(surface, bad_buffer, 0, 0);
wl_surface_damage(surface, 0, 0, 200, 200);
wl_surface_commit(surface);
// We dispatch until we receive the protocol error, or hit the timeout.
EXPECT_PROTOCOL_ERROR(
{
client.dispatch_until([]() { return false; });
},
&wl_buffer_interface,
WL_SHM_ERROR_INVALID_FD);
wl_buffer_destroy(bad_buffer);
}
// This should behave identically to truncated_shm_file_is_an_error, but runs in
// a second server instance. There have been issues with the server installing a
// SIGBUS handler (via wl_shm_buffer_begin_access()) that only works for the
// first server instance.
TEST_F(SecondShmTest, truncated_shm_file_is_an_error_on_a_second_server_instance)
{
auto surface = client.create_visible_surface(200, 200);
wl_buffer* bad_buffer = create_bad_shm_buffer(client, 200, 200);
wl_surface_attach(surface, bad_buffer, 0, 0);
wl_surface_damage(surface, 0, 0, 200, 200);
wl_surface_commit(surface);
EXPECT_PROTOCOL_ERROR(
{
client.dispatch_until([]() { return false; });
},
&wl_buffer_interface,
WL_SHM_ERROR_INVALID_FD);
wl_buffer_destroy(bad_buffer);
}
// Creating a buffer with a stride too small for its declared width must be
// rejected.
TEST_F(ShmTest, client_lies_about_buffer_size)
{
auto const width = 200, height = 200;
auto const incorrect_stride = width;
auto fd = wlcs::helpers::create_anonymous_file(height * incorrect_stride);
auto shm_pool = wl_shm_create_pool(client.shm(), fd, height * incorrect_stride);
close(fd);
auto bad_buffer = wl_shm_pool_create_buffer(
shm_pool,
0,
width, height,
incorrect_stride, // Stride is in bytes, not pixels, so this is ¼ the correct value.
WL_SHM_FORMAT_ARGB8888);
EXPECT_PROTOCOL_ERROR(
{
// Buffer creation should fail, so all we need is for the
// create_buffer call to be processed.
client.roundtrip();
},
&wl_shm_pool_interface,
WL_SHM_ERROR_INVALID_STRIDE);
wl_buffer_destroy(bad_buffer);
wl_shm_pool_destroy(shm_pool);
}