Page MenuHomeFreeBSD

Add the ability have executable jail.conf
Needs ReviewPublic

Authored by crest_freebsd_rlwinm.de on Aug 13 2024, 7:34 PM.
Tags
Referenced Files
Unknown Object (File)
Thu, May 14, 6:42 PM
Unknown Object (File)
Thu, May 14, 4:13 PM
Unknown Object (File)
Thu, May 14, 3:36 PM
Unknown Object (File)
Thu, May 14, 3:36 PM
Unknown Object (File)
Thu, May 14, 3:19 PM
Unknown Object (File)
Thu, May 14, 1:42 PM
Unknown Object (File)
Thu, May 14, 12:48 PM
Unknown Object (File)
Thu, May 14, 12:43 PM

Details

Reviewers
jamie
kevans
Summary

Jails often have repetitive boilerplate that can't be factored out of jail.conf using .include "<glob>"; or requires external lookups that require wrappers around jail(8) e.g. network interface creation or IP allocation, database lookups.

This patch checks if a jail.conf(5) is executable by trying to reopen its file descriptor for execution in which case it runs it as child process of the jail command and parses the child's standard output.

Test Plan
  • Write example configurations e.g. create a jail from each sub-directory in a directory of jail names.
  • Emit invalid jail.conf output.
  • Emit valid jail.conf, but exit with status != 0

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped

Event Timeline

I'm a little torn on the idea itself, but here's some review

usr.sbin/jail/config.c
319

Please move side-effecty initialization like this out of the declarations

320

__DECONST(char *, cfname) gets the point across a little better imo

322

These should be converted to /* single line comments */

353

Extra spacing inside the if should be removed (just after and just before opening and closing parentheses)

372

file should be declared at the top

373

file == NULL

383

Should be initialized to 0 or so to be safe.

402

Space after the above declarations

404

Extraneous spacing again around the parentheses

Using this /etc/jail.conf:

.include "/usr/local/etc/jail[.]conf";
.include "/etc/jail.d/*.conf";
.include "/usr/local/etc/jail.d/*.conf";

Place this script into /usr/local/etc/jail.d/dirs.conf:

#!/bin/sh
set -Ceu
cd "${1%/*}"
for dir in *.d; do
        [ -d "$dir" ] || continue
        name="${dir%.d}"
        printf '%s {\n\t.include "%s/*.conf";\n}\n' "$name" "$dir"
done

To write a block like this for each ${dir}.d jail directory, deriving the jail name from the directory name (stripping the .d suffix):

jail_name { # variable references aren't allowed in jail names
    .include "$name.d/*.conf";
}
usr.sbin/jail/config.c
383

open_config() sets it to -1 before attempting to fork. It's an out parameter not an in-out parameter so imo it doesn't have to be initialised, but there is no harm in initialising it.

It would make sense for the exec.clean parameter to apply to the config execution. Bit of a chicken and egg problem there, but there's still the "-l" flag.

But I'm still just not convinced this is a good idea. It's pretty far outside the norm of how configuration files are expected to work.

Put the executable jail.conf(5) behind the -x option (as chicken bit).

I had considered that the -l (exec clean) flag should be considered, but decided it really only makes sense for keeping the jail environment clean.

This revision is now accepted and ready to land.Jul 17 2025, 3:50 PM
This revision now requires review to proceed.Jul 22 2025, 8:10 PM
This revision is now accepted and ready to land.Aug 12 2025, 5:06 PM
jamie requested changes to this revision.Aug 26 2025, 8:14 PM
jamie added inline comments.
usr.sbin/jail/config.c
316

"pid_t* pid" is much more intuitive and readable than "pid_t pid[static 1]".

330

I know you were told to use single line comments, but that only counts when not overrunning 80 columns.

336

The point of the -x flag was so existing configurations that happen to have that bit set on their config files "just work." While failing with an appropriate error message is better than trying to execute a config file, it's not just working. I was expecting -x to just enable the whole attempting to execute the file.

That does mean that if someone uses an actual executable script (or even binary) as a config file without adding -x, the error message may be non-intuitive. But that's the price of POLA for existing users.

346

The code lines themselves should be limited to 80 colums. There are are a number of lines that are just a little longer, but line breaks and with proper indentation are much more readable than run-ons, at least for my archaic 80-coumn emacs windows.

346

This isn't always the current jail. If the .include line is withing a jail definition, this will be the name of that jail. But if the .include line is after a jail definition was completed, this will still name that jail.

This may take add an actual global current_jail variable that can make this distinction.

347

Do we really need to tell the config file what its name is? I suppose that could be useful, so you can use the same script file to act differently in multiple locations. Still, that seems the config script/program's

