diff mbox series

[RFC,v2,2/9] libselinux/utils: introduce selabel_compare

Message ID 20240131130840.48155-3-cgzones@googlemail.com (mailing list archive)
State New
Delegated to: Petr Lautrbach
Headers show
Series libselinux: rework selabel_file(5) database | expand

Commit Message

Christian Göttsche Jan. 31, 2024, 1:08 p.m. UTC
Add a utility around selabel_cmp(3).

Can be used by users to compare a pre-compiled fcontext file to an
original text-based file context definition file.

Can be used for development to verify compilation and parsing of the
pre-compiled fcontext format works correctly.

Signed-off-by: Christian Göttsche <cgzones@googlemail.com>
---
v2:
   split nested block into own function
---
 libselinux/utils/.gitignore        |   1 +
 libselinux/utils/selabel_compare.c | 122 +++++++++++++++++++++++++++++
 2 files changed, 123 insertions(+)
 create mode 100644 libselinux/utils/selabel_compare.c

Comments

James Carter March 7, 2024, 7:50 p.m. UTC | #1
On Wed, Jan 31, 2024 at 8:42 AM Christian Göttsche
<cgzones@googlemail.com> wrote:
>
> Add a utility around selabel_cmp(3).
>
> Can be used by users to compare a pre-compiled fcontext file to an
> original text-based file context definition file.
>
> Can be used for development to verify compilation and parsing of the
> pre-compiled fcontext format works correctly.
>
> Signed-off-by: Christian Göttsche <cgzones@googlemail.com>

I don't have any comments on the code.

It took me a while to get this to work. It was very confusing. Not
because of your code, but because of the behavior of selabel_cmp(). It
is not intuitive and I think that it needs to be better documented. It
will absolutely not work as expected on a copy of the contexts
directory.


With all that, not everything worked as I thought it would.

current = installed contexts directory
the other file is by itself (and not in a copy of the contexts directory)

1) current file_contexts.bin vs file_contexts [Expected]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
file_contexts
spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
to spec file_contexts

2) current file_contexts vs file_contexts [Not expected, diff says
they are equal]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts file_contexts
selabel_cmp: mismatch regex_str left remnant in stem tmp
selabel_cmp: mismatch child node tmp stem (root)
spec /etc/selinux/targeted/contexts/files/file_contexts is
uncomparable to spec file_contexts

3) current file_contexts.bin vs file_contexts_1 [Expected]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
file_contexts_1
spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
to spec file_contexts_1

4) file_contexts vs file_contexts_1 [Expected]
selabel_compare file_contexts file_contexts_1
spec file_contexts is equal to spec file_contexts_1


I realize now that selabel_cmp() can only find a superset or a subset
if the only difference occurs at the very end. The longer file must be
exactly like the shorter file except for the additional lines at the
end.

I tried testing with the zebra module disabled, so it should have been
a subset of the current policy, but, of course, selabel_cmp() was not
sophisticated enough to say that. It just says that everything is
uncomparable. It would be nice if something said that.


The following worked only in the most trivial case where I could just
use diff instead.
contexts1 = copy of original contexts directory
contexts2 = copy of new contexts directory with new selabel db

1) current file_contexts vs contexts1 file_contexts [Expected, but Confusing]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts
contexts1/files/file_contexts
contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
format, skipping
spec /etc/selinux/targeted/contexts/files/file_contexts is equal to
spec contexts1/files/file_contexts

2) current file_contexts.bin vs contexts1 file_contexts [Not expected.
Expected to be able to compare the current file_contexts.bin with old
text file_contexts]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
contexts1/files/file_contexts
contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
format, skipping
selabel_cmp: mismatch regex_str right remnant in stem tmp
selabel_cmp: mismatch child node tmp stem (root)
spec /etc/selinux/targeted/contexts/files/file_contexts.bin is
uncomparable to spec contexts1/files/file_contexts

3) current file_contexts.bin vs contexts1 file_contexts.bin [Expected,
but confusing]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
contexts1/files/file_contexts.bin
contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
ERROR: selabel_open - Could not obtain handle for
contexts1/files/file_contexts.bin:  No such file or directory

4) current file_contexts vs contexts2 file_contexts [Expected]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts
contexts2/files/file_contexts
spec /etc/selinux/targeted/contexts/files/file_contexts is equal to
spec contexts2/files/file_contexts

5) current file_contexts.bin vs contexts2 file_contexts [Not expected.
Expected to be able to compare the current file_contexts.bin with
contexts2 text file_contexts]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
contexts2/files/file_contexts
selabel_cmp: mismatch regex_str right remnant in stem tmp
selabel_cmp: mismatch child node tmp stem (root)
spec /etc/selinux/targeted/contexts/files/file_contexts.bin is
uncomparable to spec contexts2/files/file_contexts

6) current file_contexts.bin vs contexts2 file_contexts.bin [Expected]
selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
contexts2/files/file_contexts.bin
spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
to spec contexts2/files/file_contexts.bin

7) contexts1 file_contexts vs contexts2 file_contexts [Expected, but confusing]
selabel_compare contexts1/files/file_contexts  contexts2/files/file_contexts
contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
format, skipping
spec contexts1/files/file_contexts is equal to spec
contexts2/files/file_contexts

