diff mbox series

[2/2] landlock: Selftests for truncate(2) support.

Message ID 20220707200612.132705-3-gnoack3000@gmail.com (mailing list archive)
State Handled Elsewhere
Headers show
Series landlock: truncate(2) support | expand

Commit Message

Günther Noack July 7, 2022, 8:06 p.m. UTC
These tests exercise the following scenarios:

* File with Read, Write, Truncate rights.
* File with Read, Write rights.
* File with Truncate rights.
* File with no rights.
* Directory with Truncate rights.

For each of the scenarios, both truncate() and the open() +
ftruncate() syscalls get exercised and their results checked.

In particular, the test demonstrates that opening a file for writing
is not enough to call truncate().

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 tools/testing/selftests/landlock/fs_test.c | 80 ++++++++++++++++++++++
 1 file changed, 80 insertions(+)

Comments

Mickaël Salaün July 8, 2022, 11:17 a.m. UTC | #1
Please use "selftests/landlock:" as subject prefix and without a final dot.


On 07/07/2022 22:06, Günther Noack wrote:
> These tests exercise the following scenarios:
> 
> * File with Read, Write, Truncate rights.

Should we use a capital for access right names or does it come from Go? ;)


> * File with Read, Write rights.
> * File with Truncate rights.
> * File with no rights.
> * Directory with Truncate rights.
> 
> For each of the scenarios, both truncate() and the open() +
> ftruncate() syscalls get exercised and their results checked.
> 
> In particular, the test demonstrates that opening a file for writing
> is not enough to call truncate().

Looks good! According to my previous comment, O_TRUNC should be tested 
if it is checked by the kernel.


> 
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>   tools/testing/selftests/landlock/fs_test.c | 80 ++++++++++++++++++++++
>   1 file changed, 80 insertions(+)
> 
> diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> index cb77eaa01c91..c3e48fd12b2b 100644
> --- a/tools/testing/selftests/landlock/fs_test.c
> +++ b/tools/testing/selftests/landlock/fs_test.c
> @@ -2237,6 +2237,86 @@ TEST_F_FORK(layout1, reparent_rename)
>   	ASSERT_EQ(EXDEV, errno);
>   }
>   
> +TEST_F_FORK(layout1, truncate)

Please move this test after the proc_pipe one.


> +{
> +	const struct rule rules[] = {

You can add a first layer of rules to check truncate and ftruncate with 
a ruleset not handling LANDLOCK_ACCESS_FS_TRUNCATE.


> +		{
> +			.path = file1_s1d1,
> +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> +				  LANDLOCK_ACCESS_FS_WRITE_FILE |
> +				  LANDLOCK_ACCESS_FS_TRUNCATE,
> +		},
> +		{
> +			.path = file2_s1d2,
> +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> +				  LANDLOCK_ACCESS_FS_WRITE_FILE,
> +		},
> +		{
> +			.path = file1_s1d2,
> +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
> +		},

Move this entry before file2_s1d2 to keep the paths sorted and make this 
easier to read. You can change the access rights per path to also keep 
their ordering though.


> +		{
> +			.path = dir_s2d3,
> +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
> +		},
> +		// Implicitly: No access rights for file2_s1d1.

Comment to move after the use of file1_s1d1.

> +		{},
> +	};
> +	const int ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, rules);

Don't use ACCESS_ALL because it will change over time and we want tests 
to be deterministic. You can use rules[0].access instead.


> +	int reg_fd;
> +
> +	ASSERT_LE(0, ruleset_fd);
> +	enforce_ruleset(_metadata, ruleset_fd);
> +	ASSERT_EQ(0, close(ruleset_fd));
> +
> +	/* Read, write and truncate permissions => truncate and ftruncate work. */

It would be nice to have consistent comments such as: "Checks read, 
write and truncate access rights: truncate and ftruncate work."


> +	reg_fd = open(file1_s1d1, O_RDWR | O_CLOEXEC);
> +	ASSERT_LE(0, reg_fd);
> +	EXPECT_EQ(0, ftruncate(reg_fd, 10));

You should not use EXPECT but ASSERT here. I use EXPECT when an error 
could block a test or when it could stop a cleanup (i.e. teardown).


