diff --git a/chmod.c b/chmod.c index 8bbf791aa..8368e5b31 100644 --- a/chmod.c +++ b/chmod.c @@ -29,7 +29,7 @@ extern mode_t orig_umask; struct chmod_mode_struct { struct chmod_mode_struct *next; - int ModeAND, ModeOR; + int ModeAND, ModeOR, ModeCOPY_SRC, ModeCOPY_DST, ModeCOPY_AND, ModeOP; char flags; }; @@ -43,6 +43,20 @@ struct chmod_mode_struct { #define STATE_2ND_HALF 2 #define STATE_OCTAL_NUM 3 +static int mode_dest_special_bits(int where) +{ + int bits = 0; + + if (where & 0100) + bits |= S_ISUID; + if (where & 0010) + bits |= S_ISGID; + if (where & 0001) + bits |= S_ISVTX; + + return bits; +} + /* Parse a chmod-style argument, and break it down into one or more AND/OR * pairs in a linked list. We return a pointer to new items on success * (appending the items to the specified list), or NULL on error. */ @@ -50,13 +64,13 @@ struct chmod_mode_struct *parse_chmod(const char *modestr, struct chmod_mode_struct **root_mode_ptr) { int state = STATE_1ST_HALF; - int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0; + int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0, copybits = 0; struct chmod_mode_struct *first_mode = NULL, *curr_mode = NULL, *prev_mode = NULL; while (state != STATE_ERROR) { if (!*modestr || *modestr == ',') { - int bits; + int bits, where_specified; if (!op) { state = STATE_ERROR; @@ -70,9 +84,10 @@ struct chmod_mode_struct *parse_chmod(const char *modestr, first_mode = curr_mode; curr_mode->next = NULL; - if (where) + where_specified = where; + if (where) { bits = where * what; - else { + } else { where = 0111; bits = (where * what) & ~orig_umask; } @@ -81,18 +96,35 @@ struct chmod_mode_struct *parse_chmod(const char *modestr, case CHMOD_ADD: curr_mode->ModeAND = CHMOD_BITS; curr_mode->ModeOR = bits + topoct; + curr_mode->ModeCOPY_SRC = copybits; + curr_mode->ModeCOPY_DST = where; + curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask; + curr_mode->ModeOP = op; break; case CHMOD_SUB: curr_mode->ModeAND = CHMOD_BITS - bits - topoct; curr_mode->ModeOR = 0; + curr_mode->ModeCOPY_SRC = copybits; + curr_mode->ModeCOPY_DST = where; + curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask; + curr_mode->ModeOP = op; break; case CHMOD_EQ: - curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0); + curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0) + - (copybits ? mode_dest_special_bits(where) : 0); curr_mode->ModeOR = bits + topoct; + curr_mode->ModeCOPY_SRC = copybits; + curr_mode->ModeCOPY_DST = where; + curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask; + curr_mode->ModeOP = op; break; case CHMOD_SET: curr_mode->ModeAND = 0; curr_mode->ModeOR = bits; + curr_mode->ModeCOPY_SRC = 0; + curr_mode->ModeCOPY_DST = 0; + curr_mode->ModeCOPY_AND = CHMOD_BITS; + curr_mode->ModeOP = op; break; } @@ -103,7 +135,7 @@ struct chmod_mode_struct *parse_chmod(const char *modestr, modestr++; state = STATE_1ST_HALF; - where = what = op = topoct = topbits = flags = 0; + where = what = op = topoct = topbits = flags = copybits = 0; } switch (state) { @@ -159,26 +191,53 @@ struct chmod_mode_struct *parse_chmod(const char *modestr, case STATE_2ND_HALF: switch (*modestr) { case 'r': + if (copybits) + state = STATE_ERROR; what |= 4; break; case 'w': + if (copybits) + state = STATE_ERROR; what |= 2; break; case 'X': + if (copybits) + state = STATE_ERROR; flags |= FLAG_X_KEEP; /* FALL THROUGH */ case 'x': + if (copybits) + state = STATE_ERROR; what |= 1; break; case 's': + if (copybits) + state = STATE_ERROR; if (topbits) topoct |= topbits; else topoct = 04000; break; case 't': + if (copybits) + state = STATE_ERROR; topoct |= 01000; break; + case 'u': + if (what || topoct || copybits) + state = STATE_ERROR; + copybits = 0100; + break; + case 'g': + if (what || topoct || copybits) + state = STATE_ERROR; + copybits = 0010; + break; + case 'o': + if (what || topoct || copybits) + state = STATE_ERROR; + copybits = 0001; + break; default: state = STATE_ERROR; break; @@ -212,6 +271,20 @@ struct chmod_mode_struct *parse_chmod(const char *modestr, return first_mode; } +static int mode_copy_bits(int mode, int copy_src, int copy_dst, int copy_and) +{ + int copy_bits = 0; + + if (copy_src & 0100) + copy_bits |= (mode >> 6) & 7; + if (copy_src & 0010) + copy_bits |= (mode >> 3) & 7; + if (copy_src & 0001) + copy_bits |= mode & 7; + + return (copy_dst * copy_bits) & copy_and; +} + /* Takes an existing file permission and a list of AND/OR changes, and * create a new permissions. */ @@ -219,17 +292,25 @@ int tweak_mode(int mode, struct chmod_mode_struct *chmod_modes) { int IsX = mode & 0111; int NonPerm = mode & ~CHMOD_BITS; + int copy_bits; for ( ; chmod_modes; chmod_modes = chmod_modes->next) { if ((chmod_modes->flags & FLAG_DIRS_ONLY) && !S_ISDIR(NonPerm)) continue; if ((chmod_modes->flags & FLAG_FILES_ONLY) && S_ISDIR(NonPerm)) continue; + copy_bits = mode_copy_bits(mode, chmod_modes->ModeCOPY_SRC, + chmod_modes->ModeCOPY_DST, + chmod_modes->ModeCOPY_AND); mode &= chmod_modes->ModeAND; if ((chmod_modes->flags & FLAG_X_KEEP) && !IsX && !S_ISDIR(NonPerm)) mode |= chmod_modes->ModeOR & ~0111; else mode |= chmod_modes->ModeOR; + if (chmod_modes->ModeOP == CHMOD_SUB) + mode &= CHMOD_BITS - copy_bits; + else + mode |= copy_bits; } return mode | NonPerm; diff --git a/rsync.1.md b/rsync.1.md index 7aa34f9e7..cae408c5d 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -1513,6 +1513,16 @@ expand it. > --chmod=D2775,F664 + Symbolic permission-copy modes are also allowed, such as `g=u`, `o=g` or + `g-o`. A permission-copy item may copy from one class only (`u`, `g` or + `o`) and cannot be combined with `rwxXst` permission letters in the same + item. Use comma-separated items when you need both behaviours, such as + `g=o,o=`. + + A permission-copy `=` item also clears the special bit for each destination + class it updates (`u` clears setuid, `g` clears setgid, and `o` clears + sticky), matching GNU **chmod** behaviour. + It is also legal to specify multiple `--chmod` options, as each additional option is just appended to the list of changes to make. diff --git a/testsuite/chmod-option_test.py b/testsuite/chmod-option_test.py index 57be6486d..ad0fc77be 100644 --- a/testsuite/chmod-option_test.py +++ b/testsuite/chmod-option_test.py @@ -11,8 +11,8 @@ from rsyncfns import ( FROMDIR, SCRATCHDIR, TODIR, - build_rsyncd_conf, checkit, makepath, rmtree, - run_rsync, start_test_daemon, + build_rsyncd_conf, check_perms, checkit, makepath, rmtree, + run_rsync, start_test_daemon, test_fail, ) @@ -62,6 +62,37 @@ checkit(['-avv', '--chmod', 'ug-s,a+rX,D+w', f'{FROMDIR}/', f'{TODIR}/'], checkdir, TODIR) +def check_permcopy(chmod_arg, start_mode, expected, is_dir=False): + rmtree(FROMDIR) + rmtree(TODIR) + makepath(FROMDIR) + permcopy = FROMDIR / 'permcopy' + if is_dir: + permcopy.mkdir() + else: + permcopy.write_text('permcopy\n') + os.chmod(permcopy, start_mode) + run_rsync('-avv', f'--chmod={chmod_arg}', f'{FROMDIR}/', f'{TODIR}/') + check_perms(TODIR / 'permcopy', expected) + + +# Exercise chmod(1)-style permission copies. +check_permcopy('g=o,o=', 0o647, 'rw-rwx---') +check_permcopy('g=u', 0o741, 'rwxrwx--x') +check_permcopy('g-o', 0o775, 'rwx-w-r-x') +check_permcopy('u=g', 0o4755, 'r-xr-xr-x') +check_permcopy('g=u', 0o2755, 'rwxrwxr-x') +check_permcopy('o=u', 0o1750, 'rwxr-xrwx', is_dir=True) + +rmtree(FROMDIR) +rmtree(TODIR) +makepath(FROMDIR) +(FROMDIR / 'permcopy').write_text('permcopy\n') +proc = run_rsync('-avv', '--chmod=g=ur', f'{FROMDIR}/', f'{TODIR}/', + check=False, capture_output=True) +if proc.returncode == 0: + test_fail('--chmod=g=ur was not rejected') + # Now exercise the F-only chmod path. rmtree(FROMDIR) rmtree(checkdir)