Currently, there is no way for shell scripts to safely access resources protected by POSIX locking (fcntl with the F_SETLK/F_SETLKW commands). For example, the glibc function lckpwdf(), used to protect access to the /etc/shadow database, works by taking a F_SETLKW on /etc/.pwd.lock . Due to the odd semantics of POSIX locking (e.g. released when any file descriptor associated to the inode is closed), we cannot usefully directly expose the POSIX F_SETLK/F_SETLKW commands. However, linux 3.15 introduced F_OFD_SETLK[W], with semantics wrt. ownership and release better matching those of flock(2), and crucially they do conflict with locks obtained via F_SETLK[W]. With this, a shell script can do exec 4> /etc/.pwd.lock flock --fcntl 4 <access/modify /etc/shadow ...> flock --fcntl --unlock 4 # or just exit without conflicting with passwd(1) or other utilities that access/modify /etc/shadow. No single-letter shorthand is defined for the option, because this is somewhat low-level and the user really needs to know what he is doing. Also, this leaves the door open for teaching --fcntl to accept an optional argument: "ofd", the default, and "posix", should anyone find a use for flock(1) taking a F_SETLK[W] lock. Signed-off-by: Rasmus Villemoes <rasmus.villemoes@xxxxxxxxx> --- v2: - Shorten option name to --fcntl instead of --fcntl-ofd. - Use a do_lock() helper function switching on the API to use, making the while () condition easier to read and making it simpler to add the mentioned --fcntl=posix should the need arise. - Fix up places that need HAVE_FCNTL_OFD_LOCKS guarding. configure.ac | 6 ++++ meson.build | 3 ++ sys-utils/flock.c | 82 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index c302732e7..441b09440 100644 --- a/configure.ac +++ b/configure.ac @@ -434,6 +434,12 @@ AC_CHECK_DECLS([PR_REP_CAPACITY], [], [], [ #include <linux/pr.h> ]) +AC_CHECK_DECL([F_OFD_SETLK], + [AC_DEFINE([HAVE_FCNTL_OFD_LOCKS], [1], + [Define to 1 if fcntl.h defines F_OFD_ constants])], [], [ +#include <fcntl.h> +]) + AC_CHECK_HEADERS([security/openpam.h], [], [], [ #ifdef HAVE_SECURITY_PAM_APPL_H #include <security/pam_appl.h> diff --git a/meson.build b/meson.build index 99126f7aa..004c849f1 100644 --- a/meson.build +++ b/meson.build @@ -704,6 +704,9 @@ conf.set('HAVE_DECL_BLK_ZONE_REP_CAPACITY', have ? 1 : false) have = cc.has_header_symbol('linux/pr.h', 'PR_REP_CAPACITY') conf.set('HAVE_DECL_PR_REP_CAPACITY', have ? 1 : false) +have = cc.has_header_symbol('fcntl.h', 'F_OFD_SETLK', args: '-D_GNU_SOURCE') +conf.set('HAVE_FCNTL_OFD_LOCKS', have ? 1 : false) + code = ''' #include <time.h> #if !@0@ diff --git a/sys-utils/flock.c b/sys-utils/flock.c index 7d878ff81..17088ce7e 100644 --- a/sys-utils/flock.c +++ b/sys-utils/flock.c @@ -48,6 +48,11 @@ #include "monotonic.h" #include "timer.h" +enum { + API_FLOCK, + API_FCNTL_OFD, +}; + static void __attribute__((__noreturn__)) usage(void) { fputs(USAGE_HEADER, stdout); @@ -70,6 +75,9 @@ static void __attribute__((__noreturn__)) usage(void) fputs(_( " -o, --close close file descriptor before running command\n"), stdout); fputs(_( " -c, --command <command> run a single command string through the shell\n"), stdout); fputs(_( " -F, --no-fork execute command without forking\n"), stdout); +#ifdef HAVE_FCNTL_OFD_LOCKS + fputs(_( " --fcntl use fcntl(F_OFD_SETLK) rather than flock()\n"), stdout); +#endif fputs(_( " --verbose increase verbosity\n"), stdout); fputs(USAGE_SEPARATOR, stdout); fprintf(stdout, USAGE_HELP_OPTIONS(26)); @@ -126,6 +134,53 @@ static void __attribute__((__noreturn__)) run_program(char **cmd_argv) _exit((errno == ENOMEM) ? EX_OSERR : EX_UNAVAILABLE); } +#ifdef HAVE_FCNTL_OFD_LOCKS +static int flock_to_fcntl_type(int op) +{ + switch (op) { + case LOCK_EX: + return F_WRLCK; + case LOCK_SH: + return F_RDLCK; + case LOCK_UN: + return F_UNLCK; + default: + errx(EX_SOFTWARE, _("internal error, unknown operation %d"), op); + } +} + +static int fcntl_lock(int fd, int op, int block) +{ + struct flock arg = { + .l_type = flock_to_fcntl_type(op), + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + int cmd = (block & LOCK_NB) ? F_OFD_SETLK : F_OFD_SETLKW; + return fcntl(fd, cmd, &arg); +} +#endif + +static int do_lock(int api, int fd, int op, int block) +{ + switch (api) { + case API_FLOCK: + return flock(fd, op | block); +#ifdef HAVE_FCNTL_OFD_LOCKS + case API_FCNTL_OFD: + return fcntl_lock(fd, op, block); +#endif + /* + * Should never happen, api can never have values other than + * API_*, and must be API_FLOCK when !HAVE_FCNTL_OFD_LOCKS. + */ + default: + errx(EX_SOFTWARE, _("internal error, unknown api %d"), api); + } +} + + int main(int argc, char *argv[]) { struct ul_timer timer; @@ -140,6 +195,7 @@ int main(int argc, char *argv[]) int no_fork = 0; int status; int verbose = 0; + int api = API_FLOCK; struct timeval time_start = { 0 }, time_done = { 0 }; /* * The default exit code for lock conflict or timeout @@ -149,7 +205,8 @@ int main(int argc, char *argv[]) char **cmd_argv = NULL, *sh_c_argv[4]; const char *filename = NULL; enum { - OPT_VERBOSE = CHAR_MAX + 1 + OPT_VERBOSE = CHAR_MAX + 1, + OPT_FCNTL, }; static const struct option long_options[] = { {"shared", no_argument, NULL, 's'}, @@ -163,6 +220,9 @@ int main(int argc, char *argv[]) {"close", no_argument, NULL, 'o'}, {"no-fork", no_argument, NULL, 'F'}, {"verbose", no_argument, NULL, OPT_VERBOSE}, +#ifdef HAVE_FCNTL_OFD_LOCKS + {"fcntl", no_argument, NULL, OPT_FCNTL}, +#endif {"help", no_argument, NULL, 'h'}, {"version", no_argument, NULL, 'V'}, {NULL, 0, NULL, 0} @@ -217,6 +277,11 @@ int main(int argc, char *argv[]) if (conflict_exit_code < 0 || conflict_exit_code > 255) errx(EX_USAGE, _("exit code out of range (expected 0 to 255)")); break; +#ifdef HAVE_FCNTL_OFD_LOCKS + case OPT_FCNTL: + api = API_FCNTL_OFD; + break; +#endif case OPT_VERBOSE: verbose = 1; break; @@ -234,6 +299,13 @@ int main(int argc, char *argv[]) errx(EX_USAGE, _("the --no-fork and --close options are incompatible")); + /* + * For fcntl(F_OFD_SETLK), an exclusive lock requires that the + * file is open for write. + */ + if (api != API_FLOCK && type == LOCK_EX) + open_flags = O_WRONLY; + if (argc > optind + 1) { /* Run command */ if (!strcmp(argv[optind + 1], "-c") || @@ -280,9 +352,15 @@ int main(int argc, char *argv[]) if (verbose) gettime_monotonic(&time_start); - while (flock(fd, type | block)) { + while (do_lock(api, fd, type, block)) { switch (errno) { case EWOULDBLOCK: + /* + * Per the man page, for fcntl(), EACCES may + * be returned and means the same as + * EAGAIN/EWOULDBLOCK. + */ + case EACCES: /* -n option set and failed to lock. */ if (verbose) warnx(_("failed to get lock")); -- 2.40.1.1.g1c60b9335d