> +	EXPECT_EQ(0, ftruncate64(reg_fd, 20));
> +	ASSERT_EQ(0, close(reg_fd));
> +
> +	EXPECT_EQ(0, truncate(file1_s1d1, 10));
> +	EXPECT_EQ(0, truncate64(file1_s1d1, 20));
> +
> +	/* Just read and write permissions => no truncate variant works. */
> +	reg_fd = open(file2_s1d2, O_RDWR | O_CLOEXEC);
> +	ASSERT_LE(0, reg_fd);
> +	EXPECT_EQ(-1, ftruncate(reg_fd, 10));
> +	EXPECT_EQ(EACCES, errno);
> +	EXPECT_EQ(-1, ftruncate64(reg_fd, 20));
> +	EXPECT_EQ(EACCES, errno);
> +	ASSERT_EQ(0, close(reg_fd));
> +
> +	EXPECT_EQ(-1, truncate(file2_s1d2, 10));
> +	EXPECT_EQ(EACCES, errno);
> +	EXPECT_EQ(-1, truncate64(file2_s1d2, 20));
> +	EXPECT_EQ(EACCES, errno);
> +
> +	/* Just truncate permissions => truncate(64) works, but can't open file. */
> +	ASSERT_EQ(-1, open(file1_s1d2, O_RDWR | O_CLOEXEC));
> +	ASSERT_EQ(EACCES, errno);
> +
> +	EXPECT_EQ(0, truncate(file1_s1d2, 10));
> +	EXPECT_EQ(0, truncate64(file1_s1d2, 20));
> +
> +	/* Just truncate permission on directory => truncate(64) works, but can't open file. */
> +	ASSERT_EQ(-1, open(file1_s2d3, O_RDWR | O_CLOEXEC));
> +	ASSERT_EQ(EACCES, errno);
> +
> +	EXPECT_EQ(0, truncate(file1_s2d3, 10));
> +	EXPECT_EQ(0, truncate64(file1_s2d3, 20));
> +
> +	/* No permissions => Neither truncate nor ftruncate work. */
> +	ASSERT_EQ(-1, open(file2_s1d1, O_RDWR | O_CLOEXEC));
> +	ASSERT_EQ(EACCES, errno);
> +
> +	EXPECT_EQ(-1, truncate(file2_s1d1, 10));
> +	EXPECT_EQ(EACCES, errno);
> +	EXPECT_EQ(-1, truncate64(file2_s1d1, 20));
> +	EXPECT_EQ(EACCES, errno);

These tests are good!

> +}
> +
>   static void
>   reparent_exdev_layers_enforce1(struct __test_metadata *const _metadata)
>   {
Günther Noack July 11, 2022, 4:27 p.m. UTC | #2
On Fri, Jul 08, 2022 at 01:17:46PM +0200, Mickaël Salaün wrote:
> Please use "selftests/landlock:" as subject prefix and without a final dot.
>
>
> On 07/07/2022 22:06, Günther Noack wrote:
> > These tests exercise the following scenarios:
> >
> > * File with Read, Write, Truncate rights.
>
> Should we use a capital for access right names or does it come from Go? ;)

Done. Will be included in the next version.

>
>
> > * File with Read, Write rights.
> > * File with Truncate rights.
> > * File with no rights.
> > * Directory with Truncate rights.
> >
> > For each of the scenarios, both truncate() and the open() +
> > ftruncate() syscalls get exercised and their results checked.
> >
> > In particular, the test demonstrates that opening a file for writing
> > is not enough to call truncate().
>
> Looks good! According to my previous comment, O_TRUNC should be tested if it
> is checked by the kernel.

Done. Will be included in the next version.

>
>
> >
> > Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> > ---
> >   tools/testing/selftests/landlock/fs_test.c | 80 ++++++++++++++++++++++
> >   1 file changed, 80 insertions(+)
> >
> > diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> > index cb77eaa01c91..c3e48fd12b2b 100644
> > --- a/tools/testing/selftests/landlock/fs_test.c
> > +++ b/tools/testing/selftests/landlock/fs_test.c
> > @@ -2237,6 +2237,86 @@ TEST_F_FORK(layout1, reparent_rename)
> >   	ASSERT_EQ(EXDEV, errno);
> >   }
> > +TEST_F_FORK(layout1, truncate)
>
> Please move this test after the proc_pipe one.