8) contexts1 file_contexts vs contexts2 file_contexts.cpy (which is a
copy of file_contexts) [Not expected. Expected them to be equal]
selabel_compare contexts1/files/file_contexts contexts2/files/file_contexts.cpy
contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
format, skipping
selabel_cmp: mismatch regex_str left remnant in stem tmp
selabel_cmp: mismatch child node tmp stem (root)
spec contexts1/files/file_contexts is uncomparable to spec
contexts2/files/file_contexts.cpy

With all of the issues, I wonder how useful this actually would be. It
might cause more confusion then anything.

Thanks,
Jim

> ---
> v2:
>    split nested block into own function
> ---
>  libselinux/utils/.gitignore        |   1 +
>  libselinux/utils/selabel_compare.c | 122 +++++++++++++++++++++++++++++
>  2 files changed, 123 insertions(+)
>  create mode 100644 libselinux/utils/selabel_compare.c
>
> diff --git a/libselinux/utils/.gitignore b/libselinux/utils/.gitignore
> index b3311360..2e10b14f 100644
> --- a/libselinux/utils/.gitignore
> +++ b/libselinux/utils/.gitignore
> @@ -16,6 +16,7 @@ getseuser
>  matchpathcon
>  policyvers
>  sefcontext_compile
> +selabel_compare
>  selabel_digest
>  selabel_get_digests_all_partial_matches
>  selabel_lookup
> diff --git a/libselinux/utils/selabel_compare.c b/libselinux/utils/selabel_compare.c
> new file mode 100644
> index 00000000..9ca6eff1
> --- /dev/null
> +++ b/libselinux/utils/selabel_compare.c
> @@ -0,0 +1,122 @@
> +#include <getopt.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <selinux/label.h>
> +
> +
> +static void usage(const char *progname)
> +{
> +       fprintf(stderr,
> +               "usage: %s [-b backend] [-v] file1 file2\n\n"
> +               "Where:\n\t"
> +               "-b           The backend - \"file\", \"media\", \"x\", \"db\" or \"prop\" (defaults to \"file\")\n\t"
> +               "-v           Validate entries against loaded policy.\n\t"
> +               "file1/file2  Files containing the specs.\n",
> +               progname);
> +}
> +
> +static int compare(const char *file1, const char *file2, const char *validate, unsigned int backend)
> +{
> +       struct selabel_handle *hnd1, *hnd2;
> +       const struct selinux_opt selabel_option1[] = {
> +               { SELABEL_OPT_PATH, file1 },
> +               { SELABEL_OPT_VALIDATE, validate }
> +       };
> +       const struct selinux_opt selabel_option2[] = {
> +               { SELABEL_OPT_PATH, file2 },
> +               { SELABEL_OPT_VALIDATE, validate }
> +       };
> +       enum selabel_cmp_result result;
> +
> +       hnd1 = selabel_open(backend, selabel_option1, 2);
> +       if (!hnd1) {
> +               fprintf(stderr, "ERROR: selabel_open - Could not obtain handle for %s:  %m\n", file1);
> +               return EXIT_FAILURE;
> +       }
> +
> +       hnd2 = selabel_open(backend, selabel_option2, 2);
> +       if (!hnd2) {
> +               fprintf(stderr, "ERROR: selabel_open - Could not obtain handle for %s:  %m\n", file2);
> +               selabel_close(hnd1);
> +               return EXIT_FAILURE;
> +       }
> +
> +       result = selabel_cmp(hnd1, hnd2);
> +
> +       selabel_close(hnd2);
> +       selabel_close(hnd1);
> +
> +       switch (result) {
> +       case SELABEL_SUBSET:
> +               printf("spec %s is a subset of spec %s\n", file1, file2);
> +               break;
> +       case SELABEL_EQUAL:
> +               printf("spec %s is equal to spec %s\n", file1, file2);
> +               break;
> +       case SELABEL_SUPERSET:
> +               printf("spec %s is a superset of spec %s\n", file1, file2);
> +               break;
> +       case SELABEL_INCOMPARABLE:
> +               printf("spec %s is uncomparable to spec %s\n", file1, file2);
> +               break;
> +       default:
> +               fprintf(stderr, "ERROR: selabel_cmp - Unexpected result %d\n", result);
> +               return EXIT_FAILURE;
> +       }
> +
> +       return EXIT_SUCCESS;
> +}
> +
> +int main(int argc, char *argv[])
> +{
> +       unsigned int backend = SELABEL_CTX_FILE;
> +       int opt;
> +       const char *validate = NULL, *file1 = NULL, *file2 = NULL;
> +
> +       if (argc < 3) {
> +               usage(argv[0]);
> +               return EXIT_FAILURE;
> +       }
> +
> +       while ((opt = getopt(argc, argv, "b:v")) > 0) {
> +               switch (opt) {
> +               case 'b':
> +                       if (!strcasecmp(optarg, "file")) {
> +                               backend = SELABEL_CTX_FILE;
> +                       } else if (!strcmp(optarg, "media")) {
> +                               backend = SELABEL_CTX_MEDIA;
> +                       } else if (!strcmp(optarg, "x")) {
> +                               backend = SELABEL_CTX_X;
> +                       } else if (!strcmp(optarg, "db")) {
> +                               backend = SELABEL_CTX_DB;
> +                       } else if (!strcmp(optarg, "prop")) {
> +                               backend = SELABEL_CTX_ANDROID_PROP;
> +                       } else if (!strcmp(optarg, "service")) {
> +                               backend = SELABEL_CTX_ANDROID_SERVICE;
> +                       } else {
> +                               fprintf(stderr, "Unknown backend: %s\n", optarg);
> +                               usage(argv[0]);
> +                               return EXIT_FAILURE;
> +                       }
> +                       break;
> +               case 'v':
> +                       validate = (char *)1;
> +                       break;
> +               default:
> +                       usage(argv[0]);
> +                       return EXIT_FAILURE;
> +               }
> +       }
> +
> +       if (argc != optind + 2) {
> +               usage(argv[0]);
> +               return EXIT_FAILURE;
> +       }
> +
> +       file1 = argv[optind++];
> +       file2 = argv[optind];
> +
> +       return compare(file1, file2, validate, backend);
> +}
> --
> 2.43.0
>
>
Christian Göttsche March 11, 2024, 5:20 p.m. UTC | #2
On Thu, 7 Mar 2024 at 20:50, James Carter <jwcart2@gmail.com> wrote:
>
> On Wed, Jan 31, 2024 at 8:42 AM Christian Göttsche
> <cgzones@googlemail.com> wrote:
> >
> > Add a utility around selabel_cmp(3).
> >
> > Can be used by users to compare a pre-compiled fcontext file to an
> > original text-based file context definition file.
> >
> > Can be used for development to verify compilation and parsing of the
> > pre-compiled fcontext format works correctly.
> >
> > Signed-off-by: Christian Göttsche <cgzones@googlemail.com>
>