But do we really need to tell it in so many different ways? Surely if the config script wants to break apart its pathname into so many pieces, it's welcome to do so.

370

Again, if the config script wants environment variables that are the same as its arguments, it can do that itself. This is just oddly specific corner-case work for running a script.

Also, error messages are nice, but the setenv can all be collapsed into a single setenv error. Either there's enough memory for environment variables or there isn't. Of one of them is NULL, which is programmer error and doesn't appear to be the case anyway. But then, that's unimportant if you don't use environment variables to tell the script what its argv is.

388

Turns out fexecve of a script requires fdescfs to be mounted. That's not always the case (it's not on my own system). A core system component shouldn't depend on this.

Instead of exec_fd and fexecve, just regular execve should work just fine. Perhaps you need exec_fd just to check the executable permission, but it's not suitable for actual execution.

This revision now requires changes to proceed.Aug 26 2025, 8:14 PM

Use normal pointer syntax for the pid pointer instead of pid_t pid[static 1].

Replace fexecve(2) with execve(2) since fexecve(2) doesn't work with (shell) scripts unless /dev/fd is mounted with the non-standard "nodup" option.

See PR #294780 for more details why this change is required.

Use single line comments in open_file() where appropriate.

Switch to single line comments inside the open_file() function.

usr.sbin/jail/config.c
336

As discussed on the jails call requiring users of this feature to add an extra flag to every invocation of the jail command would make it effectively impossible to use with existing jail scripts.

Reformatted the code to 80 columns.

As discussed putting this feature behind a paranoia flag to not cause issues for anyone that just happens to have an accidentally executable jail.conf(5) makes it effectively useless.

Use the parser's existing stack of nested jails instead of incorrectly assuming the last jail parsed is also the current jail. This incorrect assumption is not true after one or more jail blocks have been closed and no new jail block has been opened.

Second attempt to update the other source files too.

For some reason Phabricator doesn't register that I edited two additional files in the latest patch I uploaded and the raw diff doesn't include them either?!?

Try to copy and paste the patch instead of uploading as a file. Sorry for the noise.

Since Phabricator ignores my attempts to update this review to modify multiple files with the diff I created a GitHub pull request at https://github.com/freebsd/freebsd-src/pull/2164 .

New attempt with git format-patch -U999999 *sigh*.

I had hoped to attend the jail user call today to be able to discuss this, but the Discord event has it an hour off and I didn't discover the authoritative source @ callfortesting.org until I was wondering why nobody was showing up. *deep sigh*

I don't think we're comfortable, as a project, to enable this for all users of jail(8) by default without an additional flag. I appreciate that you want to do stuff like this with existing jail scripts but this is a huge POLA violation (even assuming proper communication across a major branch update) with security implications, and I don't think enabling maybe-executable config scripts is a pattern that we really want propagating.

There was some discussion out-of-band after a concerned user reached out about this, and it was pointed out to me that automountd does the same thing, so I've pitched a review to try and neuter that a bit because that's terrifying: D56680.

I had hoped to attend the jail user call today to be able to discuss this, but the Discord event has it an hour off and I didn't discover the authoritative source @ callfortesting.org until I was wondering why nobody was showing up. *deep sigh*

I don't think we're comfortable, as a project, to enable this for all users of jail(8) by default without an additional flag. I appreciate that you want to do stuff like this with existing jail scripts but this is a huge POLA violation (even assuming proper communication across a major branch update) with security implications, and I don't think enabling maybe-executable config scripts is a pattern that we really want propagating.

There was some discussion out-of-band after a concerned user reached out about this, and it was pointed out to me that automountd does the same thing, so I've pitched a review to try and neuter that a bit because that's terrifying: D56680.

The jail.conf(5) format already defines hooks that by design execute as root on the host (exec.prepare/created/prestart/poststart/prestop/poststop/release). Having any untrusted jail.conf(5) on the system is a game over scenario similar to having a malicous /etc/crontab or rc.d script installed. Moving the attack surface a few milliseconds forward from the exec.prepare (or exec.prestop when removing a jail) stage to the config parsing stage doesn't increase the attack surface in a meaningful way.

Having the dynamic jail.conf process drop privs is good defense in depth when possible, but since it's sole function is to emit a jail.conf(5) snippet to standard output the security gains are minimal. Jail(8) will parse and run the jail.conf(5) including any shell hooks as root no matter which user the dynamic configuration ran as. Jail(8) also doesn't stop your when you put something stupid like .include "/tmp/dumb_idea.*.conf"; in your /etc/jail.conf.

As I discussed with Jamie: putting the executable jail.conf behind a command line flag would make this feature unusable with any existing script invoking jail(8) even the base system jail rc.d script. Requiring users to first create an explicit opt-in file in a hardcoded location writable only by root would be possible.