Done. Will be included in the next version.

>
>
> > +{
> > +	const struct rule rules[] = {
>
> You can add a first layer of rules to check truncate and ftruncate with a
> ruleset not handling LANDLOCK_ACCESS_FS_TRUNCATE.

Done. I'll add a separate test for that which will exercise the
various truncation APIs in a scenario where the ruleset does not
handle LANDLOCK_ACCESS_FS_TRUNCATE, so that it's not restricted.

Will be included in the next version.

>
>
> > +		{
> > +			.path = file1_s1d1,
> > +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> > +				  LANDLOCK_ACCESS_FS_WRITE_FILE |
> > +				  LANDLOCK_ACCESS_FS_TRUNCATE,
> > +		},
> > +		{
> > +			.path = file2_s1d2,
> > +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> > +				  LANDLOCK_ACCESS_FS_WRITE_FILE,
> > +		},
> > +		{
> > +			.path = file1_s1d2,
> > +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
> > +		},
>
> Move this entry before file2_s1d2 to keep the paths sorted and make this
> easier to read. You can change the access rights per path to also keep their
> ordering though.

I've admittedly found it difficult to remember which of these files
and subdirectories exist and how they are named and mixed up the names
at least twice when developing these tests. To make it easier, I've now
renamed these by including this at the top of the test:

char *file_rwt = file1_s1d1;
char *file_rw = file2_s1s1;
// etc

With the test using names like file_rwt, I find that easier to work
with and found myself jumping less between the "rules" on top and the
place where the assertions are written out.

This is admittedly a bit out of line with the other tests, but maybe
it's worth doing? Let me know what you think.

>
>
> > +		{
> > +			.path = dir_s2d3,
> > +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
> > +		},
> > +		// Implicitly: No access rights for file2_s1d1.
>
> Comment to move after the use of file1_s1d1.

I'm understanding this as "keep the files in order according to the
layout". I've reshuffled things a bit by renaming them, but this is
also in the right order now.

>
> > +		{},
> > +	};
> > +	const int ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, rules);
>
> Don't use ACCESS_ALL because it will change over time and we want tests to
> be deterministic. You can use rules[0].access instead.
>
>
> > +	int reg_fd;
> > +
> > +	ASSERT_LE(0, ruleset_fd);
> > +	enforce_ruleset(_metadata, ruleset_fd);
> > +	ASSERT_EQ(0, close(ruleset_fd));
> > +
> > +	/* Read, write and truncate permissions => truncate and ftruncate work. */
>
> It would be nice to have consistent comments such as: "Checks read, write
> and truncate access rights: truncate and ftruncate work."

Done. Will be included in next version.

>
>
> > +	reg_fd = open(file1_s1d1, O_RDWR | O_CLOEXEC);
> > +	ASSERT_LE(0, reg_fd);
> > +	EXPECT_EQ(0, ftruncate(reg_fd, 10));
>
> You should not use EXPECT but ASSERT here. I use EXPECT when an error could
> block a test or when it could stop a cleanup (i.e. teardown).

ASSERT is the variant that stops the test immediately, whereas EXPECT
notes down the test failure and continues execution.

So in that spirit, I tried to use:

 * ASSERT for successful open() calls where the FD is still needed later
 * ASSERT for close() (for symmetry with open())
 * EXPECT for expected-failing open() calls where the FD is not used later
 * EXPECT for everything else

I had another pass over the tests and have started to use EXPECT for a
few expected-failing open() calls.