First of all many thanks for testing!

> I don't have any comments on the code.
>
> It took me a while to get this to work. It was very confusing. Not
> because of your code, but because of the behavior of selabel_cmp(). It
> is not intuitive and I think that it needs to be better documented. It
> will absolutely not work as expected on a copy of the contexts
> directory.
>
>
> With all that, not everything worked as I thought it would.
>
> current = installed contexts directory
> the other file is by itself (and not in a copy of the contexts directory)
>
> 1) current file_contexts.bin vs file_contexts [Expected]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> file_contexts
> spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
> to spec file_contexts
>
> 2) current file_contexts vs file_contexts [Not expected, diff says
> they are equal]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts file_contexts
> selabel_cmp: mismatch regex_str left remnant in stem tmp
> selabel_cmp: mismatch child node tmp stem (root)
> spec /etc/selinux/targeted/contexts/files/file_contexts is
> uncomparable to spec file_contexts

Since the database is opened via selabel_open(3)
filecontexts.(homedirs|local) are implicitly loaded too.
It would be confusing if the selabel_compare helper would deviate from
that default behavior.
Also currently there is no information in the in-memory database from
which/how-many files the database was loaded.
Maybe that should be added so the selabel_compare helper could give a hint?

> 3) current file_contexts.bin vs file_contexts_1 [Expected]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> file_contexts_1
> spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
> to spec file_contexts_1
>
> 4) file_contexts vs file_contexts_1 [Expected]
> selabel_compare file_contexts file_contexts_1
> spec file_contexts is equal to spec file_contexts_1
>
>
> I realize now that selabel_cmp() can only find a superset or a subset
> if the only difference occurs at the very end. The longer file must be
> exactly like the shorter file except for the additional lines at the
> end.

Thanks, this is indeed a shortcoming in v2 (regex prefix length were
not considered). Fixed in wip-v3:
https://github.com/SELinuxProject/selinux/compare/f07e4c3b66416f6337d6a2ea1e0d69555f8704ad..090af7e6510edb90f4e7df55346e5023ac190b66

> I tried testing with the zebra module disabled, so it should have been
> a subset of the current policy, but, of course, selabel_cmp() was not
> sophisticated enough to say that. It just says that everything is
> uncomparable. It would be nice if something said that.
>
>
> The following worked only in the most trivial case where I could just
> use diff instead.
> contexts1 = copy of original contexts directory
> contexts2 = copy of new contexts directory with new selabel db
>
> 1) current file_contexts vs contexts1 file_contexts [Expected, but Confusing]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts
> contexts1/files/file_contexts
> contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
> format, skipping
> spec /etc/selinux/targeted/contexts/files/file_contexts is equal to
> spec contexts1/files/file_contexts

What is confusing here?

> 2) current file_contexts.bin vs contexts1 file_contexts [Not expected.
> Expected to be able to compare the current file_contexts.bin with old
> text file_contexts]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> contexts1/files/file_contexts
> contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
> format, skipping
> selabel_cmp: mismatch regex_str right remnant in stem tmp
> selabel_cmp: mismatch child node tmp stem (root)
> spec /etc/selinux/targeted/contexts/files/file_contexts.bin is
> uncomparable to spec contexts1/files/file_contexts

Gives subset with v3 (since when using a .bin extension .homedirs and
.local are not loaded).

> 3) current file_contexts.bin vs contexts1 file_contexts.bin [Expected,
> but confusing]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> contexts1/files/file_contexts.bin
> contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> ERROR: selabel_open - Could not obtain handle for
> contexts1/files/file_contexts.bin:  No such file or directory

Updated errno to EINVAL for this case.

> 4) current file_contexts vs contexts2 file_contexts [Expected]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts
> contexts2/files/file_contexts
> spec /etc/selinux/targeted/contexts/files/file_contexts is equal to
> spec contexts2/files/file_contexts
>
> 5) current file_contexts.bin vs contexts2 file_contexts [Not expected.
> Expected to be able to compare the current file_contexts.bin with
> contexts2 text file_contexts]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> contexts2/files/file_contexts
> selabel_cmp: mismatch regex_str right remnant in stem tmp
> selabel_cmp: mismatch child node tmp stem (root)
> spec /etc/selinux/targeted/contexts/files/file_contexts.bin is
> uncomparable to spec contexts2/files/file_contexts