@kevans: Can you think of a realistic situation where someone will have their jail configuration unintentionally executable? I don't think chmod -R 777 /etc is a supported configuration. Are we forced to support FAT32 as root file system on any strange platform or something like that?

@kevans: Can you think of a realistic situation where someone will have their jail configuration unintentionally executable? I don't think chmod -R 777 /etc is a supported configuration. Are we forced to support FAT32 as root file system on any strange platform or something like that?

That's the wrong question. For security related things, you have to default to 'fail safe' and this feature fails to meet that criteria.

In D46284#1299031, @imp wrote:

@kevans: Can you think of a realistic situation where someone will have their jail configuration unintentionally executable? I don't think chmod -R 777 /etc is a supported configuration. Are we forced to support FAT32 as root file system on any strange platform or something like that?

That's the wrong question. For security related things, you have to default to 'fail safe' and this feature fails to meet that criteria.

Accepted. How do you want to make the opt-in explicit without cluttering/breaking the CLI interface? Would requiring root to to create /var/db/optin-dynamic-jail.conf be an acceptable safety net?

I had hoped to attend the jail user call today to be able to discuss this, but the Discord event has it an hour off and I didn't discover the authoritative source @ callfortesting.org until I was wondering why nobody was showing up. *deep sigh*

I don't think we're comfortable, as a project, to enable this for all users of jail(8) by default without an additional flag. I appreciate that you want to do stuff like this with existing jail scripts but this is a huge POLA violation (even assuming proper communication across a major branch update) with security implications, and I don't think enabling maybe-executable config scripts is a pattern that we really want propagating.

There was some discussion out-of-band after a concerned user reached out about this, and it was pointed out to me that automountd does the same thing, so I've pitched a review to try and neuter that a bit because that's terrifying: D56680.

The jail.conf(5) format already defines hooks that by design execute as root on the host (exec.prepare/created/prestart/poststart/prestop/poststop/release). Having any untrusted jail.conf(5) on the system is a game over scenario similar to having a malicous /etc/crontab or rc.d script installed. Moving the attack surface a few milliseconds forward from the exec.prepare (or exec.prestop when removing a jail) stage to the config parsing stage doesn't increase the attack surface in a meaningful way.

I can audit static config in an automated manner for these things being set and bail out if they're unsafe, I can't easily do that if they're being produced by something we execute past the point of no return.

Having the dynamic jail.conf process drop privs is good defense in depth when possible, but since it's sole function is to emit a jail.conf(5) snippet to standard output the security gains are minimal. Jail(8) will parse and run the jail.conf(5) including any shell hooks as root no matter which user the dynamic configuration ran as. Jail(8) also doesn't stop your when you put something stupid like .include "/tmp/dumb_idea.*.conf"; in your /etc/jail.conf.

As I discussed with Jamie: putting the executable jail.conf behind a command line flag would make this feature unusable with any existing script invoking jail(8) even the base system jail rc.d script. Requiring users to first create an explicit opt-in file in a hardcoded location writable only by root would be possible.

Can you provide a little more detail here for the 'existing scripts' you're referencing? I'm not sure I see where the jail rc.d script is a problem, my recollection is that it honors jail_flags in rc.conf (and maybe even a few other variants of that name) and those can easily include a new flag to enable it.

@kevans: Can you think of a realistic situation where someone will have their jail configuration unintentionally executable? I don't think chmod -R 777 /etc is a supported configuration. Are we forced to support FAT32 as root file system on any strange platform or something like that?

You don't really have to use it as a rootfs, it's sufficient to have a flash drive with your config backed up that you tried to restore from without thinking of the consequences, because this is a really subtle behavior.

crest_freebsd_rlwinm.de added a reviewer: kevans.

Make dynamic (aka executable) jail.conf opt-in via the already parsed partial jail.conf.

Example /etc/jail.conf:

allow.exec_include; # Enable dynamic include files.
.include "/etc/jail.d/*.conf";

The added internal boolean parameter allow.exec_include is backward compatible with every working jail.conf that can exist, because attemptint to set an unknown parameter is a syntax error and the parameter is new.

Use the last instead of the first match (per jail) to enable dynamic configuration.

crest_freebsd_rlwinm.de added inline comments.
usr.sbin/jail/config.c
346

I've fixed this by chaging the scope of the current_jail variable from (file) static to extern. This way it's using the stack of (potentially) nested jails already maintained by the parser.

347

We don't have to, but I've found that the test scripts I wrote needed (most of) that information. It's almost free to obtain and export in a ready to use fashion compared to fork()+fexecve(). It avoids the scripts having to fork out to realpath/dirname/basename all the time. My goal is to make it as painless as possible to write such helpers.