The selftest framework seems inspired by the Googletest framework
(https://google.github.io/googletest/primer.html#assertions) where
this is described as: "Usually EXPECT_* are preferred, as they allow
more than one failure to be reported in a test. However, you should
use ASSERT_* if it doesn’t make sense to continue when the assertion
in question fails."

I imagined that the same advice would apply to the kernel selftests?
Please let me know if I'm overlooking subtle differences here.

>
>
> > +	EXPECT_EQ(0, ftruncate64(reg_fd, 20));
> > +	ASSERT_EQ(0, close(reg_fd));
> > +
> > +	EXPECT_EQ(0, truncate(file1_s1d1, 10));
> > +	EXPECT_EQ(0, truncate64(file1_s1d1, 20));
> > +
> > +	/* Just read and write permissions => no truncate variant works. */
> > +	reg_fd = open(file2_s1d2, O_RDWR | O_CLOEXEC);
> > +	ASSERT_LE(0, reg_fd);
> > +	EXPECT_EQ(-1, ftruncate(reg_fd, 10));
> > +	EXPECT_EQ(EACCES, errno);
> > +	EXPECT_EQ(-1, ftruncate64(reg_fd, 20));
> > +	EXPECT_EQ(EACCES, errno);
> > +	ASSERT_EQ(0, close(reg_fd));
> > +
> > +	EXPECT_EQ(-1, truncate(file2_s1d2, 10));
> > +	EXPECT_EQ(EACCES, errno);
> > +	EXPECT_EQ(-1, truncate64(file2_s1d2, 20));
> > +	EXPECT_EQ(EACCES, errno);
> > +
> > +	/* Just truncate permissions => truncate(64) works, but can't open file. */
> > +	ASSERT_EQ(-1, open(file1_s1d2, O_RDWR | O_CLOEXEC));
> > +	ASSERT_EQ(EACCES, errno);
> > +
> > +	EXPECT_EQ(0, truncate(file1_s1d2, 10));
> > +	EXPECT_EQ(0, truncate64(file1_s1d2, 20));
> > +
> > +	/* Just truncate permission on directory => truncate(64) works, but can't open file. */
> > +	ASSERT_EQ(-1, open(file1_s2d3, O_RDWR | O_CLOEXEC));
> > +	ASSERT_EQ(EACCES, errno);
> > +
> > +	EXPECT_EQ(0, truncate(file1_s2d3, 10));
> > +	EXPECT_EQ(0, truncate64(file1_s2d3, 20));
> > +
> > +	/* No permissions => Neither truncate nor ftruncate work. */
> > +	ASSERT_EQ(-1, open(file2_s1d1, O_RDWR | O_CLOEXEC));
> > +	ASSERT_EQ(EACCES, errno);
> > +
> > +	EXPECT_EQ(-1, truncate(file2_s1d1, 10));
> > +	EXPECT_EQ(EACCES, errno);
> > +	EXPECT_EQ(-1, truncate64(file2_s1d1, 20));
> > +	EXPECT_EQ(EACCES, errno);
>
> These tests are good!

Thanks :)