Should give subset (due to the missing .homedirs|.local) now.

> 6) current file_contexts.bin vs contexts2 file_contexts.bin [Expected]
> selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> contexts2/files/file_contexts.bin
> spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
> to spec contexts2/files/file_contexts.bin
>
> 7) contexts1 file_contexts vs contexts2 file_contexts [Expected, but confusing]
> selabel_compare contexts1/files/file_contexts  contexts2/files/file_contexts
> contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
> format, skipping
> spec contexts1/files/file_contexts is equal to spec
> contexts2/files/file_contexts

What is confusing here? selabel_open(3) tries to open binary fcontext
files first for performance.

> 8) contexts1 file_contexts vs contexts2 file_contexts.cpy (which is a
> copy of file_contexts) [Not expected. Expected them to be equal]
> selabel_compare contexts1/files/file_contexts contexts2/files/file_contexts.cpy
> contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
> format, skipping
> selabel_cmp: mismatch regex_str left remnant in stem tmp
> selabel_cmp: mismatch child node tmp stem (root)
> spec contexts1/files/file_contexts is uncomparable to spec
> contexts2/files/file_contexts.cpy
>
> With all of the issues, I wonder how useful this actually would be. It
> might cause more confusion then anything.
>
> Thanks,
> Jim
>
> > ---
> > v2:
> >    split nested block into own function
> > ---
> >  libselinux/utils/.gitignore        |   1 +
> >  libselinux/utils/selabel_compare.c | 122 +++++++++++++++++++++++++++++
> >  2 files changed, 123 insertions(+)
> >  create mode 100644 libselinux/utils/selabel_compare.c
> >
> > diff --git a/libselinux/utils/.gitignore b/libselinux/utils/.gitignore
> > index b3311360..2e10b14f 100644
> > --- a/libselinux/utils/.gitignore
> > +++ b/libselinux/utils/.gitignore
> > @@ -16,6 +16,7 @@ getseuser
> >  matchpathcon
> >  policyvers
> >  sefcontext_compile
> > +selabel_compare
> >  selabel_digest
> >  selabel_get_digests_all_partial_matches
> >  selabel_lookup
> > diff --git a/libselinux/utils/selabel_compare.c b/libselinux/utils/selabel_compare.c
> > new file mode 100644
> > index 00000000..9ca6eff1
> > --- /dev/null
> > +++ b/libselinux/utils/selabel_compare.c
> > @@ -0,0 +1,122 @@
> > +#include <getopt.h>
> > +#include <stdio.h>
> > +#include <stdlib.h>
> > +#include <string.h>
> > +
> > +#include <selinux/label.h>
> > +
> > +
> > +static void usage(const char *progname)
> > +{
> > +       fprintf(stderr,
> > +               "usage: %s [-b backend] [-v] file1 file2\n\n"
> > +               "Where:\n\t"
> > +               "-b           The backend - \"file\", \"media\", \"x\", \"db\" or \"prop\" (defaults to \"file\")\n\t"
> > +               "-v           Validate entries against loaded policy.\n\t"
> > +               "file1/file2  Files containing the specs.\n",
> > +               progname);
> > +}
> > +
> > +static int compare(const char *file1, const char *file2, const char *validate, unsigned int backend)
> > +{
> > +       struct selabel_handle *hnd1, *hnd2;
> > +       const struct selinux_opt selabel_option1[] = {
> > +               { SELABEL_OPT_PATH, file1 },
> > +               { SELABEL_OPT_VALIDATE, validate }
> > +       };
> > +       const struct selinux_opt selabel_option2[] = {
> > +               { SELABEL_OPT_PATH, file2 },
> > +               { SELABEL_OPT_VALIDATE, validate }
> > +       };
> > +       enum selabel_cmp_result result;
> > +
> > +       hnd1 = selabel_open(backend, selabel_option1, 2);
> > +       if (!hnd1) {
> > +               fprintf(stderr, "ERROR: selabel_open - Could not obtain handle for %s:  %m\n", file1);
> > +               return EXIT_FAILURE;
> > +       }
> > +
> > +       hnd2 = selabel_open(backend, selabel_option2, 2);
> > +       if (!hnd2) {
> > +               fprintf(stderr, "ERROR: selabel_open - Could not obtain handle for %s:  %m\n", file2);
> > +               selabel_close(hnd1);
> > +               return EXIT_FAILURE;
> > +       }
> > +
> > +       result = selabel_cmp(hnd1, hnd2);
> > +
> > +       selabel_close(hnd2);
> > +       selabel_close(hnd1);
> > +
> > +       switch (result) {
> > +       case SELABEL_SUBSET:
> > +               printf("spec %s is a subset of spec %s\n", file1, file2);
> > +               break;
> > +       case SELABEL_EQUAL:
> > +               printf("spec %s is equal to spec %s\n", file1, file2);
> > +               break;
> > +       case SELABEL_SUPERSET:
> > +               printf("spec %s is a superset of spec %s\n", file1, file2);
> > +               break;
> > +       case SELABEL_INCOMPARABLE:
> > +               printf("spec %s is uncomparable to spec %s\n", file1, file2);
> > +               break;
> > +       default:
> > +               fprintf(stderr, "ERROR: selabel_cmp - Unexpected result %d\n", result);
> > +               return EXIT_FAILURE;
> > +       }
> > +
> > +       return EXIT_SUCCESS;
> > +}
> > +
> > +int main(int argc, char *argv[])
> > +{
> > +       unsigned int backend = SELABEL_CTX_FILE;
> > +       int opt;
> > +       const char *validate = NULL, *file1 = NULL, *file2 = NULL;
> > +
> > +       if (argc < 3) {
> > +               usage(argv[0]);
> > +               return EXIT_FAILURE;
> > +       }
> > +
> > +       while ((opt = getopt(argc, argv, "b:v")) > 0) {
> > +               switch (opt) {
> > +               case 'b':
> > +                       if (!strcasecmp(optarg, "file")) {
> > +                               backend = SELABEL_CTX_FILE;
> > +                       } else if (!strcmp(optarg, "media")) {
> > +                               backend = SELABEL_CTX_MEDIA;
> > +                       } else if (!strcmp(optarg, "x")) {
> > +                               backend = SELABEL_CTX_X;
> > +                       } else if (!strcmp(optarg, "db")) {
> > +                               backend = SELABEL_CTX_DB;
> > +                       } else if (!strcmp(optarg, "prop")) {
> > +                               backend = SELABEL_CTX_ANDROID_PROP;
> > +                       } else if (!strcmp(optarg, "service")) {
> > +                               backend = SELABEL_CTX_ANDROID_SERVICE;
> > +                       } else {
> > +                               fprintf(stderr, "Unknown backend: %s\n", optarg);
> > +                               usage(argv[0]);
> > +                               return EXIT_FAILURE;
> > +                       }
> > +                       break;
> > +               case 'v':
> > +                       validate = (char *)1;
> > +                       break;
> > +               default:
> > +                       usage(argv[0]);
> > +                       return EXIT_FAILURE;
> > +               }
> > +       }
> > +
> > +       if (argc != optind + 2) {
> > +               usage(argv[0]);
> > +               return EXIT_FAILURE;
> > +       }
> > +
> > +       file1 = argv[optind++];
> > +       file2 = argv[optind];
> > +
> > +       return compare(file1, file2, validate, backend);
> > +}
> > --
> > 2.43.0
> >
> >
James Carter March 11, 2024, 8:49 p.m. UTC | #3
On Mon, Mar 11, 2024 at 1:21 PM Christian Göttsche
<cgzones@googlemail.com> wrote:
>
> On Thu, 7 Mar 2024 at 20:50, James Carter <jwcart2@gmail.com> wrote:
> >
> > On Wed, Jan 31, 2024 at 8:42 AM Christian Göttsche
> > <cgzones@googlemail.com> wrote:
> > >
> > > Add a utility around selabel_cmp(3).
> > >
> > > Can be used by users to compare a pre-compiled fcontext file to an
> > > original text-based file context definition file.
> > >
> > > Can be used for development to verify compilation and parsing of the
> > > pre-compiled fcontext format works correctly.
> > >
> > > Signed-off-by: Christian Göttsche <cgzones@googlemail.com>
> >
>
> First of all many thanks for testing!
>
> > I don't have any comments on the code.
> >
> > It took me a while to get this to work. It was very confusing. Not
> > because of your code, but because of the behavior of selabel_cmp(). It
> > is not intuitive and I think that it needs to be better documented. It
> > will absolutely not work as expected on a copy of the contexts
> > directory.
> >
> >
> > With all that, not everything worked as I thought it would.
> >
> > current = installed contexts directory
> > the other file is by itself (and not in a copy of the contexts directory)
> >
> > 1) current file_contexts.bin vs file_contexts [Expected]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> > file_contexts
> > spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
> > to spec file_contexts
> >
> > 2) current file_contexts vs file_contexts [Not expected, diff says
> > they are equal]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts file_contexts
> > selabel_cmp: mismatch regex_str left remnant in stem tmp
> > selabel_cmp: mismatch child node tmp stem (root)
> > spec /etc/selinux/targeted/contexts/files/file_contexts is
> > uncomparable to spec file_contexts
>
> Since the database is opened via selabel_open(3)
> filecontexts.(homedirs|local) are implicitly loaded too.
> It would be confusing if the selabel_compare helper would deviate from
> that default behavior.
> Also currently there is no information in the in-memory database from
> which/how-many files the database was loaded.
> Maybe that should be added so the selabel_compare helper could give a hint?
>

