[PATCH v3 0/5] Landlock: IOCTL support
Mickaël Salaün
mic at digikod.net
Thu Oct 26 14:55:30 UTC 2023
On Thu, Oct 26, 2023 at 12:07:28AM +0200, Günther Noack wrote:
> On Fri, Oct 20, 2023 at 04:57:39PM +0200, Mickaël Salaün wrote:
> > On Fri, Oct 20, 2023 at 12:09:36AM +0200, Günther Noack wrote:
> > > * We introduce a flag in struct landlock_ruleset_attr which controls whether the
> > > graphics-related IOCTLs are controlled through the LANDLOCK_ACCESS_FS_GFX
> > > access right, rather than through LANDLOCK_ACCESS_FS_IOCTL.
> > >
> > > (This could potentially also be put in the "flags" argument to
> > > landlock_create_ruleset(), but it feels a bit more appropriate in the struct I
> > > think, as it influences the interpretation of the logic. But I'm open to
> > > suggestions.)
> > >
> >
> > What would be the difference with creating a
> > LANDLOCK_ACCESS_FS_GFX_IOCTL access right?
> >
> > The main issue with this approach is that it complexifies the usage of
> > Landlock, and users would need to tweak more knobs to configure a
> > ruleset.
> >
> > What about keeping my proposal (mainly the IOCTL handling and delegation
> > logic) for the user interface, and translate that for kernel internals
> > to your proposal? See the below example.
>
> Yes!
>
> I have pondered this for about a day now, and tried to break the example in
> various ways, but I believe you are right with this -- I think we can actually
> use the "handled" flags to control the IOCTL grouping, and then translate all of
> it quickly to synthetic access rights for the internal logic. When doing the
> translation only once during ruleset enablement time, we can keep using the
> existing logic for the synthetic rights and it'll obviously work correctly when
> layers are stacked. (I paraphrase it in more detail at the end, to make sure we
> are on the same page. -- But I think we are.)
>
>
> > > Example: Without the flag, the IOCTL groups will be:
> > >
> > > These are always permitted: FIOCLEX, FIONCLEX, FIONBIO, etc.
> > > LANDLOCK_ACCESS_FS_READ_FILE: controls FIONREAD
> > > LANDLOCK_ACCESS_FS_IOCTL: controls all other IOCTL commands
> > >
> > > but when users set the flag, the IOCTL groups will be:
> > >
> > > These are always permitted: FIOCLEX, FIONCLEX, FIONBIO, etc.
> > > LANDLOCK_ACCESS_FS_READ_FILE: controls FIONREAD
> > > LANDLOCK_ACCESS_FS_GFX: controls (list of gfx-related IOCTLs)
> > > LANDLOCK_ACCESS_FS_IOCTL: controls all other IOCTL commands
> > >
> >
> > Does this mean that handling LANDLOCK_ACCESS_FS_GFX without the flag
> > would not allow GFX-related IOCTL commands? Thit would be inconsistent
> > with the way LANDLOCK_ACCESS_FS_READ_FILE is handled.
>
> Yes, that is how I had imagined that. It's true that it's slightly inconsistent
> in usage, and you are right that it creates some new concepts in the API which
> are maybe avoidable. Let's try it the way you proposed and control it with the
> "handled" flags.
>
>
> > Would this flag works with non-GFX access rights as well? Would there be
> > potentially one new flag per new access right?
> >
> > >
> > > Implementation-wise, I think it would actually look very similar to what would
> > > be needed for your proposal of having a new special meaning for "handled". It
> > > would have the slight advantage that the new flag is actually only needed at the
> > > time when we introduce a new way of grouping the IOCTL commands, so we would
> > > only burden users with the additional complexity when it's actually required.
> >
> > Indeed, and burdening users with more flags would increase the cost of
> > (properly) using Landlock.
> >
> > I'm definitely in favor to make the Landlock interface as simple as
> > possible, taking into account the inherent compatibilty complexity, and
> > pushing most of this complexity handling to user space libraries, and if
> > it not possible, pushing the rest of the complexity into the kernel.
>
> Ack, sounds good.
>
>
> > > One implementation approach that I find reasonable to think about is to create
> > > "synthetic" access rights when rulesets are enabled. That is, we introduce
> > > LANDLOCK_ACCESS_FS_SYNTHETIC_GFX_IOCTL (name TBD), but we keep this constant
> > > private to the kernel.
> > >
> > > * *At ruleset enablement time*, we populate the bit for this access right either
> > > from the LANDLOCK_ACCESS_FS_GFX or the LANDLOCK_ACCESS_FS_IOCTL bit from the
> > > same access_mask_t, depending on the IOCTL grouping which the ruleset is
> > > configured with.
> > >
> > > * *In hook_file_open*, we then check for LANDLOCK_ACCESS_FS_SYNTHETIC_GFX_IOCTL
> > > for the GFX-related IOCTL commands.
> > >
> > > I'm in favor of using the synthetic access rights, because I find it clearer to
> > > understand that the effective access rights for a file from different layers are
> > > just combined with a bitwise AND, and will give the right results. We could
> > > probably also make these path walk helpers aware of the special cases and only
> > > have the synthetic right in layer_masks_dom, but I'd prefer not to complicate
> > > these helpers even further.
> >
> > I like this synthetic access right approach, but what worries me is that
> > it will potentially double the number of access rights. This is not an
> > issue for the handled access right (i.e. per ruleset layer), but we
> > should avoid that for allowed accesses (i.e. rules). Indeed, the
> > layer_masks[] size is proportional to the number of potential allowed
> > access rights, and increasing this array could increase the kernel stack
> > size (see is_access_to_paths_allowed). It would not be an issue for now
> > though, we have a lot of room, it is just something to keep in mind.
>
> Yes, acknowledged.
>
> FWIW, LANDLOCK_ACCESS_FS_IOCTL is already 1 << 15, so adding the synthetic
> rights will indeed make access_mask_t go up to 32 bit. (This was already done
> in the patch for the metadata access, but that one was not merged yet.) I also
> feel that the stack usage is the case where this is most likely to be an issue.
>
>
> > Because of the way we need to compare file hierarchies (cf. FS_REFER),
> > it seems to be safer to only rely on (synthetic) access rights. So I
> > think it is the right approach.
> >
> > >
> > >
> > > Sorry for the long mail, I hope that the examples clarify it a bit. :)
> > >
> > > In summary, it seems conceptually cleaner to me to control every IOCTL command
> > > with only one access right, and let users control which one that should be with
> > > a separate flag, so that "handled" keeps its original semantics. It would also
> > > have the upside that we can delay that implementation until the time where we
> > > actually introduce new IOCTL-aware access rights on top of the current patch st.
> >
> > I don't see how we'll not get an inconsistent logic: a first one with
> > old/current access rights, and another one for future access rights
> > (e.g. GFX).
> >
> > >
> > > I'd be interested to hear your thoughts on it.
> >
> > Thanks for this detailed explanation, that is useful.
> >
> > I'm in favor of the synthetic access right, but I'd like to not add
> > optional flags to the user API. What do you think about the kernel
> > doing the translation to the synthetic access rights?
> >
> > To make the reasoning easier for the kernel implementation, following
> > the synthetic access rights idea, we can create these groups:
> >
> > * IOCTL_CMD_G1: FIOQSIZE
> > * IOCTL_CMD_G2: FS_IOCT_FIEMAP | FIBMAP | FIGETBSZ
> > * IOCTL_CMD_G3: FIONREAD | FIDEDUPRANGE
> > * IOCTL_CMD_G4: FICLONE | FICLONERANGE | FS_IOC_RESVSP | FS_IOC_RESVSP64
> > | FS_IOC_UNRESVSP | FS_IOC_UNRESVSP64 | FS_IOC_ZERO_RANGE
> >
> > Existing (and future) access rights would automatically get the related
> > IOCTL fine-grained rights *if* LANDLOCK_ACCESS_FS_IOCTL is handled:
> > * LANDLOCK_ACCESS_FS_WRITE_FILE: IOCTL_CMD_G1 | IOCTL_CMD_G2 | IOCTL_CMD_G4
> > * LANDLOCK_ACCESS_FS_READ_FILE: IOCTL_CMD_G1 | IOCTL_CMD_G2 | IOCTL_CMD_G3
> > * LANDLOCK_ACCESS_FS_READ_DIR: IOCTL_CMD_G1
> >
> > This works with the ruleset handled access rights and the related rules
> > allowed accesses by simply ORing the access rights.
> >
> > We should also keep in mind that some IOCTL commands may only be related
> > to some specific file types or filesystems, either now or in the future
> > (see the GFX example).
>
> I am coming around to your approach with using "handled" bits to determine the
> grouping. Let me paraphrase some key concepts to make sure we are on the same
> page:
>
> * The IOCTL groups are modeled as synthetic access rights, IOCTL_CMD_G1...G4 in
> your example. Each IOCTL command maps to exactly one of these groups.
>
> Because the presence of these groups is an implementation detail in the
> kernel, we can adapt it later and make it more fine-grained if needed.
>
> * We use "handled" bits like LANDLOCK_ACCESS_FS_WRITE_FILE to determine the
> synthetic access rights.
>
> We can populate the synthetic IOCTL_CMD_G1...G4 groups depending on how the
> "handled" bits are populated.
>
> In my understanding, the logic could roughly be this:
>
> static access_mask_t expand_ioctl(access_mask_t handled, access_mask_t am,
> access_mask_t src, access_mask_t dst)
> {
The third column "IOCTL unhandled" is not reflected here. What about
this patch?
if (!(handled & LANDLOCK_ACCESS_FS_IOCTL)) {
return am | dst;
}
> if (handled & src) {
> /* If "src" access right is handled, populate "dst" from "src". */
> return am | ((am & src) ? dst : 0);
> } else {
> /* Otherwise, populate "dst" flag from "ioctl" flag. */
> return am | ((am & LANDLOCK_ACCESS_FS_IOCTL) ? dst : 0);
> }
> }
>
> static access_mask_t expand_all_ioctl(access_mask_t handled, access_mask_t am)
> {
Instead of reapeating "am | " in expand_ioctl() and assigning am several
times in expand_all_ioctl(), you could simply do something like that:
return am |
expand_ioctl(handled, am, ...) |
expand_ioctl(handled, am, ...) |
expand_ioctl(handled, am, ...);
> am = expand_ioctl(handled, am,
> LANDLOCK_ACCESS_FS_WRITE_FILE,
> IOCTL_CMD_G1 | IOCTL_CMD_G2 | IOCTL_CMD_G4);
> am = expand_ioctl(handled, am,
> LANDLOCK_ACCESS_FS_READ_FILE,
> IOCTL_CMD_G1 | IOCTL_CMD_G2 | IOCTL_CMD_G3);
> am = expand_ioctl(handled, am,
> LANDLOCK_ACCESS_FS_READ_DIR,
> IOCTL_CMD_G1);
> return am;
> }
>
> and then during the installing of a ruleset, we'd call
> expand_all_ioctl(handled, access) for each specified file access, and
> expand_all_ioctl(handled, handled) for the handled access rights,
> to populate the synthetic IOCTL_CMD_G* access rights.
We can do these transformations directly in the new
landlock_add_fs_access_mask() and landlock_append_fs_rule().
Please base the next series on
https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git/log/?h=next
This branch might be rebased from time to time, but only minor changes
will get there.
>
> In expand_ioctl() above, if "src" is *not* handled, we populate the associated
> synthetic access rights "dst" from the value in LANDLOCK_ACCESS_FS_IOCTL.
> With that, when enabling a ruleset, we map everything to the most specific
> grouping which is available, and later on, the LSM hook can just ignore that
> different grouping configurations are possible.
>
> * In the ioctl LSM hook, each possible cmd is controlled by exactly one access
> right. The ones that you have listed are all controlled by one of the
> IOCTL_CMD_G1...G4 access rights, and all others by LANDLOCK_ACCESS_FS_IOCTL.
>
> I was previously concerned that the usage of "handled" to control the grouping
> would be at odds with the layer composition logic, but with this logic, we are
> now mapping these to the synthetic access rights at enablement time, and all the
> ruleset composition logic can stay working as it is (at least until we run out
> of bits in access_mask_t).
>
> I've also been concerned before that we would break compatibility across
> versions, but this also seems less likely now that we've discussed this in all
> this detail %-)
>
> I suspect that the normal upgrade path from one Landlock version to the next
> will be for most users to always use the full set of "handled" flags that their
> library knows about. When we add the hypothetical "GFX" flag to that set, this
> will change the IOCTL grouping a bit, so that files which were previously listed
> as having the LANDLOCK_ACCESS_FS_IOCTL right, might now not be enabled for GFX
> ioctls. But that is (A) probably correct anyway in most cases, and (B) users
> upgrading from one Landlock ABI version to the next have a chance to read their
> library changelog as part of that upgrade.
Yes, explicitly adding a new flag to a function argument should indeed
lead to read the related documentation, and hopefully test the code in
an up-to-date sandbox environment!
This strategy should help avoid long-term use of the generic
LANDLOCK_ACCESS_FS_IOCTL but converge to new dedicated access rights
instead.
>
> I think this is a reasonable approach. If you agree, I'm willing to give it a
> shot and adapt the patch set to implement that.
This looks great!
>
> —Günther
More information about the Linux-security-module-archive
mailing list