[RFC PATCH v2 4/9] selftests/landlock: Test listening restriction
Mikhail Ivanov
ivanov.mikhail1 at huawei-partners.com
Tue Aug 20 18:46:56 UTC 2024
8/20/2024 3:31 PM, Günther Noack wrote:
> On Wed, Aug 14, 2024 at 11:01:46AM +0800, Mikhail Ivanov wrote:
>> Add a test for listening restriction. It's similar to protocol.bind and
>> protocol.connect tests.
>>
>> Signed-off-by: Mikhail Ivanov <ivanov.mikhail1 at huawei-partners.com>
>> ---
>> tools/testing/selftests/landlock/net_test.c | 44 +++++++++++++++++++++
>> 1 file changed, 44 insertions(+)
>>
>> diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
>> index 8126f5c0160f..b6fe9bde205f 100644
>> --- a/tools/testing/selftests/landlock/net_test.c
>> +++ b/tools/testing/selftests/landlock/net_test.c
>> @@ -689,6 +689,50 @@ TEST_F(protocol, connect)
>> restricted, restricted);
>> }
>>
>> +TEST_F(protocol, listen)
>> +{
>> + if (variant->sandbox == TCP_SANDBOX) {
>> + const struct landlock_ruleset_attr ruleset_attr = {
>> + .handled_access_net = ACCESS_ALL,
>> + };
>> + const struct landlock_net_port_attr tcp_not_restricted_p0 = {
>> + .allowed_access = ACCESS_ALL,
>> + .port = self->srv0.port,
>> + };
>> + const struct landlock_net_port_attr tcp_denied_listen_p1 = {
>> + .allowed_access = ACCESS_ALL &
>> + ~LANDLOCK_ACCESS_NET_LISTEN_TCP,
>> + .port = self->srv1.port,
>> + };
>> + int ruleset_fd;
>> +
>> + ruleset_fd = landlock_create_ruleset(&ruleset_attr,
>> + sizeof(ruleset_attr), 0);
>
> Nit: The declaration and the assignment of ruleset_fd can be merged into one
> line and made const. (Not a big deal, but it was done a bit more consistently
> in the rest of the code, I think.)
Current variant is performed in every TEST_F() method. I assume that
this is required in order to not make a mess by combining the
ruleset_attr and several rule structures with the operation of creating
ruleset. WDYT?
>
>> + ASSERT_LE(0, ruleset_fd);
>> +
>> + /* Allows all actions for the first port. */
>> + ASSERT_EQ(0,
>> + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
>> + &tcp_not_restricted_p0, 0));
>> +
>> + /* Allows all actions despite listen. */
>> + ASSERT_EQ(0,
>> + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
>> + &tcp_denied_listen_p1, 0));
>> +
>> + enforce_ruleset(_metadata, ruleset_fd);
>> + EXPECT_EQ(0, close(ruleset_fd));
>> + }
>
> This entire "if (variant->sandbox == TCP_SANDBOX)" conditional does the exact
> same thing as the one from patch 5/9. Should that (or parts of it) get
> extracted into a suitable helper?
I don't think replacing
if (variant->sandbox == TCP_SANDBOX)
with
if (is_tcp_sandbox(variant))
will change anything, this condition is already quite simple. If
you think that such helper is more convenient, I can add it.
>
>> + bool restricted = is_restricted(&variant->prot, variant->sandbox);
>> +
>> + test_restricted_net_fixture(_metadata, &self->srv0, false, false,
>> + false);
>> + test_restricted_net_fixture(_metadata, &self->srv1, false, false,
>> + restricted);
>> + test_restricted_net_fixture(_metadata, &self->srv2, restricted,
>> + restricted, restricted);
>
> If we start having logic and conditionals in the test implementation (in this
> case, in test_restricted_test_fixture()), this might be a sign that that test
> implementation should maybe be split apart? Once the test is as complicated as
> the code under test, it does not simplify our confidence in the code much any
> more?
>
> (It is often considered bad practice to put conditionals in tests, e.g. in
> https://testing.googleblog.com/2014/07/testing-on-toilet-dont-put-logic-in.html)
>
> Do you think we have a way to simplify that?
I agree.. using 3 external booleans to control behavior of the
test is really messy. I believe the best we can do to avoid this is to
split "test_restricted_net_fixture()" into few independent tests. For
example we can turn this call:
test_restricted_net_fixture(_metadata, &self->srv0, false,
false, false);
into multiple smaller tests:
/* Tries to bind with invalid and minimal addrlen. */
EXPECT_EQ(0, TEST_BIND(&self->srv0));
/* Tries to connect with invalid and minimal addrlen. */
EXPECT_EQ(0, TEST_CONNECT(&self->srv0));
/* Tries to listen. */
EXPECT_EQ(0, TEST_LISTEN(&self->srv0));
/* Connection tests. */
EXPECT_EQ(0, TEST_CLIENT_SERVER(&self->srv0));
Each test is wrapped in a macro that implicitly passes _metadata argument.
This case in which every access is allowed can be wrapped in a macro:
TEST_UNRESTRICTED_NET_FIXTURE(&self->srv0);
Such approach has following cons though:
* A lot of duplicated code. These small helpers should be added to every
test that uses "test_restricted_net_fixture()". Currently there
are 16 calls of this helper.
* There is wouldn't be a single entity that is used to test a network
under different sandbox scenarios. If we split the helper each test
should care about (1) sandboxing, (2) running all required tests. For
example TEST_LISTEN() and TEST_CLIENT_SERVER() could not be called if
bind is restricted.
For example protocol.bind test would have following lines after
"test_restricted_net_fixture()" is removed:
TEST_UNRESTRICTED_NET_FIXTURE(&self->srv0);
if (is_restricted(&variant->prot, variant->sandbox)) {
EXPECT_EQ(-EACCES, TEST_BIND(&self->srv1));
EXPECT_EQ(0, TEST_CONNECT(&self->srv1));
EXPECT_EQ(-EACCES, TEST_BIND(&self->srv2));
EXPECT_EQ(-EACCES, TEST_CONNECT(&self->srv2));
} else {
TEST_UNRESTRICTED_NET_FIXTURE(&self->srv1);
TEST_UNRESTRICTED_NET_FIXTURE(&self->srv2);
}
I suggest leaving "test_restricted_net_fixture()" and refactor this
booleans (in the way you suggested) in order not to lose simplicity in
the testing:
bool restricted = is_restricted(&variant->prot,
variant->sandbox);
test_restricted_net_fixture(_metadata, &self->srv0,
(struct expected_net_enforcement){
.deny_bind = false,
.deny_connect = false,
.deny_listen = false
});
test_restricted_net_fixture(_metadata, &self->srv1,
(struct expected_net_enforcement){
.deny_bind = false,
.deny_connect = restricted,
.deny_listen = false
});
test_restricted_net_fixture(_metadata, &self->srv2,
(struct expected_net_enforcement){
.deny_bind = restricted,
.deny_connect = restricted,
.deny_listen = restricted
});
But it's really not obvious design issue and splitting helper can really
be a better solution. WDYT?
>
>
> Readability remark: I am not that strongly invested in this idea, but in the
> call to test_restricted_net_fixture(), it is difficult to understand "false,
> false, false", without jumping around in the file. Should we try to make this
> more explicit?
>
> I wonder whether we should just pass a struct, so that everything at least has a
> name?
>
> test_restricted_net_fixture((struct expected_net_enforcement){
> .deny_bind = false,
> .deny_connect = false,
> .deny_listen = false,
> });
>
> Then it would be clearer which boolean is which,
> and you could use the fact that unspecified struct fields are zero-initialized?
>
> (Alternatively, you could also spell out error codes here, instead of booleans.)
Agreed, this is a best option for refactoring.
I've also tried adding access_mask field to the service_fixture struct
with all accesses allowed by default. In a test, then you just need to
remove the necessary accesses after sandboxing:
if (is_restricted(&variant->prot, variant->sandbox))
clear_access(&self->srv2,
LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_TCP);
test_restricted_net_fixture(_metadata, &self->srv2);
But this solution is too implicit for the helper. Passing struct would
be better.
>
>> +}
>> +
>> TEST_F(protocol, bind_unspec)
>> {
>> const struct landlock_ruleset_attr ruleset_attr = {
>> --
>> 2.34.1
>>
>
> —Günther
More information about the Linux-security-module-archive
mailing list