I am not very familiar with the selabel code, but I could see what it was doing.
It is just very confusing behavior. The behavior is different
depending on where the files being compared are located. If neither is
in something that looks like /etc/selinux/targeted/contexts/files/,
then it works as expected, otherwise it works differently. I think the
expected behavior needs to be explained better.

> > 3) current file_contexts.bin vs file_contexts_1 [Expected]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> > file_contexts_1
> > spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
> > to spec file_contexts_1
> >
> > 4) file_contexts vs file_contexts_1 [Expected]
> > selabel_compare file_contexts file_contexts_1
> > spec file_contexts is equal to spec file_contexts_1
> >
> >
> > I realize now that selabel_cmp() can only find a superset or a subset
> > if the only difference occurs at the very end. The longer file must be
> > exactly like the shorter file except for the additional lines at the
> > end.
>
> Thanks, this is indeed a shortcoming in v2 (regex prefix length were
> not considered). Fixed in wip-v3:
> https://github.com/SELinuxProject/selinux/compare/f07e4c3b66416f6337d6a2ea1e0d69555f8704ad..090af7e6510edb90f4e7df55346e5023ac190b66
>
> > I tried testing with the zebra module disabled, so it should have been
> > a subset of the current policy, but, of course, selabel_cmp() was not
> > sophisticated enough to say that. It just says that everything is
> > uncomparable. It would be nice if something said that.
> >
> >
> > The following worked only in the most trivial case where I could just
> > use diff instead.
> > contexts1 = copy of original contexts directory
> > contexts2 = copy of new contexts directory with new selabel db
> >
> > 1) current file_contexts vs contexts1 file_contexts [Expected, but Confusing]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts
> > contexts1/files/file_contexts
> > contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> > contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
> > format, skipping
> > spec /etc/selinux/targeted/contexts/files/file_contexts is equal to
> > spec contexts1/files/file_contexts
>
> What is confusing here?
>