>
> > +}
> > +
> >   static void
> >   reparent_exdev_layers_enforce1(struct __test_metadata *const _metadata)
> >   {

--
Mickaël Salaün July 29, 2022, 11:30 a.m. UTC | #3
On 11/07/2022 18:27, Günther Noack wrote:
> On Fri, Jul 08, 2022 at 01:17:46PM +0200, Mickaël Salaün wrote:

[...]

>>
>>> +		{
>>> +			.path = file1_s1d1,
>>> +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
>>> +				  LANDLOCK_ACCESS_FS_WRITE_FILE |
>>> +				  LANDLOCK_ACCESS_FS_TRUNCATE,
>>> +		},
>>> +		{
>>> +			.path = file2_s1d2,
>>> +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
>>> +				  LANDLOCK_ACCESS_FS_WRITE_FILE,
>>> +		},
>>> +		{
>>> +			.path = file1_s1d2,
>>> +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
>>> +		},
>>
>> Move this entry before file2_s1d2 to keep the paths sorted and make this
>> easier to read. You can change the access rights per path to also keep their
>> ordering though.
> 
> I've admittedly found it difficult to remember which of these files
> and subdirectories exist and how they are named and mixed up the names
> at least twice when developing these tests. To make it easier, I've now
> renamed these by including this at the top of the test:
> 
> char *file_rwt = file1_s1d1;
> char *file_rw = file2_s1s1;
> // etc
> 
> With the test using names like file_rwt, I find that easier to work
> with and found myself jumping less between the "rules" on top and the
> place where the assertions are written out.
> 
> This is admittedly a bit out of line with the other tests, but maybe
> it's worth doing? Let me know what you think.

It indeed makes things clearer.


> 
>>
>>
>>> +		{
>>> +			.path = dir_s2d3,
>>> +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
>>> +		},
>>> +		// Implicitly: No access rights for file2_s1d1.
>>
>> Comment to move after the use of file1_s1d1.
> 
> I'm understanding this as "keep the files in order according to the
> layout". I've reshuffled things a bit by renaming them, but this is
> also in the right order now.

Right.

[...]

>>> +	reg_fd = open(file1_s1d1, O_RDWR | O_CLOEXEC);
>>> +	ASSERT_LE(0, reg_fd);
>>> +	EXPECT_EQ(0, ftruncate(reg_fd, 10));
>>
>> You should not use EXPECT but ASSERT here. I use EXPECT when an error could
>> block a test or when it could stop a cleanup (i.e. teardown).
> 
> ASSERT is the variant that stops the test immediately, whereas EXPECT
> notes down the test failure and continues execution.
> 
> So in that spirit, I tried to use:
> 
>   * ASSERT for successful open() calls where the FD is still needed later
>   * ASSERT for close() (for symmetry with open())
>   * EXPECT for expected-failing open() calls where the FD is not used later
>   * EXPECT for everything else

I understand your logic, but this gymnastic adds complexity to writing 
tests (which might be difficult to explain) for not much gain. Indeed, 
all these tests should pass, except if we add a SKIP (cf. 
https://lore.kernel.org/all/20220628222941.2642917-1-jeffxu@google.com/).

In the case of an open FD, it will not be an issue to not close it if a 
test failed, which is not the same with FIXTURE_TEARDOWN where we want 
the workspace to be clean after tests, whether they succeeded or failed.


> 
> I had another pass over the tests and have started to use EXPECT for a
> few expected-failing open() calls.
> 
> The selftest framework seems inspired by the Googletest framework
> (https://google.github.io/googletest/primer.html#assertions) where
> this is described as: "Usually EXPECT_* are preferred, as they allow
> more than one failure to be reported in a test. However, you should
> use ASSERT_* if it doesn’t make sense to continue when the assertion
> in question fails."

I think this is good in theory, but in practice, at least for the 
Landlock selftests, everything should pass. Any test failure is a 
blocker because it breaks the contract with users.

I find it very difficult to write tests that would check as much as 
possible, even if some of these tests failed, without unexpected 
behaviors (e.g. blocking the whole tests, writing to unexpected 
locations…) because it changes the previous state from a known state to 
a set of potential states (e.g. when creating or removing files). Doing 
it generically increases complexity for tests which may already be 
difficult to understand. When investigating a test failure, we can still 
replace some ASSERT with EXPECT though.


> 
> I imagined that the same advice would apply to the kernel selftests?
> Please let me know if I'm overlooking subtle differences here.

I made kselftest_harness.h generally available (outside of seccomp) but 
I guess each subsystem maintainer might handle that differently.

See 
https://lore.kernel.org/all/1b043379-b6eb-d272-c9b9-25c6960e1ef1@digikod.net/ 
for similar concerns.
Günther Noack Aug. 4, 2022, 4:12 p.m. UTC | #4
On Fri, Jul 29, 2022 at 01:30:24PM +0200, Mickaël Salaün wrote:
> On 11/07/2022 18:27, Günther Noack wrote:
> > On Fri, Jul 08, 2022 at 01:17:46PM +0200, Mickaël Salaün wrote:
>
> [...]
>
> > >
> > > > +		{
> > > > +			.path = file1_s1d1,
> > > > +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> > > > +				  LANDLOCK_ACCESS_FS_WRITE_FILE |
> > > > +				  LANDLOCK_ACCESS_FS_TRUNCATE,
> > > > +		},
> > > > +		{
> > > > +			.path = file2_s1d2,
> > > > +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> > > > +				  LANDLOCK_ACCESS_FS_WRITE_FILE,
> > > > +		},
> > > > +		{
> > > > +			.path = file1_s1d2,
> > > > +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
> > > > +		},
> > >
> > > Move this entry before file2_s1d2 to keep the paths sorted and make this
> > > easier to read. You can change the access rights per path to also keep their
> > > ordering though.
> >
> > I've admittedly found it difficult to remember which of these files
> > and subdirectories exist and how they are named and mixed up the names
> > at least twice when developing these tests. To make it easier, I've now
> > renamed these by including this at the top of the test:
> >
> > char *file_rwt = file1_s1d1;
> > char *file_rw = file2_s1s1;
> > // etc
> >
> > With the test using names like file_rwt, I find that easier to work
> > with and found myself jumping less between the "rules" on top and the
> > place where the assertions are written out.
> >
> > This is admittedly a bit out of line with the other tests, but maybe
> > it's worth doing? Let me know what you think.
>
> It indeed makes things clearer.
>
>
> >
> > >
> > >
> > > > +		{
> > > > +			.path = dir_s2d3,
> > > > +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
> > > > +		},
> > > > +		// Implicitly: No access rights for file2_s1d1.
> > >
> > > Comment to move after the use of file1_s1d1.
> >
> > I'm understanding this as "keep the files in order according to the
> > layout". I've reshuffled things a bit by renaming them, but this is
> > also in the right order now.
>
> Right.
>
> [...]
>
> > > > +	reg_fd = open(file1_s1d1, O_RDWR | O_CLOEXEC);
> > > > +	ASSERT_LE(0, reg_fd);
> > > > +	EXPECT_EQ(0, ftruncate(reg_fd, 10));
> > >
> > > You should not use EXPECT but ASSERT here. I use EXPECT when an error could
> > > block a test or when it could stop a cleanup (i.e. teardown).
> >
> > ASSERT is the variant that stops the test immediately, whereas EXPECT
> > notes down the test failure and continues execution.
> >
> > So in that spirit, I tried to use:
> >
> >   * ASSERT for successful open() calls where the FD is still needed later
> >   * ASSERT for close() (for symmetry with open())
> >   * EXPECT for expected-failing open() calls where the FD is not used later
> >   * EXPECT for everything else
>
> I understand your logic, but this gymnastic adds complexity to writing tests
> (which might be difficult to explain) for not much gain. Indeed, all these
> tests should pass, except if we add a SKIP (cf.
> https://lore.kernel.org/all/20220628222941.2642917-1-jeffxu@google.com/).
>
> In the case of an open FD, it will not be an issue to not close it if a test
> failed, which is not the same with FIXTURE_TEARDOWN where we want the
> workspace to be clean after tests, whether they succeeded or failed.
>
>
> >
> > I had another pass over the tests and have started to use EXPECT for a
> > few expected-failing open() calls.
> >
> > The selftest framework seems inspired by the Googletest framework
> > (https://google.github.io/googletest/primer.html#assertions) where
> > this is described as: "Usually EXPECT_* are preferred, as they allow
> > more than one failure to be reported in a test. However, you should
> > use ASSERT_* if it doesn’t make sense to continue when the assertion
> > in question fails."
>
> I think this is good in theory, but in practice, at least for the Landlock
> selftests, everything should pass. Any test failure is a blocker because it
> breaks the contract with users.
>
> I find it very difficult to write tests that would check as much as
> possible, even if some of these tests failed, without unexpected behaviors
> (e.g. blocking the whole tests, writing to unexpected locations…) because it
> changes the previous state from a known state to a set of potential states
> (e.g. when creating or removing files). Doing it generically increases
> complexity for tests which may already be difficult to understand. When
> investigating a test failure, we can still replace some ASSERT with EXPECT
> though.

After some other refactorings you suggested (which I'll post soon),
the bulk of the test code now just consists of long stretches of

  EXPECT_EQ(0, test_open(...));
  EXPECT_EQ(0, test_truncate(...));
  EXPECT_EQ(EACCES, test_ftruncate(...));

So these are actually reasonably independent and don't interact much,
which means that printing multiple independent failures is not too
hard. There are a bunch of ASSERT usages left, but they are hidden in
the test_foo() helpers.

I suspect you would be ok with it now, and I'll try to send the next
patch version still with EXPECT. If you still feel strongly about it,
please let me know.

(The classic JUnit/XUnit test frameworks only have an equivalent to
ASSERT and crash early during tests. On the other hand, in these
frameworks, there is also more emphasis on writing a larger number of
narrower test cases instead of the long chains of assertions and
expectations that we use here.)

>
>
> >
> > I imagined that the same advice would apply to the kernel selftests?
> > Please let me know if I'm overlooking subtle differences here.
>
> I made kselftest_harness.h generally available (outside of seccomp) but I
> guess each subsystem maintainer might handle that differently.
>
> See
> https://lore.kernel.org/all/1b043379-b6eb-d272-c9b9-25c6960e1ef1@digikod.net/
> for similar concerns.

--
diff mbox series

Patch

diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index cb77eaa01c91..c3e48fd12b2b 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -2237,6 +2237,86 @@  TEST_F_FORK(layout1, reparent_rename)
 	ASSERT_EQ(EXDEV, errno);
 }
 
+TEST_F_FORK(layout1, truncate)
+{
+	const struct rule rules[] = {
+		{
+			.path = file1_s1d1,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE |
+				  LANDLOCK_ACCESS_FS_WRITE_FILE |
+				  LANDLOCK_ACCESS_FS_TRUNCATE,
+		},
+		{
+			.path = file2_s1d2,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE |
+				  LANDLOCK_ACCESS_FS_WRITE_FILE,
+		},
+		{
+			.path = file1_s1d2,
+			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
+		},
+		{
+			.path = dir_s2d3,
+			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
+		},
+		// Implicitly: No access rights for file2_s1d1.
+		{},
+	};
+	const int ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, rules);
+	int reg_fd;
+
+	ASSERT_LE(0, ruleset_fd);
+	enforce_ruleset(_metadata, ruleset_fd);
+	ASSERT_EQ(0, close(ruleset_fd));
+
+	/* Read, write and truncate permissions => truncate and ftruncate work. */
+	reg_fd = open(file1_s1d1, O_RDWR | O_CLOEXEC);
+	ASSERT_LE(0, reg_fd);
+	EXPECT_EQ(0, ftruncate(reg_fd, 10));
+	EXPECT_EQ(0, ftruncate64(reg_fd, 20));
+	ASSERT_EQ(0, close(reg_fd));
+
+	EXPECT_EQ(0, truncate(file1_s1d1, 10));
+	EXPECT_EQ(0, truncate64(file1_s1d1, 20));
+
+	/* Just read and write permissions => no truncate variant works. */
+	reg_fd = open(file2_s1d2, O_RDWR | O_CLOEXEC);
+	ASSERT_LE(0, reg_fd);
+	EXPECT_EQ(-1, ftruncate(reg_fd, 10));
+	EXPECT_EQ(EACCES, errno);
+	EXPECT_EQ(-1, ftruncate64(reg_fd, 20));
+	EXPECT_EQ(EACCES, errno);
+	ASSERT_EQ(0, close(reg_fd));
+
+	EXPECT_EQ(-1, truncate(file2_s1d2, 10));
+	EXPECT_EQ(EACCES, errno);
+	EXPECT_EQ(-1, truncate64(file2_s1d2, 20));
+	EXPECT_EQ(EACCES, errno);
+
+	/* Just truncate permissions => truncate(64) works, but can't open file. */
+	ASSERT_EQ(-1, open(file1_s1d2, O_RDWR | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+
+	EXPECT_EQ(0, truncate(file1_s1d2, 10));
+	EXPECT_EQ(0, truncate64(file1_s1d2, 20));
+
+	/* Just truncate permission on directory => truncate(64) works, but can't open file. */
+	ASSERT_EQ(-1, open(file1_s2d3, O_RDWR | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+
+	EXPECT_EQ(0, truncate(file1_s2d3, 10));
+	EXPECT_EQ(0, truncate64(file1_s2d3, 20));
+
+	/* No permissions => Neither truncate nor ftruncate work. */
+	ASSERT_EQ(-1, open(file2_s1d1, O_RDWR | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+
+	EXPECT_EQ(-1, truncate(file2_s1d1, 10));
+	EXPECT_EQ(EACCES, errno);
+	EXPECT_EQ(-1, truncate64(file2_s1d1, 20));
+	EXPECT_EQ(EACCES, errno);
+}
+
 static void
 reparent_exdev_layers_enforce1(struct __test_metadata *const _metadata)
 {