What was confusing was the fact that it is referring to files that I
did not ask it to compare. I understand that is the behavior of the
library, but that is not what I would have expected from running the
command.

> > 2) current file_contexts.bin vs contexts1 file_contexts [Not expected.
> > Expected to be able to compare the current file_contexts.bin with old
> > text file_contexts]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> > contexts1/files/file_contexts
> > contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> > contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
> > format, skipping
> > selabel_cmp: mismatch regex_str right remnant in stem tmp
> > selabel_cmp: mismatch child node tmp stem (root)
> > spec /etc/selinux/targeted/contexts/files/file_contexts.bin is
> > uncomparable to spec contexts1/files/file_contexts
>
> Gives subset with v3 (since when using a .bin extension .homedirs and
> .local are not loaded).
>
> > 3) current file_contexts.bin vs contexts1 file_contexts.bin [Expected,
> > but confusing]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> > contexts1/files/file_contexts.bin
> > contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> > contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> > ERROR: selabel_open - Could not obtain handle for
> > contexts1/files/file_contexts.bin:  No such file or directory
>
> Updated errno to EINVAL for this case.
>
> > 4) current file_contexts vs contexts2 file_contexts [Expected]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts
> > contexts2/files/file_contexts
> > spec /etc/selinux/targeted/contexts/files/file_contexts is equal to
> > spec contexts2/files/file_contexts
> >
> > 5) current file_contexts.bin vs contexts2 file_contexts [Not expected.
> > Expected to be able to compare the current file_contexts.bin with
> > contexts2 text file_contexts]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> > contexts2/files/file_contexts
> > selabel_cmp: mismatch regex_str right remnant in stem tmp
> > selabel_cmp: mismatch child node tmp stem (root)
> > spec /etc/selinux/targeted/contexts/files/file_contexts.bin is
> > uncomparable to spec contexts2/files/file_contexts
>
> Should give subset (due to the missing .homedirs|.local) now.
>
> > 6) current file_contexts.bin vs contexts2 file_contexts.bin [Expected]
> > selabel_compare /etc/selinux/targeted/contexts/files/file_contexts.bin
> > contexts2/files/file_contexts.bin
> > spec /etc/selinux/targeted/contexts/files/file_contexts.bin is equal
> > to spec contexts2/files/file_contexts.bin
> >
> > 7) contexts1 file_contexts vs contexts2 file_contexts [Expected, but confusing]
> > selabel_compare contexts1/files/file_contexts  contexts2/files/file_contexts
> > contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> > contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
> > format, skipping
> > spec contexts1/files/file_contexts is equal to spec
> > contexts2/files/file_contexts
>
> What is confusing here? selabel_open(3) tries to open binary fcontext
> files first for performance.
>

I understand what it is doing, but I think that this would be
confusing to anyone using the tool.

Thanks,
Jim


> > 8) contexts1 file_contexts vs contexts2 file_contexts.cpy (which is a
> > copy of file_contexts) [Not expected. Expected them to be equal]
> > selabel_compare contexts1/files/file_contexts contexts2/files/file_contexts.cpy
> > contexts1/files/file_contexts.bin:  Old compiled fcontext format, skipping
> > contexts1/files/file_contexts.homedirs.bin:  Old compiled fcontext
> > format, skipping
> > selabel_cmp: mismatch regex_str left remnant in stem tmp
> > selabel_cmp: mismatch child node tmp stem (root)
> > spec contexts1/files/file_contexts is uncomparable to spec
> > contexts2/files/file_contexts.cpy
> >
> > With all of the issues, I wonder how useful this actually would be. It
> > might cause more confusion then anything.
> >
> > Thanks,
> > Jim
> >
> > > ---
> > > v2:
> > >    split nested block into own function
> > > ---
> > >  libselinux/utils/.gitignore        |   1 +
> > >  libselinux/utils/selabel_compare.c | 122 +++++++++++++++++++++++++++++
> > >  2 files changed, 123 insertions(+)
> > >  create mode 100644 libselinux/utils/selabel_compare.c
> > >
> > > diff --git a/libselinux/utils/.gitignore b/libselinux/utils/.gitignore
> > > index b3311360..2e10b14f 100644
> > > --- a/libselinux/utils/.gitignore
> > > +++ b/libselinux/utils/.gitignore
> > > @@ -16,6 +16,7 @@ getseuser
> > >  matchpathcon
> > >  policyvers
> > >  sefcontext_compile
> > > +selabel_compare
> > >  selabel_digest
> > >  selabel_get_digests_all_partial_matches
> > >  selabel_lookup
> > > diff --git a/libselinux/utils/selabel_compare.c b/libselinux/utils/selabel_compare.c
> > > new file mode 100644
> > > index 00000000..9ca6eff1
> > > --- /dev/null
> > > +++ b/libselinux/utils/selabel_compare.c
> > > @@ -0,0 +1,122 @@
> > > +#include <getopt.h>
> > > +#include <stdio.h>
> > > +#include <stdlib.h>
> > > +#include <string.h>
> > > +
> > > +#include <selinux/label.h>
> > > +
> > > +
> > > +static void usage(const char *progname)
> > > +{
> > > +       fprintf(stderr,
> > > +               "usage: %s [-b backend] [-v] file1 file2\n\n"
> > > +               "Where:\n\t"
> > > +               "-b           The backend - \"file\", \"media\", \"x\", \"db\" or \"prop\" (defaults to \"file\")\n\t"
> > > +               "-v           Validate entries against loaded policy.\n\t"
> > > +               "file1/file2  Files containing the specs.\n",
> > > +               progname);
> > > +}
> > > +
> > > +static int compare(const char *file1, const char *file2, const char *validate, unsigned int backend)
> > > +{
> > > +       struct selabel_handle *hnd1, *hnd2;
> > > +       const struct selinux_opt selabel_option1[] = {
> > > +               { SELABEL_OPT_PATH, file1 },
> > > +               { SELABEL_OPT_VALIDATE, validate }
> > > +       };
> > > +       const struct selinux_opt selabel_option2[] = {
> > > +               { SELABEL_OPT_PATH, file2 },
> > > +               { SELABEL_OPT_VALIDATE, validate }
> > > +       };
> > > +       enum selabel_cmp_result result;
> > > +
> > > +       hnd1 = selabel_open(backend, selabel_option1, 2);
> > > +       if (!hnd1) {
> > > +               fprintf(stderr, "ERROR: selabel_open - Could not obtain handle for %s:  %m\n", file1);
> > > +               return EXIT_FAILURE;
> > > +       }
> > > +
> > > +       hnd2 = selabel_open(backend, selabel_option2, 2);
> > > +       if (!hnd2) {
> > > +               fprintf(stderr, "ERROR: selabel_open - Could not obtain handle for %s:  %m\n", file2);
> > > +               selabel_close(hnd1);
> > > +               return EXIT_FAILURE;
> > > +       }
> > > +
> > > +       result = selabel_cmp(hnd1, hnd2);
> > > +
> > > +       selabel_close(hnd2);
> > > +       selabel_close(hnd1);
> > > +
> > > +       switch (result) {
> > > +       case SELABEL_SUBSET:
> > > +               printf("spec %s is a subset of spec %s\n", file1, file2);
> > > +               break;
> > > +       case SELABEL_EQUAL:
> > > +               printf("spec %s is equal to spec %s\n", file1, file2);
> > > +               break;
> > > +       case SELABEL_SUPERSET:
> > > +               printf("spec %s is a superset of spec %s\n", file1, file2);
> > > +               break;
> > > +       case SELABEL_INCOMPARABLE:
> > > +               printf("spec %s is uncomparable to spec %s\n", file1, file2);
> > > +               break;
> > > +       default:
> > > +               fprintf(stderr, "ERROR: selabel_cmp - Unexpected result %d\n", result);
> > > +               return EXIT_FAILURE;
> > > +       }
> > > +
> > > +       return EXIT_SUCCESS;
> > > +}
> > > +
> > > +int main(int argc, char *argv[])
> > > +{
> > > +       unsigned int backend = SELABEL_CTX_FILE;
> > > +       int opt;
> > > +       const char *validate = NULL, *file1 = NULL, *file2 = NULL;
> > > +
> > > +       if (argc < 3) {
> > > +               usage(argv[0]);
> > > +               return EXIT_FAILURE;
> > > +       }
> > > +
> > > +       while ((opt = getopt(argc, argv, "b:v")) > 0) {
> > > +               switch (opt) {
> > > +               case 'b':
> > > +                       if (!strcasecmp(optarg, "file")) {
> > > +                               backend = SELABEL_CTX_FILE;
> > > +                       } else if (!strcmp(optarg, "media")) {
> > > +                               backend = SELABEL_CTX_MEDIA;
> > > +                       } else if (!strcmp(optarg, "x")) {
> > > +                               backend = SELABEL_CTX_X;
> > > +                       } else if (!strcmp(optarg, "db")) {
> > > +                               backend = SELABEL_CTX_DB;
> > > +                       } else if (!strcmp(optarg, "prop")) {
> > > +                               backend = SELABEL_CTX_ANDROID_PROP;
> > > +                       } else if (!strcmp(optarg, "service")) {
> > > +                               backend = SELABEL_CTX_ANDROID_SERVICE;
> > > +                       } else {
> > > +                               fprintf(stderr, "Unknown backend: %s\n", optarg);
> > > +                               usage(argv[0]);
> > > +                               return EXIT_FAILURE;
> > > +                       }
> > > +                       break;
> > > +               case 'v':
> > > +                       validate = (char *)1;
> > > +                       break;
> > > +               default:
> > > +                       usage(argv[0]);
> > > +                       return EXIT_FAILURE;
> > > +               }
> > > +       }
> > > +
> > > +       if (argc != optind + 2) {
> > > +               usage(argv[0]);
> > > +               return EXIT_FAILURE;
> > > +       }
> > > +
> > > +       file1 = argv[optind++];
> > > +       file2 = argv[optind];
> > > +
> > > +       return compare(file1, file2, validate, backend);
> > > +}
> > > --
> > > 2.43.0
> > >
> > >
diff mbox series

Patch

diff --git a/libselinux/utils/.gitignore b/libselinux/utils/.gitignore
index b3311360..2e10b14f 100644
--- a/libselinux/utils/.gitignore
+++ b/libselinux/utils/.gitignore
@@ -16,6 +16,7 @@  getseuser
 matchpathcon
 policyvers
 sefcontext_compile
+selabel_compare
 selabel_digest
 selabel_get_digests_all_partial_matches
 selabel_lookup
diff --git a/libselinux/utils/selabel_compare.c b/libselinux/utils/selabel_compare.c
new file mode 100644
index 00000000..9ca6eff1
--- /dev/null
+++ b/libselinux/utils/selabel_compare.c
@@ -0,0 +1,122 @@ 
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <selinux/label.h>
+
+
+static void usage(const char *progname)
+{
+	fprintf(stderr,
+		"usage: %s [-b backend] [-v] file1 file2\n\n"
+		"Where:\n\t"
+		"-b           The backend - \"file\", \"media\", \"x\", \"db\" or \"prop\" (defaults to \"file\")\n\t"
+		"-v           Validate entries against loaded policy.\n\t"
+		"file1/file2  Files containing the specs.\n",
+		progname);
+}
+
+static int compare(const char *file1, const char *file2, const char *validate, unsigned int backend)
+{
+	struct selabel_handle *hnd1, *hnd2;
+	const struct selinux_opt selabel_option1[] = {
+		{ SELABEL_OPT_PATH, file1 },
+		{ SELABEL_OPT_VALIDATE, validate }
+	};
+	const struct selinux_opt selabel_option2[] = {
+		{ SELABEL_OPT_PATH, file2 },
+		{ SELABEL_OPT_VALIDATE, validate }
+	};
+	enum selabel_cmp_result result;
+
+	hnd1 = selabel_open(backend, selabel_option1, 2);
+	if (!hnd1) {
+		fprintf(stderr, "ERROR: selabel_open - Could not obtain handle for %s:  %m\n", file1);
+		return EXIT_FAILURE;
+	}
+
+	hnd2 = selabel_open(backend, selabel_option2, 2);
+	if (!hnd2) {
+		fprintf(stderr, "ERROR: selabel_open - Could not obtain handle for %s:  %m\n", file2);
+		selabel_close(hnd1);
+		return EXIT_FAILURE;
+	}
+
+	result = selabel_cmp(hnd1, hnd2);
+
+	selabel_close(hnd2);
+	selabel_close(hnd1);
+
+	switch (result) {
+	case SELABEL_SUBSET:
+		printf("spec %s is a subset of spec %s\n", file1, file2);
+		break;
+	case SELABEL_EQUAL:
+		printf("spec %s is equal to spec %s\n", file1, file2);
+		break;
+	case SELABEL_SUPERSET:
+		printf("spec %s is a superset of spec %s\n", file1, file2);
+		break;
+	case SELABEL_INCOMPARABLE:
+		printf("spec %s is uncomparable to spec %s\n", file1, file2);
+		break;
+	default:
+		fprintf(stderr, "ERROR: selabel_cmp - Unexpected result %d\n", result);
+		return EXIT_FAILURE;
+	}
+
+	return EXIT_SUCCESS;
+}
+
+int main(int argc, char *argv[])
+{
+	unsigned int backend = SELABEL_CTX_FILE;
+	int opt;
+	const char *validate = NULL, *file1 = NULL, *file2 = NULL;
+
+	if (argc < 3) {
+		usage(argv[0]);
+		return EXIT_FAILURE;
+	}
+
+	while ((opt = getopt(argc, argv, "b:v")) > 0) {
+		switch (opt) {
+		case 'b':
+			if (!strcasecmp(optarg, "file")) {
+				backend = SELABEL_CTX_FILE;
+			} else if (!strcmp(optarg, "media")) {
+				backend = SELABEL_CTX_MEDIA;
+			} else if (!strcmp(optarg, "x")) {
+				backend = SELABEL_CTX_X;
+			} else if (!strcmp(optarg, "db")) {
+				backend = SELABEL_CTX_DB;
+			} else if (!strcmp(optarg, "prop")) {
+				backend = SELABEL_CTX_ANDROID_PROP;
+			} else if (!strcmp(optarg, "service")) {
+				backend = SELABEL_CTX_ANDROID_SERVICE;
+			} else {
+				fprintf(stderr, "Unknown backend: %s\n", optarg);
+				usage(argv[0]);
+				return EXIT_FAILURE;
+			}
+			break;
+		case 'v':
+			validate = (char *)1;
+			break;
+		default:
+			usage(argv[0]);
+			return EXIT_FAILURE;
+		}
+	}
+
+	if (argc != optind + 2) {
+		usage(argv[0]);
+		return EXIT_FAILURE;
+	}
+
+	file1 = argv[optind++];
+	file2 = argv[optind];
+
+	return compare(file1, file2, validate, backend);
+}