mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-10 12:18:17 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
471e17dc0d | ||
|
|
7a36355a8c | ||
|
|
40c93b173e | ||
|
|
6dfa73fc90 | ||
|
|
d0ededae4e | ||
|
|
46ad34f28d | ||
|
|
3d10e84266 | ||
|
|
2d1a91eb0f | ||
|
|
5cd4c682c8 | ||
|
|
e8c79d2d62 | ||
|
|
9e40ef0eaf | ||
|
|
f50d53d056 | ||
|
|
11e43daf90 | ||
|
|
ea5a3a277f | ||
|
|
d4d183c58c | ||
|
|
9a382baddb | ||
|
|
63fcfa399e | ||
|
|
2979d8eddc | ||
|
|
bb6329bc8c | ||
|
|
d12bdb1579 | ||
|
|
d09edb85e6 | ||
|
|
36be3b4e70 | ||
|
|
1bea181096 | ||
|
|
314abcc437 | ||
|
|
3113011218 | ||
|
|
0f6f35e522 | ||
|
|
631f9bd464 | ||
|
|
bb806288b7 | ||
|
|
d1756203b9 | ||
|
|
d95217fbbe | ||
|
|
653c4e8db9 | ||
|
|
501165218c | ||
|
|
6bad3be6fc | ||
|
|
2a20d6ec0a | ||
|
|
4d7c243ca6 | ||
|
|
cf15b1bb24 | ||
|
|
ba3e9d681b | ||
|
|
84832f0ff7 | ||
|
|
0789709aeb | ||
|
|
b1d2c32b1d | ||
|
|
a0952930a1 | ||
|
|
8e0bd08a62 | ||
|
|
d5cce08503 | ||
|
|
bf3a11cf24 | ||
|
|
44fdf0bc43 | ||
|
|
1078876a31 | ||
|
|
f7c67d4a1d | ||
|
|
bb804f80f6 | ||
|
|
6a15079cab | ||
|
|
56f6b67453 | ||
|
|
fd8bc41967 | ||
|
|
2a44bf4e9f | ||
|
|
abe4a2717e | ||
|
|
c458873481 | ||
|
|
41f411d6e9 | ||
|
|
95e7f04148 | ||
|
|
4b4bf80f8d | ||
|
|
dc72d0409a | ||
|
|
47df88d5fb | ||
|
|
ab373ad1d0 | ||
|
|
de38616b25 | ||
|
|
c933f6227d | ||
|
|
dcc3c9c51a | ||
|
|
78e10e7e32 | ||
|
|
f704aa4aed | ||
|
|
8da62816c8 | ||
|
|
5bf9940de4 | ||
|
|
00160f76b8 | ||
|
|
1eab6f43e8 | ||
|
|
c1df37d954 | ||
|
|
93e0d01798 | ||
|
|
2f9bbe835c | ||
|
|
9022a0a5bc | ||
|
|
c3abc6c095 | ||
|
|
576ce422fe | ||
|
|
06212f112a | ||
|
|
c529163ef0 | ||
|
|
33257bdcaa | ||
|
|
2fbb708f15 | ||
|
|
b607369f5f | ||
|
|
1c0bd88f0b | ||
|
|
05dabb0fdb | ||
|
|
ce0f6d5a25 | ||
|
|
6edb7dea2a | ||
|
|
8678d89b2c | ||
|
|
e7986502cb | ||
|
|
a52cb0abc9 | ||
|
|
ec6c8fd932 | ||
|
|
5eb99bb6b2 | ||
|
|
4572d1743c | ||
|
|
32f36fa845 | ||
|
|
5c20cd157a | ||
|
|
8d82b07b54 | ||
|
|
5f0f8f298e | ||
|
|
93a67aa995 | ||
|
|
4ce54db5ef | ||
|
|
51618b74c0 | ||
|
|
6b885e5175 | ||
|
|
acfc94ef48 | ||
|
|
abcf37a1d3 | ||
|
|
9694994d5c | ||
|
|
5cf902f87f | ||
|
|
bbcef46455 | ||
|
|
72f2ceaa88 | ||
|
|
c5bc4e3677 | ||
|
|
1a69d2e20d | ||
|
|
4d8cbbecac | ||
|
|
2532e9c17b | ||
|
|
5da6051243 | ||
|
|
4694b73d75 | ||
|
|
957ce5038d | ||
|
|
f0949ad0de | ||
|
|
38bc594f87 | ||
|
|
7053485ac1 | ||
|
|
05bd16a469 | ||
|
|
4b4c6809ed | ||
|
|
0293df8a81 | ||
|
|
b3a560061b | ||
|
|
0bfcd3b0f2 | ||
|
|
a646ded755 | ||
|
|
470cb86bdf | ||
|
|
c0e6948d0f | ||
|
|
8367407f98 | ||
|
|
b471a29888 | ||
|
|
7b16872eff | ||
|
|
7aea9d5f8e | ||
|
|
db380b62ac | ||
|
|
4ce6d097fd | ||
|
|
21fade1bbb | ||
|
|
ab4e81d749 | ||
|
|
fd86492913 | ||
|
|
cfd40f55cb | ||
|
|
de9000eb30 | ||
|
|
3fe1ed512c | ||
|
|
3b826d6683 | ||
|
|
de6ed4724d | ||
|
|
d978342145 | ||
|
|
594ab1e194 | ||
|
|
f10f666306 | ||
|
|
93c0e0144f | ||
|
|
e6044af38f | ||
|
|
8c78fe8e0b | ||
|
|
15141f6803 | ||
|
|
70c121d86f | ||
|
|
78767976ef | ||
|
|
71b01c4b5c | ||
|
|
7105287d46 | ||
|
|
d2aa9fb034 | ||
|
|
527b61afb9 | ||
|
|
912644c94d | ||
|
|
7761384497 | ||
|
|
9da5b5450f | ||
|
|
ede9a1a3ba | ||
|
|
0f5986cda8 | ||
|
|
7490ad2420 | ||
|
|
3534cab477 | ||
|
|
cfbf411fc3 | ||
|
|
feb26929de | ||
|
|
f602f7d86d | ||
|
|
81a88dc590 | ||
|
|
da4354303e | ||
|
|
96ed617a33 | ||
|
|
c53d107f97 | ||
|
|
4992f7e98b | ||
|
|
5022446815 | ||
|
|
39fbc02b79 | ||
|
|
a308186f4b | ||
|
|
42f7c32ce5 | ||
|
|
94a7ccd63d | ||
|
|
d443e8dc93 | ||
|
|
1777c884cf | ||
|
|
fbeb553b73 | ||
|
|
07bf9af5b5 | ||
|
|
fd4c75116b | ||
|
|
ad64e99590 | ||
|
|
674c175e31 | ||
|
|
0d0e505902 | ||
|
|
ce0e1c3ced | ||
|
|
22efd249ad | ||
|
|
a83b76aa30 | ||
|
|
7a6d45b02b | ||
|
|
54ee418964 | ||
|
|
2e0b572057 | ||
|
|
6fa1f15c66 | ||
|
|
13a010959b | ||
|
|
9502c4a1c7 | ||
|
|
8a3a3e4de1 | ||
|
|
07a7a19790 | ||
|
|
03535a62a5 | ||
|
|
c144c0931c | ||
|
|
88b4d77b18 | ||
|
|
32ef91adcb | ||
|
|
298efc7fe4 | ||
|
|
c304dd74e4 | ||
|
|
fe3ab9ca9b | ||
|
|
7b6fbf5940 | ||
|
|
2ae254671c | ||
|
|
11d3090730 | ||
|
|
4e17fd8505 | ||
|
|
63f744adb1 | ||
|
|
96ff1a0ce2 | ||
|
|
c158d241d4 | ||
|
|
27be267ee3 | ||
|
|
8d3b7be875 | ||
|
|
e7c017914f | ||
|
|
a701236c64 | ||
|
|
0f84b4a776 | ||
|
|
334aacb448 | ||
|
|
1624b6daac | ||
|
|
33beee7652 | ||
|
|
e927a00b2e | ||
|
|
2d10070c90 | ||
|
|
cd22f195ef | ||
|
|
502fb584c4 | ||
|
|
68ff15e35c | ||
|
|
fe69ae7a7d | ||
|
|
5eb05f7409 | ||
|
|
054d0eb475 | ||
|
|
21ddac1a67 | ||
|
|
3d3150c24a | ||
|
|
fe1adcfd73 | ||
|
|
a3de9e553d | ||
|
|
81850e8b27 | ||
|
|
49cb45adcc | ||
|
|
7e58170a9c | ||
|
|
aed7714325 | ||
|
|
853050d25d | ||
|
|
0abb4ae6cb | ||
|
|
ce17dfe8f0 | ||
|
|
0071ba5be2 | ||
|
|
5d1b15086d | ||
|
|
d1d3829901 | ||
|
|
3c674b7fdf | ||
|
|
065ec23e4b | ||
|
|
161608034e | ||
|
|
885a0a8ecc | ||
|
|
32aa900fd0 | ||
|
|
ae014a56f3 | ||
|
|
72098206ce | ||
|
|
493a8e99d6 | ||
|
|
a55ab05c19 | ||
|
|
4705b6fb92 | ||
|
|
1346bc623f | ||
|
|
88cee08963 | ||
|
|
1604890301 | ||
|
|
d199b43149 | ||
|
|
3eeedf8d08 | ||
|
|
b62d7ed6d4 | ||
|
|
dd3e6bfece | ||
|
|
defd7110ac | ||
|
|
54965efca9 | ||
|
|
adab781538 | ||
|
|
3b84610ccb | ||
|
|
9062840a91 | ||
|
|
3c9a12011e | ||
|
|
5cb4b8290b | ||
|
|
e6956e0a30 | ||
|
|
e2a24e8581 | ||
|
|
f5fa55672d | ||
|
|
5553271274 | ||
|
|
3f5884a3bb | ||
|
|
4e67f87479 | ||
|
|
8118744f2a | ||
|
|
412cddf6be | ||
|
|
399cf1aa5d | ||
|
|
fe93ffcd65 | ||
|
|
a27095f0e1 | ||
|
|
14ab548efc | ||
|
|
7bebcee20f | ||
|
|
e9eda5d5df | ||
|
|
04e4ee4ece | ||
|
|
aae9534a6b | ||
|
|
d0f2444aa5 | ||
|
|
e6cb8788f8 | ||
|
|
11e3e2390a | ||
|
|
d0c0ca2d26 | ||
|
|
4ef775fa97 | ||
|
|
8b042907e5 | ||
|
|
b88080bd49 | ||
|
|
30751ca864 | ||
|
|
0fdd7cd6a2 | ||
|
|
fdc58b7cce | ||
|
|
85eedb242e | ||
|
|
806dff20d9 | ||
|
|
8f63c498e9 | ||
|
|
df2833b318 | ||
|
|
7214372a8a | ||
|
|
497357800a | ||
|
|
fa084c4ae3 | ||
|
|
4148419736 | ||
|
|
66712a90b3 | ||
|
|
b780749ffb | ||
|
|
d25c5e4b11 | ||
|
|
1ddfe17d65 | ||
|
|
6e6b4135ab | ||
|
|
c2b8e4532b | ||
|
|
7b66c0665f | ||
|
|
49f8dd1ca4 | ||
|
|
6fad1d7d74 | ||
|
|
b8562dbf4a | ||
|
|
d66846351d | ||
|
|
bad790dd2e | ||
|
|
c67d1935d8 | ||
|
|
1d6770edbc | ||
|
|
135f2eca01 | ||
|
|
6bfb487155 | ||
|
|
b1d68089e5 | ||
|
|
a850e5d57e | ||
|
|
6e3f17cea2 | ||
|
|
4a55da168c | ||
|
|
7639ce4607 | ||
|
|
0d31a20845 | ||
|
|
a5a7500707 | ||
|
|
24b44290ab | ||
|
|
0d0399bb14 | ||
|
|
c1d7b5c6f9 | ||
|
|
24e3d4d83c | ||
|
|
9df00b6dc3 | ||
|
|
3cd70a3761 | ||
|
|
981ba2a7b1 | ||
|
|
5de07c13c1 | ||
|
|
a2ce82b35e | ||
|
|
5e88945a3c | ||
|
|
fb7daf02f6 | ||
|
|
c5b7ea0bd2 | ||
|
|
0b08fa4285 | ||
|
|
cb44fc5f1b | ||
|
|
eb3796a8c5 | ||
|
|
571f87dd12 | ||
|
|
ea866650be | ||
|
|
c7c0109944 | ||
|
|
ac282725cd | ||
|
|
6777170037 | ||
|
|
b3107260a2 | ||
|
|
7db73ad9a1 | ||
|
|
3691b719fa | ||
|
|
fe946581ba | ||
|
|
4634b0ada7 | ||
|
|
83a24c2117 | ||
|
|
39aa750b1c | ||
|
|
c73e0063b7 | ||
|
|
09656e19c1 | ||
|
|
5972ebdaf8 | ||
|
|
489f3e4521 | ||
|
|
ebfb3c0056 | ||
|
|
e16a001d39 | ||
|
|
5cf7c50524 | ||
|
|
ad3bfab05d | ||
|
|
e8d10dc2ad | ||
|
|
ad14569561 | ||
|
|
e21cdabd71 | ||
|
|
c0219caf15 | ||
|
|
68df17ae00 | ||
|
|
3748c3288d | ||
|
|
907505c004 | ||
|
|
8bbea98392 | ||
|
|
f2eef1f0d2 | ||
|
|
d395d8df06 | ||
|
|
14d6c29d81 | ||
|
|
34f40b9ea7 | ||
|
|
5b36673d0a | ||
|
|
1687230672 | ||
|
|
f196279c29 | ||
|
|
ed11852ed0 | ||
|
|
9f0afbea4f | ||
|
|
034a4f3b1e | ||
|
|
4f5a5857ce | ||
|
|
bc63ea82f2 | ||
|
|
0d4fb1bc89 | ||
|
|
52480aaac2 | ||
|
|
702a8f61b7 | ||
|
|
2928b2742e | ||
|
|
f1d5a3c815 | ||
|
|
3086dbc0fd | ||
|
|
63e599b921 | ||
|
|
340238421d | ||
|
|
31fbb17d23 | ||
|
|
edf298ace5 | ||
|
|
1d5b5ab83a | ||
|
|
b0ba699031 | ||
|
|
e57c7f5d87 | ||
|
|
05f30c05c9 | ||
|
|
922681e140 | ||
|
|
273b9f265f | ||
|
|
0d546ee3b4 | ||
|
|
d6124a82a4 | ||
|
|
1d828f35ca | ||
|
|
7bba25e675 | ||
|
|
6e3140d5ba | ||
|
|
1d8f47cc71 | ||
|
|
743d715d43 | ||
|
|
4b862306e5 | ||
|
|
70948a9dc3 | ||
|
|
951bf0a446 | ||
|
|
bea8a3a16f | ||
|
|
bf8aab51e8 | ||
|
|
1f689ec0c2 | ||
|
|
8839314025 | ||
|
|
47e087d8eb | ||
|
|
e1c5f0e93a | ||
|
|
cfdc27c613 | ||
|
|
7e7372a0c5 | ||
|
|
8cad2097e9 | ||
|
|
d039cfa829 | ||
|
|
0af88421dc | ||
|
|
9d014670df | ||
|
|
647a00a278 |
No files matched your search
@@ -1 +1,17 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
# The rsync-web/ subdirectory holds the project website source content
|
||||
# (mirrors what gets pushed to https://rsync.samba.org). Exclude it from
|
||||
# `git archive` output so the release source tarball produced by
|
||||
# packaging/release.py step_7_tarball does not bloat with HTML the
|
||||
# tarball doesn't need.
|
||||
/rsync-web/ export-ignore
|
||||
|
||||
# old_versions/ holds static binaries of historical rsync releases, used by the
|
||||
# version-mixing test suite (.github/workflows/ubuntu-version-mix.yml) to run
|
||||
# the current code against a real old peer over the daemon / remote-shell.
|
||||
# Mark the binaries as binary so the `text=auto eol=lf` rule above can't try to
|
||||
# normalise line endings and corrupt them; export-ignore keeps them out of the
|
||||
# release source tarball.
|
||||
/old_versions/rsync_* binary
|
||||
/old_versions/rsync_* export-ignore
|
||||
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: RsyncProject
|
||||
patreon: AndrewTridgell
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Lint GitHub Actions workflows
|
||||
|
||||
# Static-check the workflow YAML with rhysd/actionlint. Catches missing
|
||||
# secrets, bad expressions, expression-type errors, unsupported runner
|
||||
# images, and (via embedded shellcheck) common pitfalls in `run:` scripts.
|
||||
# Trigger only on changes under .github/workflows/ so the rest of the
|
||||
# matrix isn't billed when nothing here moves.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/*.yml'
|
||||
- '.github/actionlint.yaml'
|
||||
- '.github/actionlint.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths:
|
||||
- '.github/workflows/*.yml'
|
||||
- '.github/actionlint.yaml'
|
||||
- '.github/actionlint.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
actionlint:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: actionlint
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install actionlint
|
||||
# Pin a version so this job is reproducible; bump deliberately.
|
||||
# The download script verifies a SHA256 of the release tarball.
|
||||
run: |
|
||||
bash <(curl --proto '=https' --tlsv1.2 -fsSL \
|
||||
https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) \
|
||||
1.7.12
|
||||
echo "$PWD" >>"$GITHUB_PATH"
|
||||
- name: actionlint --version
|
||||
run: actionlint -version
|
||||
- name: actionlint .github/workflows/*.yml
|
||||
run: actionlint -color
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/almalinux-8-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/almalinux-8-build.yml'
|
||||
@@ -22,6 +22,9 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: almalinux:8
|
||||
@@ -59,13 +62,19 @@ jobs:
|
||||
run: ./rsync --version
|
||||
- name: check
|
||||
# In the container we already run as root, so no sudo. The
|
||||
# crtimes-not-supported skip matches the other Linux jobs.
|
||||
run: RSYNC_EXPECT_SKIPPED=crtimes make check
|
||||
# crtimes-not-supported skip matches the other Linux jobs;
|
||||
# daemon-chroot-acl and proxy-response-line-too-long skip because
|
||||
# the default (secure) transport opens no listening socket.
|
||||
run: RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run exercising the real loopback-TCP daemon path.
|
||||
run: ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
|
||||
- name: ssl file list
|
||||
run: ./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: almalinux-8-bin
|
||||
path: |
|
||||
rsync
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
name: Build static rsync for Android
|
||||
|
||||
# Cross-compiles statically-linked rsync binaries with the Android NDK,
|
||||
# suitable for dropping onto a phone (adb push / Termux) with no shared
|
||||
# libraries. arm64-v8a covers all modern phones; armeabi-v7a covers older
|
||||
# 32-bit devices. The binaries are uploaded as workflow artifacts.
|
||||
#
|
||||
# These are cross-compiled, so the test suite can't run here; we sanity
|
||||
# check that each binary is the right architecture, is static, and that
|
||||
# it executes (`--version`) under qemu-user.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/android-static-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/android-static-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Minimum supported API level. 24 (Android 7.0) runs on every modern
|
||||
# phone while keeping broad reach; bump if you need newer Bionic APIs.
|
||||
ANDROID_API: 24
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ matrix.abi }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- abi: arm64-v8a # modern phones
|
||||
triple: aarch64-linux-android
|
||||
qemu: qemu-aarch64-static
|
||||
- abi: armeabi-v7a # older 32-bit phones
|
||||
triple: armv7a-linux-androideabi
|
||||
qemu: qemu-arm-static
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install build prerequisites
|
||||
run: sudo apt-get update && sudo apt-get install -y autoconf automake gawk qemu-user-static
|
||||
|
||||
- name: Configure and build (${{ matrix.abi }})
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NDK="${ANDROID_NDK_LATEST_HOME:-$ANDROID_NDK_ROOT}"
|
||||
TC="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
export CC="$TC/${{ matrix.triple }}${ANDROID_API}-clang"
|
||||
export AR="$TC/llvm-ar" RANLIB="$TC/llvm-ranlib" STRIP="$TC/llvm-strip"
|
||||
export CFLAGS="-O2" LDFLAGS="-static"
|
||||
|
||||
# Bionic doesn't declare lchmod()/lutimes() until API 36, but the
|
||||
# symbols link, so configure mis-detects them -- force them off so
|
||||
# rsync uses its fallbacks. The other cache vars restore values
|
||||
# that configure can't probe when cross-compiling (Android runs a
|
||||
# normal Linux kernel, so these match the native Linux result).
|
||||
export ac_cv_func_lchmod=no ac_cv_func_lutimes=no \
|
||||
rsync_cv_HAVE_SOCKETPAIR=yes \
|
||||
rsync_cv_MKNOD_CREATES_FIFOS=yes \
|
||||
rsync_cv_MKNOD_CREATES_SOCKETS=yes
|
||||
|
||||
# Self-contained build: drop optional external libraries so the
|
||||
# static binary needs nothing at runtime. rsync keeps md5/md4
|
||||
# checksums and its bundled zlib.
|
||||
./configure --host=${{ matrix.triple }} --build=x86_64-pc-linux-gnu \
|
||||
--enable-ipv6 \
|
||||
--disable-zstd --disable-lz4 --disable-xxhash --disable-openssl \
|
||||
--disable-iconv --disable-iconv-open \
|
||||
--disable-acl-support --disable-xattr-support \
|
||||
--disable-md2man --disable-roll-simd \
|
||||
--with-included-popt --with-included-zlib
|
||||
|
||||
# Generate the awk-built headers serially first so the parallel
|
||||
# build can't race on proto.h <- daemon-parm.h.
|
||||
make proto.h
|
||||
make -j"$(nproc)" rsync
|
||||
"$STRIP" rsync
|
||||
|
||||
- name: Verify binary
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
file rsync
|
||||
# Gate: must be a statically-linked executable (no interpreter).
|
||||
file rsync | grep -q "statically linked"
|
||||
if file rsync | grep -q "dynamically linked"; then
|
||||
echo "ERROR: binary is not static" >&2; exit 1
|
||||
fi
|
||||
# Best-effort: confirm it actually runs under qemu-user.
|
||||
${{ matrix.qemu }} ./rsync --version | head -3 || \
|
||||
echo "WARNING: qemu smoke test did not run cleanly (check on a real device)"
|
||||
|
||||
- name: Package
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER=$(sed -n 's/.*RSYNC_VERSION "\([^"]*\)".*/\1/p' version.h)
|
||||
out="rsync-${VER}-android-${{ matrix.abi }}"
|
||||
mkdir -p dist
|
||||
cp rsync "dist/$out"
|
||||
( cd dist && sha256sum "$out" > "$out.sha256" )
|
||||
echo "ARTIFACT_NAME=rsync-android-${{ matrix.abi }}" >>"$GITHUB_ENV"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: dist/
|
||||
@@ -0,0 +1,75 @@
|
||||
name: rsync ASan+UBSan (clang)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/asan-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/asan-build.yml'
|
||||
schedule:
|
||||
# Weekly (Mon 09:42 UTC): catch breakage from a moving ubuntu-latest/clang
|
||||
# toolchain (a new clang can add a UBSan check, or change ASan behaviour)
|
||||
# that no code push would otherwise trigger. Push/PR already gate every
|
||||
# code change, so daily would just re-run an unchanged tree.
|
||||
- cron: '42 9 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
asan:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: rsync ASan+UBSan (clang)
|
||||
env:
|
||||
# rsync intentionally leaks small allocations at process exit, so leak
|
||||
# detection would be all noise; chase only memory-safety errors.
|
||||
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
|
||||
# UBSan is a gate: -fno-sanitize-recover=undefined (below) aborts on the
|
||||
# first finding and halt_on_error=1 makes that fatal, so any undefined
|
||||
# behaviour fails the run. This needs the tree to be UBSan-clean: the
|
||||
# remaining findings are fixed in code (hashtable/mdfour shifts, xattrs,
|
||||
# and log.c's file_struct, kept aligned via rounding.h); only byteorder.h's
|
||||
# intentional unaligned accessors are suppressed, with no_sanitize.
|
||||
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
# -DNDEBUG builds as a shipped release does (assert() compiled out), so
|
||||
# AddressSanitizer catches the over-reads/over-writes that an "assert()
|
||||
# instead of a real bounds check" bug would cause in a production build.
|
||||
# UBSan rides along on the same build; -fno-sanitize-recover=undefined
|
||||
# makes any undefined behaviour abort (and thus fail the run) instead of
|
||||
# merely printing it.
|
||||
run: |
|
||||
CC=clang \
|
||||
CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer -g -O1 -DNDEBUG" \
|
||||
LDFLAGS="-fsanitize=address,undefined" \
|
||||
./configure --with-rrsync --disable-md2man
|
||||
- name: make
|
||||
# check-progs builds rsync plus the test helper programs (tls, trimslash,
|
||||
# t_unsafe, ...) that runtests.py requires; plain "make" builds only rsync
|
||||
# and runtests aborts on the missing helpers.
|
||||
run: make check-progs
|
||||
- name: info
|
||||
run: ./rsync --version
|
||||
- name: check (stdio-pipe transport)
|
||||
# ASan+UBSan-instrumented coverage of the transfer, daemon, sender,
|
||||
# receiver and metadata paths over the default stdio-pipe transport.
|
||||
run: ./runtests.py --rsync-bin="$PWD/rsync" -j8
|
||||
- name: check (TCP daemon transport)
|
||||
# --use-tcp also exercises the loopback rsyncd listener and the client's
|
||||
# TCP connection path.
|
||||
run: ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j8
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Coverage (Ubuntu)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/coverage.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/coverage.yml'
|
||||
schedule:
|
||||
- cron: '42 9 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: gcov coverage
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl gcovr
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --enable-coverage --with-rrsync
|
||||
- name: make
|
||||
run: make
|
||||
- name: info
|
||||
run: rsync --version
|
||||
# Two coverage runs: the default pipe transport, then a second pass over a
|
||||
# real loopback rsyncd (--use-tcp) which also exercises the require_tcp-only
|
||||
# tests. gcovr's --print-summary line/branch/decision totals go to the step
|
||||
# log (and the job summary below), so the numbers are visible in CI.
|
||||
# `make coverage` exits with the suite's status, so a regression fails CI.
|
||||
- name: coverage (pipe transport)
|
||||
run: |
|
||||
set -o pipefail
|
||||
sudo make coverage 2>&1 | tee cov-pipe.log
|
||||
- name: coverage (TCP transport)
|
||||
run: |
|
||||
set -o pipefail
|
||||
sudo make coverage-tcp 2>&1 | tee cov-tcp.log
|
||||
- name: coverage summary
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo "## gcov coverage"
|
||||
echo "### Pipe transport (\`make coverage\`)"
|
||||
echo '```'
|
||||
grep -E '^(lines|functions|branches|decisions):' cov-pipe.log || echo '(no summary -- see step log)'
|
||||
echo '```'
|
||||
echo "### TCP transport (\`make coverage-tcp\`)"
|
||||
echo '```'
|
||||
grep -E '^(lines|functions|branches|decisions):' cov-tcp.log || echo '(no summary -- see step log)'
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: upload HTML reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: coverage-html
|
||||
path: |
|
||||
coverage
|
||||
coverage-tcp
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/cygwin-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/cygwin-build.yml'
|
||||
@@ -16,6 +16,9 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: windows-2022
|
||||
name: Test rsync on Cygwin
|
||||
steps:
|
||||
@@ -39,12 +42,22 @@ jobs:
|
||||
- name: info
|
||||
run: bash -c '/usr/local/bin/rsync --version'
|
||||
- name: check
|
||||
run: bash -c 'RSYNC_EXPECT_SKIPPED=acls-default,acls,bare-do-open-symlink-race,chdir-symlink-race,chmod-symlink-race,chown,daemon-chroot-acl,devices,dir-sgid,open-noatime,protected-regular,sender-flist-symlink-leak,simd-checksum,symlink-dirlink-basis make check'
|
||||
# chown-fake / devices-fake / xattrs / xattrs-hlink now RUN on Cygwin
|
||||
# (rsyncfns.py drives xattrs via getfattr/setfattr from the `attr`
|
||||
# package installed above), verified on a real Cygwin host. The real
|
||||
# chown/devices tests still skip (need root/mknod), as do the
|
||||
# RESOLVE_BENEATH symlink-race tests.
|
||||
run: bash -c 'RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/cygwin.txt make check'
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run with daemon tests over a real loopback rsyncd; the default
|
||||
# 'make check' above uses the secure stdio-pipe transport.
|
||||
run: bash -c './runtests.py --rsync-bin=`pwd`/rsync.exe --use-tcp -j 8'
|
||||
- name: ssl file list
|
||||
run: bash -c 'PATH="/usr/local/bin:$PATH" rsync-ssl --no-motd download.samba.org::rsyncftp/ || true'
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: cygwin-bin
|
||||
path: |
|
||||
rsync.exe
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
name: Test fleettest harness
|
||||
|
||||
# Bitrot check for testsuite/fleettest.py (the developer fleet CI harness).
|
||||
# fleettest is meant to be run by developers on a modern Ubuntu box, so this
|
||||
# job runs only on ubuntu-latest: it stands up a one-host "fleet" of two
|
||||
# targets that both ssh to localhost and runs a real fleettest pass against it.
|
||||
# It does not run on the BSD/Solaris/macOS/Cygwin matrix.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'testsuite/fleettest.py'
|
||||
- '.github/workflows/fleettest.yml'
|
||||
- 'runtests.py'
|
||||
- 'testsuite/skiplist/**'
|
||||
- 'testsuite/skiplist-spec_test.py'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths:
|
||||
- 'testsuite/fleettest.py'
|
||||
- '.github/workflows/fleettest.yml'
|
||||
- 'runtests.py'
|
||||
- 'testsuite/skiplist/**'
|
||||
- 'testsuite/skiplist-spec_test.py'
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '17 7 * * 1'
|
||||
|
||||
jobs:
|
||||
fleettest:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: fleettest against localhost
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc g++ gawk autoconf automake \
|
||||
acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev \
|
||||
python3-cmarkgfm openssl rsync openssh-server
|
||||
- name: set up ssh to localhost
|
||||
run: |
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
|
||||
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
sudo systemctl start ssh || sudo service ssh start
|
||||
# fleettest connects with `ssh -o BatchMode=yes localhost`, which won't
|
||||
# answer a host-key prompt -- so pre-trust localhost in known_hosts.
|
||||
ssh-keyscan -H localhost 127.0.0.1 >> ~/.ssh/known_hosts 2>/dev/null
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=15 localhost 'echo ssh-to-localhost-ok'
|
||||
- name: write localhost fleet config
|
||||
run: |
|
||||
cat > fleettest-ci.json <<'EOF'
|
||||
{ "targets": [
|
||||
{ "name": "local-a", "ssh_host": "localhost", "workflow": "none.yml",
|
||||
"configure_flags": [], "builddir": "rsync-citest-a", "privilege": "sudo" },
|
||||
{ "name": "local-b", "ssh_host": "localhost", "workflow": "none.yml",
|
||||
"configure_flags": [], "builddir": "rsync-citest-b", "privilege": "sudo" }
|
||||
] }
|
||||
EOF
|
||||
- name: fleettest --list (config sanity)
|
||||
run: python3 testsuite/fleettest.py --fleet fleettest-ci.json --list
|
||||
- name: run fleettest against localhost
|
||||
# Two targets both on localhost exercise the parallel multi-target path
|
||||
# and the per-run dir / port isolation; exit 0 iff every cell is OK.
|
||||
run: python3 testsuite/fleettest.py --fleet fleettest-ci.json --timing
|
||||
@@ -7,15 +7,18 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/freebsd-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/freebsd-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * *'
|
||||
- cron: '42 8 * * 1'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: Test rsync on FreeBSD
|
||||
steps:
|
||||
@@ -35,10 +38,12 @@ jobs:
|
||||
make
|
||||
./rsync --version
|
||||
make check
|
||||
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
|
||||
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: freebsd-bin
|
||||
path: |
|
||||
rsync
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/macos-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/macos-build.yml'
|
||||
@@ -16,6 +16,9 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: macos-latest
|
||||
name: Test rsync on macOS
|
||||
steps:
|
||||
@@ -26,7 +29,7 @@ jobs:
|
||||
run: |
|
||||
brew install automake openssl xxhash zstd lz4
|
||||
pip3 install --user --break-system-packages commonmark
|
||||
echo "$(brew --prefix)/bin" >>$GITHUB_PATH
|
||||
echo "$(brew --prefix)/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: |
|
||||
BREW_PREFIX=$(brew --prefix)
|
||||
@@ -41,12 +44,20 @@ jobs:
|
||||
- name: info
|
||||
run: rsync --version
|
||||
- name: check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=acls-default,chmod-temp-dir,chown-fake,daemon-chroot-acl,devices-fake,dir-sgid,open-noatime,protected-regular,simd-checksum,xattrs-hlink,xattrs make check
|
||||
# chown-fake / devices-fake / xattrs / xattrs-hlink now RUN on macOS
|
||||
# (rsyncfns.py drives xattrs via the `xattr` command), verified on a
|
||||
# real macOS host, so they're no longer in the skip set.
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/macos.txt make check
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run with daemon tests over a real loopback rsyncd; the default
|
||||
# 'make check' above uses the secure stdio-pipe transport.
|
||||
run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
|
||||
- name: ssl file list
|
||||
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: macos-bin
|
||||
path: |
|
||||
rsync
|
||||
|
||||
@@ -7,15 +7,18 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/netbsd-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/netbsd-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * *'
|
||||
- cron: '42 8 * * 1'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: Test rsync on NetBSD
|
||||
steps:
|
||||
@@ -36,10 +39,12 @@ jobs:
|
||||
make
|
||||
./rsync --version
|
||||
make check
|
||||
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
|
||||
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: netbsd-bin
|
||||
path: |
|
||||
rsync
|
||||
|
||||
@@ -7,15 +7,18 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/openbsd-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/openbsd-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * *'
|
||||
- cron: '42 8 * * 1'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: Test rsync on OpenBSD
|
||||
steps:
|
||||
@@ -36,11 +39,28 @@ jobs:
|
||||
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4
|
||||
make
|
||||
./rsync --version
|
||||
# The flipper (symlink-race) tests are excluded on OpenBSD, as on the
|
||||
# fleet's OpenBSD box: this kernel has a connect()-under-rename-load
|
||||
# lost-wakeup and an FFS rename-storm corruption that hang them to
|
||||
# the 300s timeout for non-rsync reasons (see
|
||||
# dev-notes/openbsd-connect-lost-wakeup-report.txt); the protections
|
||||
# they exercise are verified on the Linux/BSD boxes.
|
||||
export RSYNC_EXCLUDE=acl-symlink-race,sender-readlink-atfd,sender-remove-source-secure
|
||||
make check
|
||||
# The --use-tcp daemon tests run at -j2 here (vs -j8 elsewhere): this
|
||||
# job runs inside a nested VM, and at -j8 the many concurrent loopback
|
||||
# daemons occasionally lose a connection-handshake timing race under
|
||||
# that resource pressure, hanging one test to the 300s timeout. It is
|
||||
# an environment artifact, not an rsync bug (the handshake is
|
||||
# deadlock-free and unreproducible elsewhere, even pinned to 1 CPU at
|
||||
# -j8); -j2 keeps the VM from over-subscribing. The pipe `make check`
|
||||
# above stays at the default parallelism.
|
||||
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 2
|
||||
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: openbsd-bin
|
||||
path: |
|
||||
rsync
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
name: rsync scan-build (clang analyzer)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/scan-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/scan-build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# GATING run: pinned clang-18 on a pinned runner so the checker set -- and
|
||||
# thus the expected zero -- is deterministic. The tree is kept clean for
|
||||
# clang-18, so --status-bugs (non-zero exit on any report) fails the build
|
||||
# when a new finding appears. Pin both the analyzer (clang-18/clang-tools-18)
|
||||
# and the runner (ubuntu-24.04, whose apt repos carry those packages).
|
||||
gate-clang18:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-24.04
|
||||
name: scan-build gate (clang-18, pinned)
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-18 clang-tools-18 acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libpopt-dev openssl
|
||||
- name: configure (under scan-build)
|
||||
# Run configure under scan-build so its analyzer compiler-wrapper is baked
|
||||
# into the Makefile's $(CC); --disable-md2man avoids the doc toolchain.
|
||||
run: scan-build-18 ./configure --with-rrsync --disable-md2man
|
||||
- name: scan-build (gating)
|
||||
# --status-bugs makes scan-build exit non-zero if it finds ANY report.
|
||||
# pipefail + 'exit $status' propagate that through the tee so the job goes
|
||||
# red while still printing the summary; the report uploads for triage.
|
||||
run: |
|
||||
set -o pipefail
|
||||
status=0
|
||||
scan-build-18 --status-bugs -o "$PWD/scan-report" make check-progs -j"$(nproc)" 2>&1 | tee scan-build.out || status=$?
|
||||
echo '## scan-build gate (clang-18)' >>"$GITHUB_STEP_SUMMARY"
|
||||
grep -E 'scan-build: .* bugs? found|scan-build: No bugs found' scan-build.out >>"$GITHUB_STEP_SUMMARY" || true
|
||||
exit $status
|
||||
- name: upload report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: scan-build-report-clang18
|
||||
path: scan-report
|
||||
if-no-files-found: ignore
|
||||
|
||||
# INFORMATIONAL run: whatever clang ubuntu-latest currently ships. Newer
|
||||
# clang releases enable extra, FP-heavy checkers (e.g. unix.Chroot
|
||||
# "no chdir after chroot", alpha.unix.Stream) that the gate deliberately
|
||||
# avoids, so this is NOT a gate (no --status-bugs). It surfaces what the
|
||||
# newest analyzer sees -- useful for spotting genuine new findings before a
|
||||
# gate bump -- without blocking merges. continue-on-error keeps a noisy or
|
||||
# broken run from affecting the workflow's required status.
|
||||
informational-latest:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: scan-build (latest clang, informational)
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang clang-tools acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libpopt-dev openssl
|
||||
- name: configure (under scan-build)
|
||||
run: scan-build ./configure --with-rrsync --disable-md2man
|
||||
- name: scan-build (informational)
|
||||
run: |
|
||||
scan-build -o "$PWD/scan-report" make check-progs -j"$(nproc)" 2>&1 | tee scan-build.out
|
||||
echo '## scan-build informational (latest clang)' >>"$GITHUB_STEP_SUMMARY"
|
||||
grep -E 'scan-build: .* bugs? found|scan-build: No bugs found' scan-build.out >>"$GITHUB_STEP_SUMMARY" || true
|
||||
- name: upload report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: scan-build-report-latest
|
||||
path: scan-report
|
||||
if-no-files-found: ignore
|
||||
@@ -7,15 +7,18 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/solaris-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/solaris-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * *'
|
||||
- cron: '42 8 * * 1'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: Test rsync on Solaris
|
||||
steps:
|
||||
@@ -35,10 +38,12 @@ jobs:
|
||||
make
|
||||
./rsync --version
|
||||
make check
|
||||
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
|
||||
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: solaris-bin
|
||||
path: |
|
||||
rsync
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-22.04-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-22.04-build.yml'
|
||||
@@ -20,6 +20,9 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-22.04
|
||||
name: Test rsync on Ubuntu 22.04
|
||||
steps:
|
||||
@@ -29,7 +32,7 @@ jobs:
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
|
||||
echo "/usr/local/bin" >>$GITHUB_PATH
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --with-rrsync
|
||||
- name: make
|
||||
@@ -39,16 +42,21 @@ jobs:
|
||||
- name: info
|
||||
run: rsync --version
|
||||
- name: check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check
|
||||
- name: check30
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check30
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check30
|
||||
- name: check29
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check29
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt make check29
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run with daemon tests over a real loopback rsyncd; the default
|
||||
# 'make check' above uses the secure stdio-pipe transport.
|
||||
run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
|
||||
- name: ssl file list
|
||||
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: ubuntu-22.04-bin
|
||||
path: |
|
||||
rsync
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-build.yml'
|
||||
@@ -16,6 +16,9 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: Test rsync on Ubuntu
|
||||
steps:
|
||||
@@ -25,7 +28,7 @@ jobs:
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
|
||||
echo "/usr/local/bin" >>$GITHUB_PATH
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --with-rrsync
|
||||
- name: make
|
||||
@@ -35,16 +38,39 @@ jobs:
|
||||
- name: info
|
||||
run: rsync --version
|
||||
- name: check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check
|
||||
- name: check30
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check30
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check30
|
||||
- name: check29
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check29
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt make check29
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run with daemon tests over a real loopback rsyncd. The default
|
||||
# 'make check' above uses the secure stdio-pipe transport (no listening
|
||||
# sockets); this run exercises the real TCP accept/auth path. Skip-set
|
||||
# is env-dependent here (chroot-acl), so leave RSYNC_EXPECT_SKIPPED unset.
|
||||
run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
|
||||
- name: check (non-root, targeted)
|
||||
# Every run above is root (sudo), so privilege-sensitive tests never hit
|
||||
# their non-root path. Run those here as the unprivileged 'runner' user
|
||||
# (NO sudo). Explicit test names make runtests.py full_run False, so
|
||||
# RSYNC_EXPECT_SKIPPED is bypassed -- no per-platform skip list needed.
|
||||
# daemon-namecvt-empty-response -- REQUIRES non-root (skips as root by
|
||||
# design); the only test with no other CI coverage (Benjamin #2).
|
||||
# ownership-depth -- non-root takes the group-only remap path.
|
||||
# daemon -- non-root takes the default-config path.
|
||||
# CONVENTION: a new test that requires/meaningfully exercises a non-root
|
||||
# path must be added to the list below (kept in sync with the fleet
|
||||
# harness's nonroot_tests).
|
||||
run: |
|
||||
sudo rm -rf testtmp # prior root steps left it root-owned
|
||||
./runtests.py --rsync-bin="$PWD/rsync" \
|
||||
daemon-namecvt-empty-response ownership-depth daemon
|
||||
- name: ssl file list
|
||||
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
|
||||
- name: save artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 45
|
||||
name: ubuntu-bin
|
||||
path: |
|
||||
rsync
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
name: Test rsync version mixing on Ubuntu
|
||||
|
||||
# Runs the CURRENT test suite with two different rsync binaries: the freshly
|
||||
# built ./rsync as the client/driver, and a committed OLD static binary
|
||||
# (old_versions/rsync_<ver>) as the daemon / remote-shell peer. This exercises
|
||||
# real version mixing over the wire -- more convincing than --protocol forcing,
|
||||
# which only makes the current binary speak an old protocol.
|
||||
#
|
||||
# Direction is fixed: the current binary always drives (only it understands the
|
||||
# new test scripts); the old binary is only ever the server/daemon side. The
|
||||
# reverse (old client driving new scripts) is not possible -- but one test,
|
||||
# reverse-daemon-delta, swaps the roles internally (current build as the daemon,
|
||||
# old binary as the client) to cover the backward-compat direction: a current
|
||||
# daemon serving the installed base of old clients.
|
||||
#
|
||||
# The per-version manifest testsuite/expect/rsync_<ver>.expect lists exactly
|
||||
# which tests run and each one's expected outcome (pass/skip/fail/xfail), so an
|
||||
# old peer's known feature gaps are recorded rather than treated as breakage.
|
||||
#
|
||||
# All peers run in a SINGLE job (looped, not a matrix) so the PR shows one check
|
||||
# line rather than one per version. Each peer/transport is a foldable ::group::
|
||||
# in the log, and a failure annotates which peer/transport broke.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-version-mix.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-version-mix.yml'
|
||||
schedule:
|
||||
- cron: '52 8 * * 1'
|
||||
|
||||
jobs:
|
||||
version-mix:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
name: rsync version-mix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --with-rrsync
|
||||
- name: make
|
||||
# check-progs builds rsync AND the test helper programs (tls, trimslash,
|
||||
# t_unsafe, ...) that runtests.py requires; plain `make` does not.
|
||||
run: make check-progs
|
||||
- name: info
|
||||
run: ./rsync --version | head -1
|
||||
- name: version mixing (all peers, pipe + TCP transports)
|
||||
run: |
|
||||
rc=0
|
||||
for peer in old_versions/rsync_*; do
|
||||
chmod +x "$peer"
|
||||
name=$(basename "$peer")
|
||||
expect="testsuite/expect/$name.expect"
|
||||
for transport in pipe tcp; do
|
||||
tcp=()
|
||||
[ "$transport" = tcp ] && tcp=(--use-tcp)
|
||||
echo "::group::$name ($transport): $("$peer" --version | head -1)"
|
||||
if ! ./runtests.py --rsync-bin="$PWD/rsync" --rsync-bin2="$PWD/$peer" \
|
||||
--expect-result "$expect" "${tcp[@]}" -j 8; then
|
||||
echo "::error::version-mix failed: $name ($transport)"
|
||||
rc=1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
done
|
||||
exit $rc
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Valgrind memcheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/valgrind.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/valgrind.yml'
|
||||
schedule:
|
||||
- cron: '17 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
memcheck:
|
||||
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
|
||||
# minutes; labels need triage access, so fork PRs can't self-enable.
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
privilege: [ user, root ]
|
||||
transport: [ pipe, tcp ]
|
||||
name: memcheck (${{ matrix.privilege }}, ${{ matrix.transport }})
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y valgrind acl libacl1-dev attr libattr1-dev \
|
||||
liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --with-rrsync --enable-debug
|
||||
- name: make
|
||||
run: make check-progs # builds rsync + the test helper programs runtests.py needs
|
||||
- name: info
|
||||
run: ./rsync --version
|
||||
|
||||
# Run the whole suite under valgrind. We gate on memory *errors* (uninit
|
||||
# reads, invalid read/write, bad frees, uninit syscall params), not leaks:
|
||||
# rsync deliberately leaves file-list/socket/option memory unfreed at exit
|
||||
# (short-lived process; the OS reclaims), so --leak-check=no avoids a sea of
|
||||
# by-design "definitely lost" reports. Functional pass/fail is covered by
|
||||
# the other workflows, so the suite is allowed to finish regardless of
|
||||
# per-test results; the scan step below is the gate. --error-exitcode=0
|
||||
# keeps valgrind from perturbing test exit codes; the bundled
|
||||
# testsuite/valgrind.supp silences known-benign reports.
|
||||
- name: run testsuite under valgrind
|
||||
run: |
|
||||
SUDO=
|
||||
[ "${{ matrix.privilege }}" = root ] && SUDO="sudo -E"
|
||||
TCP=
|
||||
[ "${{ matrix.transport }}" = tcp ] && TCP="--use-tcp"
|
||||
$SUDO ./runtests.py --valgrind \
|
||||
--valgrind-opts="--leak-check=no --error-exitcode=0" \
|
||||
$TCP -j8 --preserve-scratch || true
|
||||
|
||||
- name: scan for unsuppressed valgrind errors
|
||||
run: |
|
||||
sudo chown -R "$USER" testtmp 2>/dev/null || true
|
||||
mapfile -t logs < <(find testtmp -name 'valgrind.*.log' 2>/dev/null)
|
||||
if [ "${#logs[@]}" -eq 0 ]; then
|
||||
echo "::error::no valgrind logs were produced -- the suite did not run"
|
||||
exit 1
|
||||
fi
|
||||
echo "scanned ${#logs[@]} valgrind log(s)"
|
||||
bad=()
|
||||
for f in "${logs[@]}"; do
|
||||
grep -qE 'ERROR SUMMARY: [1-9][0-9]* errors' "$f" && bad+=("$f")
|
||||
done
|
||||
if [ "${#bad[@]}" -ne 0 ]; then
|
||||
echo "::error::valgrind reported unsuppressed errors in ${#bad[@]} run(s)"
|
||||
for f in "${bad[@]}"; do
|
||||
echo "===== $f ====="
|
||||
sed 's/==[0-9]*== //' "$f" | grep -A18 \
|
||||
-E 'depends on uninitialised|points to uninitialised|Invalid (read|write|free)|lost in loss record|Mismatched free' \
|
||||
| head -60
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
echo "valgrind clean: no unsuppressed errors"
|
||||
|
||||
- name: upload valgrind logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: valgrind-logs-${{ matrix.privilege }}-${{ matrix.transport }}
|
||||
path: testtmp/**/valgrind.*.log
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
+13
@@ -45,6 +45,17 @@ aclocal.m4
|
||||
/t_unsafe
|
||||
/wildtest
|
||||
/getfsdev
|
||||
/simdtest
|
||||
/t_acl
|
||||
/t_chmod_secure
|
||||
/t_clean_fname
|
||||
/t_hashtable_overflow
|
||||
/t_iwildmatch
|
||||
/t_rename_secure
|
||||
/t_safe_arg
|
||||
/t_safe_arg_main
|
||||
/t_secure_relpath
|
||||
/t_symlink_secure
|
||||
/rounding.h
|
||||
/doc/rsync.pdf
|
||||
/doc/rsync.ps
|
||||
@@ -52,6 +63,8 @@ aclocal.m4
|
||||
/testsuite/chown-fake.test
|
||||
/testsuite/devices-fake.test
|
||||
/testsuite/xattrs-hlink.test
|
||||
/testsuite/fleettest.json
|
||||
/fleettest-logs
|
||||
/patches
|
||||
/patches.gen
|
||||
/build
|
||||
|
||||
+28
@@ -7,6 +7,34 @@ option to use if you want to just skip that feature. What follows are various
|
||||
support libraries that you may want to install to build rsync with the maximum
|
||||
features (the impatient can skip down to the package summary):
|
||||
|
||||
## Ubuntu users: skip the build, use the PPA
|
||||
|
||||
If you are on a currently supported Ubuntu series (jammy 22.04 LTS, noble
|
||||
24.04 LTS, questing 25.10, resolute 26.04 LTS) and just want the latest
|
||||
upstream rsync, the rsync project maintains a Launchpad PPA that tracks
|
||||
stable releases:
|
||||
|
||||
> sudo add-apt-repository ppa:rsyncproject/rsync
|
||||
> sudo apt update && sudo apt install rsync
|
||||
|
||||
See [the PPA page][ppa] for current build status across architectures.
|
||||
|
||||
[ppa]: https://launchpad.net/~rsyncproject/+archive/ubuntu/rsync
|
||||
|
||||
To test the upcoming release instead, there is also a [`rsync-latest`
|
||||
PPA][ppa-latest] that is rebuilt from the tip of the git master branch. These
|
||||
are development snapshots whose version numbers (such as
|
||||
`3.5.0~git20260601...`) deliberately sort below the matching stable release, so
|
||||
the stable PPA above will never silently move you from a release onto a
|
||||
snapshot. Use it for testing only -- it may contain unreleased changes:
|
||||
|
||||
> sudo add-apt-repository ppa:rsyncproject/rsync-latest
|
||||
> sudo apt update && sudo apt install rsync
|
||||
|
||||
[ppa-latest]: https://launchpad.net/~rsyncproject/+archive/ubuntu/rsync-latest
|
||||
|
||||
The rest of this document covers building from source.
|
||||
|
||||
## The basic setup
|
||||
|
||||
You need to have a C compiler installed and optionally a C++ compiler in order
|
||||
|
||||
+232
-24
@@ -40,11 +40,12 @@ GENFILES=configure.sh aclocal.m4 config.h.in rsync.1 rsync.1.html \
|
||||
HEADERS=byteorder.h config.h errcode.h proto.h rsync.h ifuncs.h itypes.h inums.h \
|
||||
lib/pool_alloc.h lib/mdigest.h lib/md-defines.h
|
||||
LIBOBJ=lib/wildmatch.o lib/compat.o lib/snprintf.o lib/mdfour.o lib/md5.o \
|
||||
lib/permstring.o lib/pool_alloc.o lib/sysacls.o lib/sysxattrs.o @LIBOBJS@
|
||||
lib/permstring.o lib/pool_alloc.o lib/sysacls.o lib/sysxattrs.o lib/acl.o @LIBOBJS@
|
||||
zlib_OBJS=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \
|
||||
zlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o
|
||||
OBJS1=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
|
||||
util1.o util2.o main.o checksum.o match.o syscall.o log.o backup.o delete.o
|
||||
OBJS1_NO_MAIN=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
|
||||
util1.o util2.o checksum.o match.o syscall.o log.o backup.o delete.o
|
||||
OBJS1=$(OBJS1_NO_MAIN) main.o
|
||||
OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \
|
||||
usage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o
|
||||
OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@
|
||||
@@ -58,12 +59,15 @@ TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/perms
|
||||
# Programs we must have to run the test cases
|
||||
CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \
|
||||
testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \
|
||||
t_secure_relpath$(EXEEXT) wildtest$(EXEEXT) simdtest$(EXEEXT)
|
||||
t_rename_secure$(EXEEXT) t_symlink_secure$(EXEEXT) t_secure_relpath$(EXEEXT) t_acl$(EXEEXT) t_hashtable_overflow$(EXEEXT) t_iwildmatch$(EXEEXT) t_clean_fname$(EXEEXT) t_safe_arg$(EXEEXT) wildtest$(EXEEXT) simdtest$(EXEEXT)
|
||||
|
||||
CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test
|
||||
CHECK_SYMLINKS = testsuite/chown-fake_test.py testsuite/devices-fake_test.py \
|
||||
testsuite/xattrs-hlink_test.py testsuite/exclude-lsh_test.py
|
||||
|
||||
# Objects for CHECK_PROGS to clean
|
||||
CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_secure_relpath.o trimslash.o wildtest.o
|
||||
CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_rename_secure.o t_symlink_secure.o t_secure_relpath.o t_acl.o t_hashtable_overflow.o t_iwildmatch.o t_clean_fname.o t_safe_arg.o trimslash.o wildtest.o
|
||||
# Compile-only feature-shape checks.
|
||||
CHECK_COMPILE_OBJS=syscall-no-at-fdcwd.o
|
||||
|
||||
# note that the -I. is needed to handle config.h when using VPATH
|
||||
.c.o:
|
||||
@@ -76,6 +80,10 @@ CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_se
|
||||
all: Makefile rsync$(EXEEXT) stunnel-rsyncd.conf @MAKE_RRSYNC@ @MAKE_MAN@
|
||||
.PHONY: all
|
||||
|
||||
syscall-no-at-fdcwd.o: syscall.c $(HEADERS)
|
||||
$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \
|
||||
-DRSYNC_TEST_NO_AT_FDCWD -c $(srcdir)/syscall.c -o $@
|
||||
|
||||
.PHONY: install
|
||||
install: all
|
||||
-$(MKDIR_P) $(DESTDIR)$(bindir)
|
||||
@@ -83,12 +91,19 @@ install: all
|
||||
$(INSTALLCMD) -m 755 $(srcdir)/rsync-ssl $(DESTDIR)$(bindir)
|
||||
-$(MKDIR_P) $(DESTDIR)$(mandir)/man1
|
||||
-$(MKDIR_P) $(DESTDIR)$(mandir)/man5
|
||||
if test -f rsync.1; then $(INSTALLMAN) -m 644 rsync.1 $(DESTDIR)$(mandir)/man1; fi
|
||||
if test -f rsync-ssl.1; then $(INSTALLMAN) -m 644 rsync-ssl.1 $(DESTDIR)$(mandir)/man1; fi
|
||||
if test -f rsyncd.conf.5; then $(INSTALLMAN) -m 644 rsyncd.conf.5 $(DESTDIR)$(mandir)/man5; fi
|
||||
for fn in rsync.1 rsync-ssl.1; do \
|
||||
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \
|
||||
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \
|
||||
done
|
||||
for fn in rsyncd.conf.5; do \
|
||||
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man5; \
|
||||
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man5; fi; \
|
||||
done
|
||||
if test "$(with_rrsync)" = yes; then \
|
||||
$(INSTALLCMD) -m 755 rrsync $(DESTDIR)$(bindir); \
|
||||
if test -f rrsync.1; then $(INSTALLMAN) -m 644 rrsync.1 $(DESTDIR)$(mandir)/man1; fi; \
|
||||
fn=rrsync.1; \
|
||||
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \
|
||||
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \
|
||||
fi
|
||||
|
||||
install-ssl-daemon: stunnel-rsyncd.conf
|
||||
@@ -103,6 +118,21 @@ install-all: install install-ssl-daemon
|
||||
install-strip:
|
||||
$(MAKE) INSTALL_STRIP='-s' install
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(bindir)/rsync$(EXEEXT) $(DESTDIR)$(bindir)/rsync-ssl
|
||||
rm -f $(DESTDIR)$(bindir)/rrsync
|
||||
rm -f $(DESTDIR)$(mandir)/man1/rsync.1 $(DESTDIR)$(mandir)/man1/rsync-ssl.1
|
||||
rm -f $(DESTDIR)$(mandir)/man1/rrsync.1
|
||||
rm -f $(DESTDIR)$(mandir)/man5/rsyncd.conf.5
|
||||
|
||||
.PHONY: uninstall-ssl-daemon
|
||||
uninstall-ssl-daemon:
|
||||
rm -f $(DESTDIR)/etc/stunnel/rsyncd.conf
|
||||
|
||||
.PHONY: uninstall-all
|
||||
uninstall-all: uninstall uninstall-ssl-daemon
|
||||
|
||||
rsync$(EXEEXT): $(OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
|
||||
|
||||
@@ -116,6 +146,7 @@ usage.o: version.h latest-year.h help-rsync.h help-rsyncd.h git-version.h defaul
|
||||
loadparm.o: default-dont-compress.h daemon-parm.h
|
||||
|
||||
flist.o: rounding.h
|
||||
log.o: rounding.h
|
||||
|
||||
default-cvsignore.h default-dont-compress.h: rsync.1.md define-from-md.awk
|
||||
$(AWK) -f $(srcdir)/define-from-md.awk -v hfile=$@ $(srcdir)/rsync.1.md
|
||||
@@ -179,14 +210,55 @@ T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/sn
|
||||
t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS)
|
||||
|
||||
T_HASHTABLE_OVERFLOW_OBJ = t_hashtable_overflow.o hashtable.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
|
||||
t_hashtable_overflow$(EXEEXT): $(T_HASHTABLE_OVERFLOW_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_HASHTABLE_OVERFLOW_OBJ) $(LIBS)
|
||||
|
||||
T_IWILDMATCH_OBJ = t_iwildmatch.o lib/wildmatch.o
|
||||
t_iwildmatch$(EXEEXT): $(T_IWILDMATCH_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_IWILDMATCH_OBJ) $(LIBS)
|
||||
|
||||
T_CLEAN_FNAME_OBJ = t_clean_fname.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
|
||||
t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CLEAN_FNAME_OBJ) $(LIBS)
|
||||
|
||||
# safe_arg lives in options.c alongside the whole option parser. Rather than
|
||||
# rely on a non-portable linker --gc-sections to drop the parser (GNU ld only;
|
||||
# macOS ld64 and the cygwin PE linker do not), link the real rsync objects so
|
||||
# every dep resolves. t_safe_arg_main.o is main.c with main() renamed out, to
|
||||
# supply main.c's globals while letting t_safe_arg.o provide the test's main().
|
||||
# OBJS minus main.o is spelled out via OBJS1_NO_MAIN because $(filter-out) is
|
||||
# GNU-make-only; BSD and Solaris make expand it to nothing.
|
||||
t_safe_arg_main.o: main.c $(HEADERS)
|
||||
$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) -Dmain=rsync_unused_main -c $(srcdir)/main.c -o t_safe_arg_main.o
|
||||
T_SAFE_ARG_OBJ = t_safe_arg.o t_safe_arg_main.o $(OBJS1_NO_MAIN) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@
|
||||
t_safe_arg$(EXEEXT): $(T_SAFE_ARG_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SAFE_ARG_OBJ) $(LIBS)
|
||||
|
||||
T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
|
||||
t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS)
|
||||
|
||||
T_RENAME_SECURE_OBJ = t_rename_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
|
||||
t_rename_secure$(EXEEXT): $(T_RENAME_SECURE_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_RENAME_SECURE_OBJ) $(LIBS)
|
||||
|
||||
T_SYMLINK_SECURE_OBJ = t_symlink_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
|
||||
t_symlink_secure$(EXEEXT): $(T_SYMLINK_SECURE_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SYMLINK_SECURE_OBJ) $(LIBS)
|
||||
|
||||
T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
|
||||
t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS)
|
||||
|
||||
# Unit test for lib/acl.c: compares our fd/at ACL ops against the system libacl
|
||||
# (linked via $(LIBS), which carries -lacl). lib/acl.o references no rsync
|
||||
# globals, so this links with no stubs. Self-skips (exit 77) when built
|
||||
# without SUPPORT_ACL_FD.
|
||||
T_ACL_OBJ = t_acl.o lib/acl.o
|
||||
t_acl$(EXEEXT): $(T_ACL_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_ACL_OBJ) $(LIBS)
|
||||
|
||||
.PHONY: conf
|
||||
conf: configure.sh config.h.in
|
||||
|
||||
@@ -276,9 +348,11 @@ rrsync.1: support/rrsync.1.md md-convert Makefile
|
||||
|
||||
.PHONY: clean
|
||||
clean: cleantests
|
||||
rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \
|
||||
rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \
|
||||
git-version.h rounding rounding.h *.old rsync*.1 rsync*.5 @MAKE_RRSYNC_1@ \
|
||||
*.html daemon-parm.h help-*.h default-*.h proto.h proto.h-tstamp
|
||||
rm -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda
|
||||
rm -rf coverage coverage-tcp coverage-all coverage-fallback
|
||||
|
||||
.PHONY: cleantests
|
||||
cleantests:
|
||||
@@ -319,17 +393,148 @@ test: check
|
||||
# catch Bash-isms earlier even if we're running on GNU. Of course, we
|
||||
# might lose in the future where POSIX diverges from old sh.
|
||||
|
||||
# `make check` runs tests in parallel by default. Override with
|
||||
# `make check CHECK_J=1` (serial) or any other value.
|
||||
CHECK_J = 8
|
||||
|
||||
# Parallelism for `make coverage`. Defaults to the same as CHECK_J: the
|
||||
# coverage build sets -fprofile-update=atomic (atomic in-memory counters) and
|
||||
# gcc's libgcov serializes the per-source .gcda read-modify-write merge with a
|
||||
# file lock, so concurrent rsync processes (incl. the forked sender/generator/
|
||||
# receiver) accumulate exactly -- verified by a count-linearity check (a hot
|
||||
# line accumulates identically at -j1 and -P16). Override with
|
||||
# `make coverage COVERAGE_J=1` if your libgcov does not lock .gcda merges.
|
||||
COVERAGE_J = $(CHECK_J)
|
||||
|
||||
# Output directory and extra runtests.py flags for `make coverage`. The
|
||||
# `coverage-tcp` target reuses the coverage recipe with --use-tcp (real
|
||||
# loopback rsyncd, which exercises the TCP accept/auth path and the
|
||||
# require_tcp-only tests) and a separate output directory.
|
||||
COVERAGE_DIR = coverage
|
||||
COVERAGE_RUNFLAGS =
|
||||
|
||||
# Excluded from the coverage report so the percentages reflect rsync's own
|
||||
# runtime source. Three buckets:
|
||||
# (1) Bundled third-party code rsync ships but does not own: zlib/, popt/, and
|
||||
# the named lib/ imports (PostgreSQL getaddrinfo, ISC inet_ntop/inet_pton,
|
||||
# standalone getpass). The other lib/*.c are rsync's own and stay in.
|
||||
# (2) Test-helper / build-time programs that link against rsync objects but are
|
||||
# not the rsync runtime: t_*.c, tls.c, wildtest.c, testrun.c, getgroups.c,
|
||||
# getfsdev.c, trimslash.c, rounding.c. These have their own main() and are
|
||||
# either driven directly by a test (counted there) or are configure-time
|
||||
# probes; counting them as "rsync uncovered" is noise.
|
||||
# (3) Compile-time-dead fallbacks under this build's config.h: lib/md5.c (the
|
||||
# reference md5 -- openssl's EVP path is used when HAVE_OPENSSL) and
|
||||
# lib/snprintf.c (only the #include line survives under
|
||||
# HAVE_C99_VSNPRINTF). Covering these would mean a separate non-openssl /
|
||||
# non-C99 build, which is out of scope for this report.
|
||||
COVERAGE_EXCLUDE = -e '(^|/)zlib/' -e '(^|/)popt/' \
|
||||
-e '(^|/)lib/(getaddrinfo|getpass|inet_ntop|inet_pton)\.' \
|
||||
-e '(^|/)(t_[a-z_]+|tls|wildtest|testrun|getgroups|getfsdev|trimslash|rounding)\.c$$' \
|
||||
-e '(^|/)lib/(md5|snprintf)\.c$$'
|
||||
|
||||
# Build everything the test suite needs (rsync + helper programs + symlinks)
|
||||
# WITHOUT running it. Used by CI jobs that invoke runtests.py directly with
|
||||
# custom options (e.g. the version-mix workflow's --rsync-bin2/--expect-result).
|
||||
.PHONY: check-progs
|
||||
check-progs: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)
|
||||
|
||||
.PHONY: check
|
||||
check: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
|
||||
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT)
|
||||
check: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)
|
||||
"$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J)
|
||||
|
||||
.PHONY: check29
|
||||
check29: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
|
||||
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) --protocol=29
|
||||
check29: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)
|
||||
"$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=29
|
||||
|
||||
.PHONY: check30
|
||||
check30: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
|
||||
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) --protocol=30
|
||||
check30: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)
|
||||
"$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=30
|
||||
|
||||
# Whole-suite gcov coverage report (HTML, with branch + decision coverage).
|
||||
# Requires a build configured with --enable-coverage and the `gcovr` tool
|
||||
# (pip install gcovr). Runs the suite in parallel (COVERAGE_J, default CHECK_J):
|
||||
# this is safe because the coverage build uses -fprofile-update=atomic and
|
||||
# libgcov locks the per-source .gcda during its merge, so concurrent rsync
|
||||
# processes accumulate exactly (see COVERAGE_J above). Use COVERAGE_J=1 if your
|
||||
# toolchain's libgcov does not lock .gcda merges.
|
||||
.PHONY: coverage
|
||||
coverage: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
|
||||
@case '$(CFLAGS)' in *--coverage*) ;; \
|
||||
*) echo "*** not a coverage build; reconfigure with --enable-coverage"; exit 1 ;; esac
|
||||
@command -v gcovr >/dev/null 2>&1 || { echo "*** gcovr not found (pip install gcovr)"; exit 1; }
|
||||
find . -name '*.gcda' -delete
|
||||
@# Daemon modules with `uid = <non-root>` setuid the per-connection child
|
||||
@# (and so the forked generator/receiver), which then cannot create or
|
||||
@# merge .gcda files in a root-owned build dir -- silently dropping ALL
|
||||
@# coverage from those processes. Make every .gcno's directory
|
||||
@# world-writable so any uid can create the sibling .gcda, and set a
|
||||
@# default ACL of o::rw so the .gcda are world-mergeable regardless of
|
||||
@# the creator's umask (every test process resets umask to 022 via
|
||||
@# rsyncfns.py, so a Makefile-level `umask 0` would not survive).
|
||||
@find . -name '*.gcno' -printf '%h\n' 2>/dev/null | sort -u | \
|
||||
while read d; do \
|
||||
chmod a+rwx "$$d"; \
|
||||
setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' "$$d" 2>/dev/null || true; \
|
||||
done
|
||||
@rc=0; "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $(COVERAGE_RUNFLAGS) || rc=$$?; \
|
||||
rm -rf $(COVERAGE_DIR) && mkdir -p $(COVERAGE_DIR); \
|
||||
gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \
|
||||
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file \
|
||||
--html-details -o $(COVERAGE_DIR)/index.html . || exit $$?; \
|
||||
echo "Coverage report written to $(COVERAGE_DIR)/index.html"; \
|
||||
if test $$rc != 0; then \
|
||||
echo "*** test suite FAILED (status $$rc) -- coverage report still written above"; \
|
||||
fi; \
|
||||
exit $$rc
|
||||
|
||||
# Same as `make coverage` but with the daemon tests run over a real loopback
|
||||
# rsyncd (--use-tcp), into a separate report directory.
|
||||
.PHONY: coverage-tcp
|
||||
coverage-tcp:
|
||||
$(MAKE) coverage COVERAGE_RUNFLAGS=--use-tcp COVERAGE_DIR=coverage-tcp
|
||||
|
||||
# Comprehensive single report: run the suite under several configurations,
|
||||
# accumulating into the shared .gcda counters (NOT cleared between runs), then
|
||||
# emit one merged, rsync-scoped report. Covers the default (pipe) transport, the
|
||||
# protocol-29/30 compat branches, and the real-TCP daemon path (which also runs
|
||||
# the require_tcp-only tests). Run under sudo to additionally cover root-only
|
||||
# paths (devices, chown, use-chroot, protected-regular). Local target -- CI uses
|
||||
# the plain `coverage`/`coverage-tcp` targets.
|
||||
.PHONY: coverage-all
|
||||
coverage-all: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
|
||||
@case '$(CFLAGS)' in *--coverage*) ;; \
|
||||
*) echo "*** not a coverage build; reconfigure with --enable-coverage"; exit 1 ;; esac
|
||||
@command -v gcovr >/dev/null 2>&1 || { echo "*** gcovr not found (pip install gcovr)"; exit 1; }
|
||||
find . -name '*.gcda' -delete
|
||||
@# See the `coverage` target above for why: setuid'd daemon children must
|
||||
@# be able to create/merge .gcda owned by a different uid.
|
||||
@find . -name '*.gcno' -printf '%h\n' 2>/dev/null | sort -u | \
|
||||
while read d; do \
|
||||
chmod a+rwx "$$d"; \
|
||||
setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' "$$d" 2>/dev/null || true; \
|
||||
done
|
||||
@rc=0; \
|
||||
for cfg in '' '--protocol=30' '--protocol=29' '--use-tcp'; do \
|
||||
echo "===== coverage-all: runtests.py $$cfg ====="; \
|
||||
"$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $$cfg || rc=$$?; \
|
||||
done; \
|
||||
rm -rf coverage-all && mkdir -p coverage-all; \
|
||||
gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \
|
||||
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file \
|
||||
--html-details -o coverage-all/index.html . || exit $$?; \
|
||||
echo "Merged coverage report written to coverage-all/index.html"; \
|
||||
if test $$rc != 0; then \
|
||||
echo "*** some suite runs FAILED (status $$rc) -- report still written above"; \
|
||||
fi; \
|
||||
exit $$rc
|
||||
|
||||
# Coverage for the portable (non-openat2) resolver tier. Requires a SEPARATE
|
||||
# build configured with --enable-coverage --disable-openat2: its .gcno differ
|
||||
# from the openat2 build, so this report cannot be merged with the others.
|
||||
.PHONY: coverage-fallback
|
||||
coverage-fallback:
|
||||
$(MAKE) coverage COVERAGE_DIR=coverage-fallback
|
||||
|
||||
wildtest.o: wildtest.c t_stub.o lib/wildmatch.c rsync.h config.h
|
||||
wildtest$(EXEEXT): wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@
|
||||
@@ -343,14 +548,17 @@ simdtest$(EXEEXT): simd-checksum-x86_64.cpp $(HEADERS)
|
||||
touch $@; \
|
||||
fi
|
||||
|
||||
testsuite/chown-fake.test:
|
||||
ln -s chown.test $(srcdir)/testsuite/chown-fake.test
|
||||
testsuite/chown-fake_test.py:
|
||||
ln -s chown_test.py $(srcdir)/testsuite/chown-fake_test.py
|
||||
|
||||
testsuite/devices-fake.test:
|
||||
ln -s devices.test $(srcdir)/testsuite/devices-fake.test
|
||||
testsuite/devices-fake_test.py:
|
||||
ln -s devices_test.py $(srcdir)/testsuite/devices-fake_test.py
|
||||
|
||||
testsuite/xattrs-hlink.test:
|
||||
ln -s xattrs.test $(srcdir)/testsuite/xattrs-hlink.test
|
||||
testsuite/xattrs-hlink_test.py:
|
||||
ln -s xattrs_test.py $(srcdir)/testsuite/xattrs-hlink_test.py
|
||||
|
||||
testsuite/exclude-lsh_test.py:
|
||||
ln -s exclude_test.py $(srcdir)/testsuite/exclude-lsh_test.py
|
||||
|
||||
# This does *not* depend on building or installing: you can use it to
|
||||
# check a version installed from a binary or some other source tree,
|
||||
@@ -358,7 +566,7 @@ testsuite/xattrs-hlink.test:
|
||||
|
||||
.PHONY: installcheck
|
||||
installcheck: $(CHECK_PROGS) $(CHECK_SYMLINKS)
|
||||
$(srcdir)/runtests.py --rsync-bin="$(bindir)/rsync$(EXEEXT)" --srcdir="$(srcdir)" --tooldir=`pwd`
|
||||
"$(srcdir)/runtests.py" --rsync-bin="$(bindir)/rsync$(EXEEXT)" --srcdir="$(srcdir)" --tooldir="`pwd`" -j $(CHECK_J)
|
||||
|
||||
# TODO: Add 'dist' target; need to know which files will be included
|
||||
|
||||
|
||||
@@ -1,3 +1,546 @@
|
||||
# NEWS for rsync 3.5.0 (13 Aug 2026)
|
||||
|
||||
## Changes in this version:
|
||||
|
||||
### Thanks!
|
||||
|
||||
This has been an extraordinary release developed over several months
|
||||
and I'd like to thank everyone who has helped make it possible. The
|
||||
volume of security issues we had to deal with would have been quite
|
||||
overwhelming without the help that I've received.
|
||||
|
||||
I'm particularly grateful to Zen Dodd (Tao), Omar Elsayed (seks99x),
|
||||
Will Sargeant, Paul Mackerras, Aleksa Sarai and Leonid Bugaev (buger)
|
||||
who joined the rsync admins group helping to triage all the issues,
|
||||
develop new tests, review PRs and helped develop the guidelines we used
|
||||
for where to draw the line between a security issue and expected
|
||||
behaviour (a surprisingly difficult thing to do in some cases). You've
|
||||
all been a huge help and rsync is much better off for your assistance.
|
||||
|
||||
A big thank you also to Filipe Casal from Trail of Bits who worked with
|
||||
us on the "Patch the Planet" program. Filipe provided a huge trove of
|
||||
valuable tests and security reports.
|
||||
|
||||
Also a big thank you to Greg Kroah-Hartman for invaluable advice and
|
||||
security reports and to Stuart Inglis for particularly high quality
|
||||
bug reports and testing.
|
||||
|
||||
Many thanks to everyone who submitted bug reports, credits are listed
|
||||
below against individual items.
|
||||
|
||||
Finally, thank you to everyone who joined in the discussion and
|
||||
testing on the rsync-security mailing list, and to the rsync user
|
||||
community for your patience in waiting for this release.
|
||||
|
||||
### SECURITY FIXES:
|
||||
|
||||
This release fixes 33 security issues found during a focused audit of rsync's
|
||||
path handling and daemon protocol, a companion daemon-protocol fuzzing pass, and
|
||||
reports from external researchers -- plus several robustness hardenings. CVE
|
||||
IDs were assigned by VulnCheck (CNA); the precise "introduced in" version ranges
|
||||
accompany each advisory, and many are much narrower than "everything before
|
||||
3.5.0". Every fix ships with a regression test in the test suite that fails on
|
||||
the unfixed tree. Many thanks to the external researchers credited below.
|
||||
|
||||
Link following (CWE-59/61) -- a local user who controls a path component plants
|
||||
a symlink that a privileged rsync then follows:
|
||||
|
||||
- CVE-2026-53802 (HIGH): Arbitrary file read / transfer-shaping via symlinked
|
||||
operator-supplied input files. rsync followed attacker-planted symlinks in
|
||||
`--filter` merge files (including per-directory merges and `-C` `.cvsignore`),
|
||||
`--files-from` / `--include-from` / `--exclude-from`, and the client
|
||||
`--password-file` / daemon secrets file -- reading an arbitrary file as filter
|
||||
rules, or sending a victim file's contents as the daemon authentication
|
||||
response. Operator-supplied paths are now resolved component-by-component with
|
||||
`openat(O_PATH|O_NOFOLLOW)`, allowing a symlink component only when it is owned
|
||||
by uid 0 or the effective uid.
|
||||
|
||||
- CVE-2026-53803 (HIGH): Arbitrary file write / privilege escalation via
|
||||
symlinked operator-supplied output paths -- `--log-file`,
|
||||
`--write-batch`/`--read-batch`, and the daemon's motd / lock / early-input /
|
||||
`--config` opens. A planted symlink (or parent component) could redirect the
|
||||
write, e.g. append the log to `authorized_keys`; `--read-batch` could also feed
|
||||
chosen bytes to the protocol parser. Same trusted-owner path walk, plus an
|
||||
`S_ISREG` check on the `--read-batch` file.
|
||||
|
||||
- CVE-2026-53785 (HIGH): Under `--relative`, the receiver's implied-parent
|
||||
creation (`make_path()`) built the parent chain with a plain `mkdir()` on the
|
||||
full path, so a planted parent symlink placed the new directories and file
|
||||
outside the destination tree. `make_path()` now creates each component through
|
||||
the held-directory-fd primitive. Reported by Omar Elsayed (seks99x).
|
||||
|
||||
- CVE-2026-53784 (HIGH): Daemon module-root chdir escape under `use chroot =
|
||||
no`: a plain `chdir()` followed a planted parent-component symlink, serving
|
||||
files from outside the module. The module-root chdir now goes through the
|
||||
secure resolver.
|
||||
|
||||
- CVE-2026-53793 (HIGH): Chroot `/./` inner-module escape -- a symlinked
|
||||
parent component inside the inner module reached a sibling outside it (the
|
||||
generator basis stat, the receiver write/finish path, the module chdir, and the
|
||||
receiver's delta-basis open). The secure resolver is now engaged for all of
|
||||
those paths.
|
||||
|
||||
- CVE-2026-53795 (HIGH): An absolute `--temp-dir` or `--link-dest` disabled
|
||||
the receiver's rename/link confinement. `do_rename_at()`/`do_link_at()` bailed
|
||||
to the unconfined path-based call whenever *either* path was absolute, so an
|
||||
absolute source (the temp file, or the link-dest basis) let `finish_transfer()`'s
|
||||
tmp->final rename -- or a hard-link create -- follow a destination parent
|
||||
component an attacker flipped to a symlink mid-transfer, writing the file outside
|
||||
the tree. Each side is now confined independently. Reported by Omar Elsayed
|
||||
(seks99x).
|
||||
|
||||
- CVE-2026-53796 (MEDIUM): A non-daemon receiver's one-time `chdir()` into the
|
||||
operator-named destination was not fully confined (a relative destination took a
|
||||
plain `chdir()`), so an attacker who raced the named destination from a directory
|
||||
to a symlink moved the receiver's CWD -- and every file it then created --
|
||||
outside the tree. The destination chdir now uses the same ownership-checked
|
||||
`O_NOFOLLOW` walk as the daemon module chdir (see BEHAVIOR CHANGES). Reported by
|
||||
Omar Elsayed (seks99x).
|
||||
|
||||
- CVE-2026-53797 (MEDIUM): A non-daemon sender opened each transferred file's
|
||||
content by path (leaf `O_NOFOLLOW` only), so a source parent component an
|
||||
unprivileged user raced to a symlink after the file-list scan was followed --
|
||||
reading a file from outside the source tree into an attacker-readable
|
||||
destination. The content open is now anchored at the transfer root with
|
||||
`secure_relative_open()`; `-L` / `--copy-unsafe-links` / `-k` still follow, and
|
||||
`--insecure-links` restores the legacy open.
|
||||
|
||||
- CVE-2026-53799 (MEDIUM): Receiver ACL/xattr metadata application followed a
|
||||
symlink race -> arbitrary ACL set (local privilege escalation). When preserving
|
||||
metadata (`-A`/`--acls`, `-X`/`--xattrs`, or fake-super ACL-as-xattr), the
|
||||
receiver applied each entry's ACL/xattrs by path via `acl_set_file()` /
|
||||
`setxattr()`. A local user who raced a just-received entry (or a parent) into a
|
||||
symlink before the apply could redirect an attacker-chosen ACL -- the bytes are
|
||||
carried in the source entry -- onto a victim inode outside the destination tree,
|
||||
granting rwx on a root-owned file. The apply now pins each entry's inode with an
|
||||
`O_RDONLY|O_NOFOLLOW` fd and sets all metadata on the held inode (Linux 6.13+
|
||||
`*xattrat` syscalls, or a patched libacl's `*_at` bindings, else the
|
||||
`/proc/self/fd` compat path). Where neither primitive exists (the BSDs, Solaris,
|
||||
macOS, or a `/proc`-less Linux container) it falls back to the path-based apply to
|
||||
keep `--acls` functional -- a documented residual, refusable via `refuse options =
|
||||
acls`.
|
||||
|
||||
- CVE-2026-53800 (MEDIUM): Sender `--remove-source-files` unlink followed a
|
||||
parent-component symlink race -> arbitrary file deletion outside the source tree.
|
||||
The post-send unlink and its same-file safety re-stat resolved by path relative to
|
||||
the process CWD, so an unprivileged user who raced a source parent into a symlink
|
||||
after the file was sent could make a higher-authority sender (a root
|
||||
`--remove-source-files` run, or a daemon module not refusing the option) delete a
|
||||
file outside the served tree. The removal is now resolved through the secure
|
||||
held-dirfd walk anchored at the served module root (daemon) or transfer-root CWD
|
||||
(local sender), the safety re-stat is confined likewise, and the per-file dev/ino
|
||||
is only computed when `--remove-source-files` is in effect.
|
||||
|
||||
- CVE-2026-53801 (MEDIUM): Sender/daemon directory-scan enumeration escaped the
|
||||
transfer root / module -> out-of-tree disclosure. The sender enumerated each
|
||||
source directory with a plain `opendir()` on the accumulated path, not through the
|
||||
secure resolver (the enumeration sibling of the previous item, which confined only
|
||||
the content open). A parent component raced to a symlink between the file-list
|
||||
scan and the recursive `opendir()` -- or, in daemon following mode
|
||||
(`-L`/`--copy-dirlinks`/`--copy-unsafe-links`), an in-module symlinked directory
|
||||
pointing outside the module -- let a higher-authority sender enumerate an
|
||||
out-of-tree directory and copy its entry names, metadata and symlink targets. The
|
||||
directory scan is now confined through a held `opendir` fd anchored at the transfer
|
||||
root / module.
|
||||
|
||||
`support/rrsync` (the restricted SSH wrapper):
|
||||
|
||||
- CVE-2026-53783 (HIGH): rrsync restricted-directory escape. It validated each
|
||||
argument with `realpath()` and then exec'd rsync against the same name (a
|
||||
TOCTOU window), and left dangerous options enabled in a restricted subdir.
|
||||
rrsync now inode-pins the validated path and roots the argument it hands rsync
|
||||
at that pinned fd, denies `--copy-unsafe-links`, forces `--no-D`, and refuses a
|
||||
symlinked `--log-file`. The pin relies on Linux's `/proc/self/fd` magic links
|
||||
being bound to the open inode, so it is Linux-only; on the BSDs, macOS, Solaris
|
||||
and Cygwin rrsync passes the `realpath()`-validated name as it always did.
|
||||
Two limits are worth stating: under `--relative` only the anchor the
|
||||
transmitted name starts from is pinned, so a component below it can still be
|
||||
raced, and the final component of an ordinary sender argument is not pinned
|
||||
either (rsync does not follow a symlink there, and the options that would
|
||||
change that are refused in a restricted dir).
|
||||
|
||||
- A filter rule that failed to parse was echoed back verbatim, including when
|
||||
the rule came from a merge file's contents. A per-directory merge rule names
|
||||
a file the peer chooses and travels over the protocol rather than in an
|
||||
argument, so this let a peer read back any line of any file the server process
|
||||
could open that is not valid filter syntax -- through an `rrsync` restricted
|
||||
account as well as a daemon module, since neither confined a merge open that
|
||||
the wrapper never sees. A syntax error in a rule read from a file now reports
|
||||
the file and line rather than the text; a rule given as an argument is still
|
||||
shown. The `--debug=FILTER` traces print the same file-derived text, so
|
||||
`rrsync` now refuses a peer-selected `--debug` (a stock client never sends
|
||||
one). An operator who turns debugging on for their own server still sees the
|
||||
rule text.
|
||||
|
||||
- Redacting those diagnostics did not close the merge route on its own, because
|
||||
the worst shape produces no diagnostic at all: an exclude-only merge (the `-`
|
||||
modifier) makes every line of the file a pattern, so nothing fails to parse
|
||||
and the peer reads the contents off which of its own names went missing from
|
||||
the file list. Through an `rrsync` restricted account that needs no
|
||||
`--delete` and no verbosity on a pull. The open is now confined rather than
|
||||
the disclosure suppressed: rsync gained `--confine-root=DIR`, which refuses an
|
||||
operator- or peer-supplied path that resolves outside DIR, and `rrsync` passes
|
||||
its restricted directory. A merge file inside that directory keeps working.
|
||||
A daemon already had this through its module root and is unaffected.
|
||||
|
||||
Daemon protocol / identity:
|
||||
|
||||
- CVE-2026-53786 (MEDIUM): A client-supplied `--filter` merge file bypassed
|
||||
the module filter list (it was checked against the module-prefixed path, which
|
||||
never matched a module rule). The module-dir prefix is now stripped before the
|
||||
check. Reported by Mitchell Benjamin (Revamp Studio).
|
||||
|
||||
- CVE-2026-53798 (MEDIUM): The daemon name converter mapped an unknown name to
|
||||
uid/gid 0 (an empty response was read as `atol("") == 0`); with `fake super =
|
||||
yes` the stored metadata became root-owned. An empty/non-numeric response is
|
||||
now treated as a lookup failure. Reported by Mitchell Benjamin (Revamp
|
||||
Studio).
|
||||
|
||||
- CVE-2026-53788 (MEDIUM): A peer-controlled name containing a newline/CR was
|
||||
written verbatim into the name-converter line protocol, allowing request
|
||||
injection. Converter tokens containing control characters are now rejected.
|
||||
Reported by Mitchell Benjamin (Revamp Studio).
|
||||
|
||||
- CVE-2026-53789 (MEDIUM): A malicious daemon-sender could widen `--delete`
|
||||
scope by omitting the "no content dir" flag on an implied parent, making the
|
||||
receiver run `delete_in_dir()` on it. Implied-parent directories are now
|
||||
forced non-content on the receiver. Reported by Mitchell Benjamin (Revamp
|
||||
Studio).
|
||||
|
||||
- CVE-2026-53791 (CRITICAL): With `proxy protocol = true`, a client connecting
|
||||
directly (not via the trusted proxy) could send a PROXY header to spoof its
|
||||
source address and bypass host-based access control. A forwarded address is
|
||||
now honoured only from a configured trusted-proxy peer.
|
||||
|
||||
Injection and memory safety:
|
||||
|
||||
- CVE-2026-53790 (HIGH): Command / argument injection via unquoted peer- or
|
||||
host-controlled values -- the `RSYNC_CONNECT_PROG` `%H` host substitution, the
|
||||
daemon exec-hook `%RSYNC_*%` expansions, rsync-ssl hostspecs, and a missing
|
||||
newline/CR in remote-shell argument quoting. Each sink is now quoted or
|
||||
validated (the hook escaping is confined to the shell-executed hooks, so
|
||||
ordinary daemon string parameters such as `path` are unaffected).
|
||||
|
||||
- CVE-2026-53792 (MEDIUM): A malicious receiver sending a checksum header with a
|
||||
block count > 0 but block length == 0 drove the sender's rolling-match
|
||||
arithmetic negative. A zero block length is now rejected.
|
||||
|
||||
- CVE-2026-53794 (MEDIUM): `--max-alloc=0` disabled the per-allocation size
|
||||
cap (the defense behind CVE-2024-12084) and could be forwarded on the wire to
|
||||
an unpatched daemon. A zero max-alloc is now rejected at both the client and
|
||||
the daemon. Reported by Azizcan Dastan (Milenium Security).
|
||||
|
||||
Peer-triggerable memory corruption in the daemon protocol, found by a
|
||||
daemon-protocol fuzzing pass and reported by Greg Kroah-Hartman. Each is a
|
||||
WRITE reachable from the wire, which is why these were split out from the
|
||||
crash-only findings in the same pass:
|
||||
|
||||
- CVE-2026-70461 (HIGH): a one-byte heap out-of-bounds write in
|
||||
`add_implied_include()`, driven by a peer-supplied filter rule whose trailing
|
||||
backslash was not counted when sizing the copy.
|
||||
|
||||
- CVE-2026-70458 (HIGH): an out-of-bounds write from a file entry marked
|
||||
`FLAG_HLINKED` that the receiver accepted even though `-H` was not in effect,
|
||||
so the hard-link extra slots it then wrote were never allocated.
|
||||
|
||||
- CVE-2026-70456 (HIGH): an out-of-bounds heap write in `read_args()` when the
|
||||
peer's argument count lands exactly on `maxargs` -- the trailing NULL went one
|
||||
past the end of the array.
|
||||
|
||||
- CVE-2026-70457 (MEDIUM): an attacker-chosen-offset write in
|
||||
`parse_size_arg()`'s error formatting, reachable through an over-large
|
||||
`--max-size` / `--min-size` / `--max-alloc` forwarded to a daemon.
|
||||
|
||||
- CVE-2026-70459 (MEDIUM): a wild-pointer read crashing the per-connection
|
||||
daemon child, from a crafted first incremental file list whose transfer root
|
||||
is "." with a non-directory mode -- `parent_ndx` stayed 0 while `dir_flist`
|
||||
was still empty, so the generator dereferenced a never-written slot.
|
||||
Companion to CVE-2026-43620; reproduced on released 3.2.7, 3.4.0 and 3.4.1.
|
||||
|
||||
Daemon availability and access control:
|
||||
|
||||
- CVE-2026-70464 (HIGH): an unauthenticated peer could complete the `@RSYNCD`
|
||||
greeting and then stall forever -- sending a line with no terminator, or
|
||||
trickling NUL-terminated arguments into `read_args()` one byte at a time --
|
||||
holding a per-connection child open past the module's `max connections`
|
||||
limit. The `timeout` parameter did not cover it, because `set_io_timeout()`
|
||||
ran after the `read_args()` calls that needed covering. A separate deadline
|
||||
now spans both, and the early-protocol argument count is bounded. Reported
|
||||
independently by Chamal De Silva and by Michal Ruprich (Red Hat QE).
|
||||
|
||||
- CVE-2026-70455 (HIGH): a daemon client could request an arbitrary Zstandard
|
||||
worker count via `--compress-threads`; 256 was measured as 257 threads in a
|
||||
single connection. Now capped at 8 on a daemon, while local and
|
||||
remote-shell invocations keep the operator's value. Reported, fixed and
|
||||
tested by Filipe Casal of Trail of Bits, in collaboration with OpenAI.
|
||||
|
||||
- CVE-2026-70453 (HIGH): quadratic CPU exhaustion in `hash_search()` from a
|
||||
crafted chain of equal weak checksums. The chain walk is now bounded. First
|
||||
reported as a performance problem in public rsync issue #217 by heyciao
|
||||
(2021); recognised as a security issue, bounded and regression-tested by
|
||||
Stuart Inglis. This one was already public and was not embargoed.
|
||||
|
||||
- CVE-2026-70452 (HIGH): `hosts deny` failed OPEN when a configured hostname
|
||||
could not be resolved -- with `forward lookup` enabled, which is the default,
|
||||
an unresolvable deny token admitted the host it was meant to block. It now
|
||||
fails closed. Sibling of CVE-2026-43617. Reported by Leonid Bugaev.
|
||||
|
||||
- CVE-2026-70463 (HIGH): `auth users` ignored its documented comma-only
|
||||
parsing. With a leading comma the split should be on commas alone, so that a
|
||||
group name containing a space can be written; it split on whitespace too, so
|
||||
a `deny` or `:ro` rule naming such a group was broken into two meaningless
|
||||
tokens and never fired. Reported by Andres Berbescu.
|
||||
|
||||
- CVE-2026-70460 (HIGH): a peer-supplied `--partial-dir` or `--backup-dir` was
|
||||
resolved by pathname, so an in-module symlink could redirect it and place
|
||||
files outside the daemon's module root. Those paths are now confined.
|
||||
Reported by Omar Elsayed (seks99x).
|
||||
|
||||
Client-side:
|
||||
|
||||
- CVE-2026-70462 (MEDIUM): a peer-supplied `MSG_IO_TIMEOUT` defeated the
|
||||
client's own I/O timeout -- a large value overflowed signed arithmetic, and a
|
||||
non-positive value disabled the timeout outright. The value is now capped on
|
||||
receipt and the arithmetic made overflow-safe. Reported by Z3R0S! (z3r0s6);
|
||||
the non-positive case was reported by Leonid Bugaev.
|
||||
|
||||
- CVE-2026-70454 (MEDIUM): `rsync-ssl` established an unauthenticated TLS
|
||||
connection. In stunnel mode it neither required CA verification nor bound
|
||||
the certificate to the requested hostname, so an active network attacker
|
||||
could impersonate the server; the openssl backend had a matching hostname
|
||||
gap in 3.2.0 through 3.2.3 (found and fixed in 2020 by Matt McCutchen).
|
||||
stunnel mode now requires certificate verification and hostname binding
|
||||
unless an explicit insecure opt-out is set, and the GnuTLS backend is
|
||||
refused conservatively rather than used unverified (Greg Kroah-Hartman).
|
||||
|
||||
Robustness hardening (no CVE assigned): the `RSYNC_PROXY` CONNECT request and
|
||||
proxy response headers are length-bounded, and peer-requested xattr expansion is
|
||||
capped.
|
||||
|
||||
A second-pass source audit (reported by Leonid Bugaev) hardened several memory-
|
||||
safety and robustness paths: the hashtable and file-list size computations are
|
||||
guarded against a 32-bit integer overflow that a peer's entry count could
|
||||
otherwise wrap into an under-allocation, and the
|
||||
`SIGUSR2` handler is now async-signal-safe (it only sets a flag, deferring the
|
||||
summary/close-out work to safe poll points). Separately, the xattr/ACL metadata
|
||||
copy now reads the *source* through a held no-follow fd as well as writing the
|
||||
destination through one -- closing a parent-symlink race on the `--copy-dest` and
|
||||
backup source -- and the cross-tree operator-path metadata apply is now fd-pinned
|
||||
under `--fake-super` too (previously it fell back to a path-based set for a
|
||||
`fake super = yes` daemon staging through an absolute `--temp-dir`/`--backup-dir`).
|
||||
|
||||
### SECURITY RELATED:
|
||||
|
||||
- Mask a peer-supplied I/O-error value to the defined `IOERR_*` bits, both the
|
||||
incoming `MSG_IO_ERROR` message (`io.c`) and the file-list trailer (`flist.c`),
|
||||
so a malicious peer cannot set arbitrary (undefined) error flags that would be
|
||||
stored in the local `io_error` and re-forwarded upstream. (Undefined bits
|
||||
never reached the exit code, which maps only the defined bits.) Reported by
|
||||
Leonid Bugaev.
|
||||
|
||||
- Escape control characters in filenames written to the log file (CWE-117 log
|
||||
injection): a transferred name containing control bytes -- C0 (tab excepted)
|
||||
and C1 `0x80`-`0x9f`, including CSI `0x9b` -- could otherwise inject terminal
|
||||
escape sequences into an administrator's terminal when the log is viewed.
|
||||
Reported by Leonid Bugaev.
|
||||
|
||||
- Stop `safe_arg()` leaking an uninitialized byte into a quoted filename. In
|
||||
filename mode the writer suppresses the escaping backslash before a wildcard,
|
||||
but the counter that sized the buffer reserved a slot for every backslash, so
|
||||
the two disagreed and left an uninitialized heap byte in the returned string
|
||||
-- which is handed to the remote shell when `--protect-args` is off. The
|
||||
counter now mirrors the writer, and guarding the wildcard test with `f[1]`
|
||||
also fixes a trailing backslash (previously `strchr()` matched the string
|
||||
terminator, so the backslash was not doubled). Reported by Leonid Bugaev.
|
||||
|
||||
- Close a `--safe-links` bypass in `--backup`: when symlinks can be hard-linked,
|
||||
`make_backup()`'s link/rename fast path hard-linked an unsafe (out-of-tree)
|
||||
symlink into the backup area and skipped the `safe_symlinks` check the copy
|
||||
path applies, silently preserving a link `--safe-links` was meant to drop. The
|
||||
safe-links check now runs before the fast path, and a symlink whose target is
|
||||
unreadable is failed closed rather than backed up unchecked. Reported by
|
||||
Leonid Bugaev.
|
||||
|
||||
- Extend the operator-directory ownership walk to the backup leaf sinks:
|
||||
`do_symlink_at()` (backing a symlink up into an operator `--backup-dir`) and
|
||||
`do_rmdir_at()` (removing a pre-existing backup directory) now resolve their
|
||||
parent through the same ownership walk, so a foreign-owned parent symlink no
|
||||
longer redirects the backup symlink-create or directory-removal outside the
|
||||
backup tree. `--insecure-links` (or a module's `insecure links = yes`) restores
|
||||
the legacy follow. Reported by Omar Elsayed (seks99x).
|
||||
|
||||
- Confine an absolute operator source/destination through the ownership walk in
|
||||
`robust_rename()`'s cross-filesystem (EXDEV) copy fallback, so a raced parent
|
||||
symlink cannot redirect the fallback copy or its source unlink out of the tree.
|
||||
Reported by Leonid Bugaev.
|
||||
|
||||
- Bound the number of equal-weak-checksum blocks examined per offset in
|
||||
`hash_search()` (issue #217), so a crafted or degenerate checksum set with a
|
||||
very long equal-checksum chain cannot drive the sender's per-offset
|
||||
match-verify into a quadratic blow-up (CPU DoS). Fix by Stuart Inglis.
|
||||
|
||||
### BUG FIXES:
|
||||
|
||||
- Fix an off-by-one in `clean_fname()`'s `..`-collapse path normalization.
|
||||
Reported by Leonid Bugaev.
|
||||
|
||||
- The AVX2 rolling-checksum assembly (`--enable-roll-asm`) read up to 64 bytes
|
||||
past the end of the buffer it was given. The loop is software-pipelined and
|
||||
preloaded the 64 bytes after the ones it was folding in, so its last iteration
|
||||
always reached beyond the data -- the remainder is by construction under 64
|
||||
bytes. It normally landed in slack inside rsync's map window and went
|
||||
unnoticed; where the buffer ended at a page boundary it was a SIGSEGV mid
|
||||
transfer, reported on macOS x86-64 by Roland Kletzing. Reported checksums are
|
||||
unchanged.
|
||||
|
||||
- `--link-dest` no longer fails the transfer when the destination refuses to
|
||||
hard-link a symlink, device node, FIFO or socket. Whether rsync hard-links
|
||||
those at all was decided at build time, on whatever filesystem the source tree
|
||||
happened to sit on, and one host can hold both answers -- macOS builds on
|
||||
APFS, which can, and backs up to HFS+, which returns ENOTSUP. Such an entry
|
||||
is now copied, exactly as it already is in a build that cannot link them and
|
||||
as a regular file in the same position already was; the run used to exit 23
|
||||
even though the entry was then created correctly. The fallback covers any
|
||||
refusal, since the error does not identify one on its own: link(2) documents
|
||||
EPERM both for a filesystem without hard links and for a permission refusal.
|
||||
Still outstanding: under `-H`, a group of such entries hard-linked to each
|
||||
other also needs a link within the destination, and where the destination
|
||||
cannot hard-link the type at all, the members after the first are still lost.
|
||||
|
||||
- `--out-format` / `--log-file-format` now emit a literal `%` for `%%` instead of
|
||||
mis-parsing the following character (added by Leonid Bugaev); a follow-up bounds
|
||||
`log_format_has()`'s width-digit scan to match `log_formatted()`, closing a `%C`
|
||||
read past the checksum field.
|
||||
|
||||
- A CVS `.cvsignore` (or `-C`) file containing a `!` clear-list token no longer
|
||||
aborts with a spurious "rule has trailing characters" error. Reported by
|
||||
Leonid Bugaev.
|
||||
|
||||
- `--chmod=a+s` now sets both the setuid and setgid bits, matching `chmod(1)`
|
||||
(it previously set setuid only). Reported by Leonid Bugaev.
|
||||
|
||||
- Case-insensitive wildcard matching (used by daemon `hosts allow`/`hosts deny`
|
||||
rules) now folds characters inside a `[...]` bracket expression, not just
|
||||
literal pattern characters. Reported by Leonid Bugaev.
|
||||
|
||||
### BEHAVIOR CHANGES:
|
||||
|
||||
- A non-daemon receiver follows an operator-named symlinked destination directory
|
||||
only when the symlink is owned by root or the running user (e.g. `rsync -a src/
|
||||
/backup/` where `/backup -> /mnt/disk`); a destination symlinked by another uid
|
||||
is now refused, closing a chdir TOCTOU where an attacker raced the named
|
||||
destination into a symlink. `--insecure-links` restores the unconditional
|
||||
follow.
|
||||
|
||||
- On platforms without a race-safe way to create a unix socket in a subdirectory
|
||||
(the BSDs, macOS, Solaris, which lack `bindat()`), a nested socket transferred
|
||||
under `--specials` is skipped with a warning instead of failing the whole
|
||||
transfer. Top-level sockets are unaffected.
|
||||
|
||||
- `proxy protocol = true` with no `proxy protocol hosts` rejects all connections
|
||||
(fail-closed); the daemon now warns about this at startup.
|
||||
|
||||
- `support/rrsync` in a restricted subdirectory forces `--no-D` (device/special
|
||||
semantics are stripped, so a plain `rsync -a` still works) and denies
|
||||
`--copy-unsafe-links`.
|
||||
|
||||
- The path resolver now follows in-tree directory symlinks uniformly on every
|
||||
platform via a single race-free per-component `O_NOFOLLOW` walk, so `-K` /
|
||||
`-L` / `-k` and `-R` through an in-tree symlinked parent behave the same
|
||||
everywhere.
|
||||
|
||||
# NEWS for rsync 3.4.4 (8 Jun 2026)
|
||||
|
||||
## Changes in this version:
|
||||
|
||||
This is a conservative point release that backports regression fixes
|
||||
on top of 3.4.3. No new features are included.
|
||||
|
||||
### BUG FIXES:
|
||||
|
||||
- Honour a relative alt-basis directory (e.g. `--link-dest=../sibling`,
|
||||
`--copy-dest`, `--compare-dest`) on a daemon receiver running with
|
||||
`use chroot = no`. Such a path is re-anchored at the module root but
|
||||
was then rejected by the receiver's secure open; it now works where
|
||||
kernel-enforced confinement is available. See the PORTABILITY note
|
||||
below for the platform limitation. Fixes #915.
|
||||
|
||||
- sender: open a module-root-absolute path for a `path = /` module so a
|
||||
daemon serving the filesystem root can satisfy absolute request
|
||||
paths again. Fixes #897.
|
||||
|
||||
- flist: accept the missing-args mode-0 entry in recv_file_entry.
|
||||
Fixes #910.
|
||||
|
||||
- receiver: fix a false "failed verification -- update discarded" when
|
||||
resuming a delta transfer with an absolute `--partial-dir`.
|
||||
|
||||
- receiver: fix a NULL dereference on the delta discard path.
|
||||
|
||||
- generator: cap the block s2length at the negotiated checksum length.
|
||||
|
||||
- main: fix `--mkpath` with `--dry-run` for a file-to-file copy.
|
||||
Fixes #880.
|
||||
|
||||
- daemon: un-backslash escaped option args. Fixes #829.
|
||||
|
||||
- token: drain the matched-block insert deflate. Fixes #951.
|
||||
|
||||
- Fix the "update skips a file of a different type" case and the
|
||||
daemon upload delete stats.
|
||||
|
||||
- alloc: revert "zero all new memory from allocations". Fixes #959.
|
||||
|
||||
- Always clear the stat buffer and validate nanoseconds before use.
|
||||
|
||||
### PORTABILITY / BUILD:
|
||||
|
||||
- The relative alt-basis fix for daemon receivers (#915) relies on
|
||||
kernel "stay below dirfd" path resolution -- `openat2(RESOLVE_BENEATH)`
|
||||
on Linux 5.6+, or `openat()` with `O_RESOLVE_BENEATH` on FreeBSD 13+
|
||||
and macOS 15+. On platforms that lack it (Solaris, OpenBSD, NetBSD,
|
||||
Cygwin and older Linux) `secure_relative_open()` deliberately rejects
|
||||
any path with a `..` component, so relative alt-basis directories
|
||||
remain unavailable there -- function traded for safety, matching the
|
||||
trade-off already documented for the #715 fix. Absolute alt-basis
|
||||
paths are unaffected on every platform.
|
||||
|
||||
- openat2 is now autodetected at configure time (HAVE_OPENAT2): the
|
||||
`openat2(RESOLVE_BENEATH)` resolver is compiled in only when both
|
||||
`<linux/openat2.h>` and the `SYS_openat2` syscall number are present,
|
||||
fixing the build on older kernels/headers. Fixes #924, #905, #900,
|
||||
#904.
|
||||
|
||||
- Fall back to do_mknod() when mknodat() / mkfifoat() are unavailable.
|
||||
Fixes #896.
|
||||
|
||||
- Install generated manpages correctly in an out-of-tree build.
|
||||
|
||||
### DEVELOPER RELATED:
|
||||
|
||||
- Added a CI workflow that builds this stable branch and runs the
|
||||
`v34-stable-testsuite` regression suite against the built binary,
|
||||
giving regression coverage without importing the full master test
|
||||
suite into the stable branch.
|
||||
|
||||
- Added a check-progs target for fleettest and extended the build
|
||||
workflows to run on `*-stable` release branches.
|
||||
|
||||
### CREDITS:
|
||||
|
||||
Thanks to everyone who helped with this release:
|
||||
|
||||
- Code contributions from Zen Dodd (steadytao), Mike-Goutokuji,
|
||||
pterror, and Stiliyan Tonev (Bark).
|
||||
|
||||
- Zen Dodd (steadytao) also reviewed the 3.4.4 backport set (PR #980).
|
||||
|
||||
- Bug reports from @mmayer (#924), @fda77 (#905), @darkshram (#900),
|
||||
@ketas (#904), @pkzc (#880), @brabalan (#951), @elcamlost (#829),
|
||||
@debohman (#896), @guilherme-puida (#959), @fufu65 (#915),
|
||||
@JetAppsClark (#928), @moonlitbugs (#897), @mgkeeley (#910), and
|
||||
@sylvain-ilm (#724, #725).
|
||||
|
||||
# NEWS for rsync 3.4.3 (20 May 2026)
|
||||
|
||||
## Changes in this version:
|
||||
@@ -119,6 +662,14 @@ modtime.
|
||||
|
||||
### BUG FIXES:
|
||||
|
||||
- Fixed a bypass of `--safe-links` when `--backup` is also used on a system that supports hard-linking symlinks (Linux, macOS). An escaping symlink that should have been skipped was silently preserved in the backup area.
|
||||
|
||||
- Fixed a spurious abort when using `-C` (cvs-exclude) mode with a `.cvsignore` file that contained a `!` (clear-list) token.
|
||||
|
||||
- Updated the `--max-alloc` documentation to reflect that 0 is now rejected (CVE-2026-53794).
|
||||
|
||||
- Fixed the EXIT VALUES table: removed nonexistent code 6, added missing codes 15/16/19, corrected SIGUSR1 classification.
|
||||
|
||||
- Fixed a regression introduced by the 3.4.0 secure_relative_open()
|
||||
CVE fix where legitimate directory symlinks on the receiver side
|
||||
(e.g. when using `-K` / `--copy-dirlinks`) caused "failed
|
||||
@@ -5169,6 +5720,8 @@ to develop and test fixes.
|
||||
|
||||
| RELEASE DATE | VER. | DATE OF COMMIT\* | PROTOCOL |
|
||||
|--------------|--------|------------------|-------------|
|
||||
| 13 Aug 2026 | 3.5.0 | | 32 |
|
||||
| 08 Jun 2026 | 3.4.4 | | 32 |
|
||||
| 20 May 2026 | 3.4.3 | | 32 |
|
||||
| 28 Apr 2026 | 3.4.2 | | 32 |
|
||||
| 16 Jan 2025 | 3.4.1 | | 32 |
|
||||
|
||||
@@ -93,6 +93,15 @@ details.
|
||||
[3]: https://rsync.samba.org/lists.html
|
||||
|
||||
|
||||
DISCORD
|
||||
-------
|
||||
|
||||
There is also an rsync [Discord server][d] for real-time chat about rsync
|
||||
and its development.
|
||||
|
||||
[d]: https://discord.gg/Avfvy9zhdp
|
||||
|
||||
|
||||
BUG REPORTS
|
||||
-----------
|
||||
|
||||
@@ -136,6 +145,8 @@ COPYRIGHT
|
||||
Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many
|
||||
people from around the world have helped to maintain and improve it.
|
||||
|
||||
Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.
|
||||
|
||||
Rsync may be used, modified and redistributed only under the terms of
|
||||
the GNU General Public License, found in the file [COPYING][9] in this
|
||||
distribution, or at [the Free Software Foundation][10].
|
||||
|
||||
+529
@@ -11,3 +11,532 @@ Email your vulnerability information to rsync's maintainer:
|
||||
|
||||
Rsync Project <rsync.project@gmail.com>
|
||||
|
||||
|
||||
## Approach to platform residuals
|
||||
|
||||
rsync hardens its security-sensitive operations — path resolution, metadata
|
||||
application, file/socket creation — against local attacks such as parent-symlink
|
||||
TOCTOU races. Some of these operations can only be made race-safe with a
|
||||
primitive the underlying OS provides (an `*at()` syscall on a held directory fd,
|
||||
an fdescfs-style `/proc/self/fd` magic symlink, `mknodat()`, the `*xattrat`
|
||||
syscalls, and so on), and that primitive is not available on every supported
|
||||
platform.
|
||||
|
||||
The guiding rule for those cases is:
|
||||
|
||||
> **On a modern Linux system every issue described in this document is fully
|
||||
> addressed.** Where an operation *can* be secured on some platforms but *cannot*
|
||||
> be secured on others, and the residual risk is a *local* privilege-escalation
|
||||
> or data-disclosure class (an attacker who already has write access inside the
|
||||
> transferred tree), rsync prefers keeping the operation functional on the
|
||||
> platforms that lack the primitive over disabling a long-standing feature for
|
||||
> everyone on those platforms.
|
||||
|
||||
So a hardened operation takes the race-safe path wherever the platform offers one
|
||||
and falls back to the historical (path-based, unconfined) behaviour only where it
|
||||
does not — rather than refusing the operation outright. Each such fallback is an
|
||||
accepted residual, documented under "Known residuals" below, and on the daemon it
|
||||
can be turned off per feature with `refuse options = ...`. The residuals are
|
||||
therefore confined to non-Linux platforms (the BSDs, macOS, Solaris/illumos),
|
||||
Cygwin, and — for a few features — pre-6.13 Linux kernels; a current, normally
|
||||
configured Linux deployment carries none of them. (The `/proc/self/fd`-based
|
||||
fallbacks assume a mounted `/proc`, which every standard Linux provides; a
|
||||
deliberately `/proc`-less container is the one Linux case that can still hit a
|
||||
residual.)
|
||||
|
||||
The one deliberate exception is an operation whose unconfined fallback would
|
||||
*create a new filesystem object at an attacker-influenceable path* rather than set
|
||||
metadata on the object rsync already transferred: the nested-socket `bind()` on
|
||||
platforms without a race-safe socket-create (no `bindat()`). There the unsafe path
|
||||
is an out-of-tree write/create primitive, not a same-object metadata race, and a
|
||||
transferred socket inode is a worthless placeholder, so rsync refuses (skips) it
|
||||
rather than keeping it functional. A leaf permission change is likewise failed
|
||||
closed rather than applied through a raced symlink, but only as a rare backstop:
|
||||
the common file/dir/FIFO case is secured on every platform via `fchmod` on a held
|
||||
fd, so no real functionality is lost.
|
||||
|
||||
This trade-off applies only to these local-attacker residual classes. Remotely
|
||||
reachable defects — memory safety, authentication bypass, protocol parsing, input
|
||||
bounds — are fixed unconditionally on all platforms, never left as a residual.
|
||||
|
||||
|
||||
## Robustness against malicious peers
|
||||
|
||||
rsync treats everything the peer sends — the file list, checksum headers,
|
||||
multiplexed messages, forwarded daemon arguments, filter rules — as untrusted,
|
||||
and bounds-checks it before use. A peer-triggerable crash of a connection's
|
||||
worker process is treated as a defect to be fixed, even though the daemon's
|
||||
fork-per-connection model confines such a fault to that one connection rather
|
||||
than the whole service.
|
||||
|
||||
Alongside the issues enumerated elsewhere in this document, the code is hardened
|
||||
continuously through protocol fuzzing (driving the daemon protocol against a
|
||||
writable module) and static analysis, with a CI gate. This release closes a
|
||||
batch of peer-triggerable faults found that way: NULL-dereference and
|
||||
reachable-assert crashes from crafted file lists or indices, reads past a
|
||||
file-list allocation (mostly bounded over-reads of an entry's extra slots),
|
||||
unbounded merge-file and suffix-list recursion, and several bounded
|
||||
out-of-bounds writes driven by peer-supplied lengths or option arguments. Each
|
||||
is fixed at the root with a bounds or validity check plus a defence-in-depth
|
||||
guard at the use site, and carries a regression test.
|
||||
|
||||
Two further peer-input hardenings in this release: a peer-supplied I/O-error
|
||||
value (the `MSG_IO_ERROR` message and the file-list trailer) is masked to the
|
||||
defined `IOERR_*` bits, so a peer cannot set arbitrary error flags in the local
|
||||
`io_error` that would be stored and re-forwarded upstream; and control
|
||||
characters in a (peer-controlled) filename written to the log file are escaped,
|
||||
so a name carrying C0/C1 terminal-escape bytes cannot inject sequences into an
|
||||
administrator's terminal when the log is viewed (CWE-117). The number of
|
||||
equal-weak-checksum blocks `hash_search()` examines per offset is also bounded
|
||||
(issue #217), so a crafted or degenerate checksum set with a very long
|
||||
equal-checksum chain cannot drive the sender's per-offset match-verify into a
|
||||
quadratic walk and pin one connection's CPU.
|
||||
|
||||
Contributors adding code that consumes peer input should validate it at the
|
||||
point of receipt rather than relying on a downstream check.
|
||||
|
||||
|
||||
## Symlink-race-safe path resolution
|
||||
|
||||
This section documents how rsync defends against parent-directory symlink races
|
||||
(a TOCTOU / confused-deputy class) and the per-platform approach it takes, so
|
||||
that contributors and automated agents extend the code consistently rather than
|
||||
reintroducing the weakness.
|
||||
|
||||
### The threat
|
||||
|
||||
Many rsync operations resolve pathnames that an unprivileged party can partially
|
||||
control: a receiver writing into a destination tree, a sender reading a source
|
||||
tree, and temp and partial files, and so on. (The operator-chosen directory
|
||||
paths — `--link-dest`/`--compare-dest`/`--copy-dest`/`--backup-dir`/`--temp-dir`/
|
||||
`--partial-dir` — may legitimately point outside the tree, so they are resolved
|
||||
by the ownership walk described under *Symlink defense for operator-supplied
|
||||
paths* below rather than the strict transfer-path resolver here.) If someone who
|
||||
can write inside that tree races a
|
||||
parent directory component between a real directory and a symlink ("symlink
|
||||
flipping"), a path-based syscall — `open`, `stat`, `chmod`, `chown`, `utimes`,
|
||||
`rename`, `unlink`, `mkdir`, `mknod`, `symlink`, hard-link creation — can be
|
||||
redirected to a target *outside* the intended tree. When rsync resolves that
|
||||
path with more authority than the component's controller and without a
|
||||
confinement boundary, this is a confused-deputy bug (e.g. a root nightly backup
|
||||
capturing `/etc/shadow`, or a root receiver chmod/chown/unlink-ing a system
|
||||
file).
|
||||
|
||||
`O_NOFOLLOW` on the final component is **not** sufficient: the *parent*
|
||||
components must be resolved safely.
|
||||
|
||||
The boundary that matters is **authority plus confinement**, not "daemon vs
|
||||
non-daemon". A non-chroot daemon module, a root-run local transfer, and a
|
||||
two-user transfer are all unconfined privileged path resolvers. Where a real
|
||||
confinement boundary already exists (e.g. a per-module `chroot`) that is the
|
||||
strongest protection; otherwise rsync must resolve paths defensively.
|
||||
|
||||
A `chroot` is only a boundary for the *outer* path it confines. A daemon module
|
||||
written as `path = /outer/./inner` (`use chroot = yes`) chroots to `/outer` but
|
||||
treats `/inner` as the module root, so a symlink inside the module that points to
|
||||
a sibling of `/inner` is still inside the chroot yet outside the module — the
|
||||
inner module therefore needs the same defensive resolution as a non-chroot
|
||||
module. The single gate that decides when hardened resolution applies is
|
||||
"unconfined privileged resolver": `am_daemon && (!am_chrooted || module_dirlen)`
|
||||
for the daemon (any non-chroot module, plus a `/./` inner-module chroot), and any
|
||||
non-chroot receiver. The local sender's content open is confined the same way for
|
||||
default symlink handling; only the symlink-following modes (`-L`/`--copy-links`/
|
||||
`--copy-unsafe-links`/`-k`) and `--insecure-links` are excluded, so those keep
|
||||
following symlinks by design.
|
||||
|
||||
### The mechanism
|
||||
|
||||
Resolution of attacker-influenceable paths goes through `secure_relative_open()`
|
||||
and the `do_*_at()` wrappers in `syscall.c`, never a raw `open()`/`rename()`/
|
||||
`chmod()` on a full path string. The principle is: **trust the operator-named
|
||||
transfer root, and confine all resolution beneath it**, rejecting escapes via
|
||||
`..` above the anchor, absolute symlinks, or out-of-tree symlinks.
|
||||
`secure_relative_open()` resolves the parent directory by walking it one
|
||||
component at a time on a stack of held directory fds, then operates on the final
|
||||
component with an at-style call on the resulting directory fd.
|
||||
|
||||
For per-entry work the receiver and generator go one step further and hold the
|
||||
parent directory open: `open_dir_secure()` resolves an entry's directory once
|
||||
(via `secure_relative_open()`), `held_dfd_for()` caches that descriptor for the
|
||||
duration of the entry, and every operation on the entry — `lstat`, the temp-file
|
||||
`mkstemp`, the temp->final `rename`, `chmod`/`chown`/`utimes`, `mkdir`, special-
|
||||
file and symlink creation, the delta-basis open, and the recursive delete — runs
|
||||
through that one held fd via an `*at()` call (`do_*_atfd()`). Because the
|
||||
descriptor is pinned to the directory inode, a parent component flipped to a
|
||||
symlink *after* the open cannot redirect any of those operations. The alternate-
|
||||
destination lookups are confined the same way (`basis_link_stat()` in
|
||||
`generator.c` and `secure_basis_open()` in `receiver.c`), so a peer-chosen
|
||||
`--link-dest`/`--compare-dest`/`--copy-dest` basis index cannot reach an
|
||||
out-of-module file through a symlinked parent.
|
||||
|
||||
The sender's source-directory *enumeration* is confined the same way as its
|
||||
content open. `send_directory()` opens each scanned directory through
|
||||
`secure_opendir()` — which resolves it via `secure_relative_open()` /
|
||||
`secure_relative_open_at()` and turns the held fd into the `DIR*` with
|
||||
`fdopendir()` — so a parent component raced into a symlink, or (for a daemon
|
||||
following mode) an in-module symlink pointing outside the module, cannot redirect
|
||||
the scan to enumerate an out-of-tree directory and leak its entry names, metadata
|
||||
and symlink targets. For a daemon, both the enumeration and the content open
|
||||
anchor at the served module root **pinned by identity**: `module_dirfd` is opened
|
||||
(`open(".")`) the moment the daemon `chdir`s into the module, while still
|
||||
privileged, and module-relative paths resolve beneath that fd via
|
||||
`secure_relative_open_at()`. Anchoring at the held fd rather than re-resolving the
|
||||
absolute module path keeps the confinement working after the daemon drops to the
|
||||
module uid even when the module sits under a directory that uid cannot traverse
|
||||
(e.g. a `0700` home — re-resolving the absolute path would `EACCES`), and is
|
||||
immune to the logical-path-versus-real-cwd skew a followed in-tree directory
|
||||
symlink would otherwise introduce.
|
||||
|
||||
### Path resolution
|
||||
|
||||
`secure_relative_open()` resolves a path with a single portable mechanism on
|
||||
every platform: a per-component walk on a stack of held directory fds. Each
|
||||
component is opened relative to the held parent with `openat(parent_fd,
|
||||
"component", O_NOFOLLOW)`; descending into a real subdirectory pushes its fd, a
|
||||
`..` pops back to the already-held parent (a pop at the anchor is refused), and an
|
||||
in-tree directory symlink is followed by reading its target and walking that off
|
||||
the same stack (absolute targets refused, symlink hops bounded). The final
|
||||
component is opened `O_NOFOLLOW`.
|
||||
|
||||
Because every component is opened relative to a *pinned* fd under `O_NOFOLLOW`,
|
||||
and `..` is resolved by the held-fd stack rather than by the kernel, the walk is
|
||||
race-free by construction: no rename or symlink swap of any path name can redirect
|
||||
resolution outside the anchor subtree, and no kernel "beneath" primitive
|
||||
(`openat2(RESOLVE_BENEATH)` / `openat(O_RESOLVE_BENEATH)`) is required. The
|
||||
confinement is therefore uniform across Linux, the BSDs, macOS and
|
||||
Solaris/illumos, on old and new kernels alike, with nothing to probe or fall back
|
||||
to at runtime (and so no `openat2`/seccomp interaction to worry about in sandboxed
|
||||
environments). Cygwin is the exception, because its directory descriptors and
|
||||
symlink emulation do not give the held-fd walk the same inode pinning — see the
|
||||
Cygwin residual below.
|
||||
|
||||
Legitimate *in-tree* directory symlinks are followed, so `--keep-dirlinks` /
|
||||
`--copy-links` and a symlinked module path keep working. A relative alternate-dest
|
||||
such as `--compare-dest=../01` may legitimately climb to a sibling still inside the
|
||||
module; such a `..` path is re-anchored at the module root and its in-module climb
|
||||
adjudicated by the walk (the `..` pops to the held parent), while escapes above the
|
||||
anchor are still rejected.
|
||||
|
||||
### Leaf operations
|
||||
|
||||
The final operation is hardened as well, following `cp`: reads use `O_NOFOLLOW`
|
||||
so a flipped leaf symlink is not followed, and new or destination files are
|
||||
created with `O_CREAT|O_EXCL` (rsync's temporary files use `mkstemp`) so a
|
||||
planted symlink at the target cannot be written through. A leaf `chmod` is the
|
||||
one operation with no portable no-follow form: it is closed by opening the leaf
|
||||
`O_RDONLY|O_NOFOLLOW` and `fchmod`-ing the held fd (refusing a symlink leaf with
|
||||
`ELOOP`), falling back to `fchmodat(AT_SYMLINK_NOFOLLOW)` and then the
|
||||
`fchmodat2()` syscall, and failing closed with a warning rather than ever
|
||||
chmod-ing through a raced leaf symlink.
|
||||
|
||||
### Guidance for contributors
|
||||
|
||||
* When adding code that performs a path-based syscall on a path that can be
|
||||
influenced by the remote peer or by another local user, use a `do_*_at()`
|
||||
wrapper (or `secure_relative_open()`), not a raw full-path syscall.
|
||||
* When introducing a new operation, add a matching `do_<op>_at()` wrapper that
|
||||
resolves the parent with `secure_relative_open()` and acts via an at-style call
|
||||
on the returned dirfd.
|
||||
* Do not assume a non-daemon transfer is safe; the question is whether rsync has
|
||||
more authority than whoever controls the path components.
|
||||
* On platforms whose API lacks an at-style equivalent (e.g. `setattrlist()`),
|
||||
follow the residuals policy at the top of this document: for a metadata
|
||||
operation on the already-transferred object (ACLs, xattrs, crtimes, permissions)
|
||||
fall back to the path-based call to keep the feature functional and document the
|
||||
residual; but where the unsafe fallback would *create a new object on an
|
||||
unconfined path* (the nested-socket `bind()` case), refuse it instead — that is
|
||||
an out-of-tree write/create primitive, not a same-object metadata race, and the
|
||||
lost functionality is negligible.
|
||||
|
||||
|
||||
## Symlink defense for operator-supplied paths
|
||||
|
||||
rsync opens several operator-supplied paths during normal operation. These fall
|
||||
into two groups, both governed by the same ownership-walk policy below:
|
||||
|
||||
* operator **files**: `--log-file`, `--password-file`, `--early-input` (a client
|
||||
read whose contents are forwarded to the daemon's early-exec), `--files-from`,
|
||||
`--include-from`, `--exclude-from`, `--filter=. file`, `--write-batch`,
|
||||
`--read-batch`, per-directory filter merge files (`-C` / `-F` / `dir-merge`),
|
||||
and on the daemon side `motd file =`, `secrets file =`, `lock file =`, and
|
||||
`rsyncd.conf` itself.
|
||||
* operator **directories**: `--backup-dir`, `--temp-dir`/`-T`, `--partial-dir`,
|
||||
and the `--link-dest`/`--compare-dest`/`--copy-dest` basis lookup. These take
|
||||
a directory the operator chose, which may legitimately point outside the
|
||||
transfer tree (`--backup-dir=/var/backups`), so they are resolved with the
|
||||
ownership walk rather than the strict transfer-path resolver.
|
||||
|
||||
The daemon module-root `chdir()` under
|
||||
`use chroot = no` and the non-daemon receiver's `chdir()` into the
|
||||
operator-named destination directory are in the same class: both follow
|
||||
the operator's/root's own symlinked target (the `/backup -> /mnt/disk`
|
||||
admin pattern) but refuse one an attacker raced in from another uid,
|
||||
unless `--insecure-links` restores the legacy plain `chdir()`.
|
||||
|
||||
Each of these reads or writes a path the operator or sender chose, which
|
||||
may transit attacker-influenceable parent directories (the `/tmp/somedir/`
|
||||
class) or be planted directly (the `/home/$user/.cvsignore` class when
|
||||
root runs `rsync -a /home /backup`).
|
||||
|
||||
rsync's defense, applied uniformly to all of the above, is a
|
||||
component-by-component path walk (`open_no_attacker_symlinks` in
|
||||
`util1.c`) that allows symlinks **only** when the symlink itself is owned
|
||||
by uid 0 or the running process's effective uid. Symlinks owned by any
|
||||
other uid are refused with `ELOOP` at any path component (parent or leaf).
|
||||
Plain `O_NOFOLLOW` would be leaf-only and would not defend the
|
||||
`/tmp/somedir/log` parent-component plant; this walk does.
|
||||
|
||||
The trust model preserves legitimate setups such as `/var/log -> /data/log`
|
||||
(root-owned dir-symlink) and a non-root user's own `~/log -> /data/me`
|
||||
symlink; it refuses an attacker's `/tmp/somedir -> /attack/path` plant.
|
||||
For `--read-batch` an additional `fstat()` check refuses non-regular
|
||||
files (FIFOs, devices) at the batch path, since the batch content drives
|
||||
the receiver's protocol parser.
|
||||
|
||||
**Policy.** A symlink at **any** path component (parent or leaf) is **followed
|
||||
iff it is owned by uid 0 or the process's effective uid, and refused (`ELOOP`)
|
||||
otherwise**, identically for **absolute and relative** operator paths. The trust
|
||||
signal is **authority (ownership)**, not **location**: an operator path may
|
||||
legitimately point outside the transfer tree, so it cannot be confined by
|
||||
location the way a transfer path is. This is deliberately distinct from the
|
||||
transfer-path resolver `secure_relative_open()` (see *Symlink-race-safe path
|
||||
resolution* above), which refuses **all** symlinks and anchors **beneath the
|
||||
transfer root** — correct for peer-named paths, which never legitimately escape.
|
||||
For the operator directory paths, a refused symlink simply makes the target look
|
||||
absent (no backup/temp/basis is taken through it) and the transfer proceeds
|
||||
normally; the operator's own symlinked target keeps working.
|
||||
|
||||
**The daemon `exclude`/`filter` chain is not a symlink boundary.** The daemon
|
||||
filter chain (`exclude`, `exclude from`, `filter`, …) matches the *logical*
|
||||
module-relative **name** of each item, not the physical file it resolves to. It
|
||||
is a visibility/tamper filter — a peer cannot *name* a daemon-excluded path to
|
||||
pull, push to, or delete it — but it is **not** a security boundary against
|
||||
symlinks: an in-module symlink whose own name is not excluded can be followed to
|
||||
an excluded target (the name the filter sees, e.g. `link`, is not the excluded
|
||||
name, e.g. `secret`). This is by design and is the long-standing behaviour of
|
||||
stock rsync; the defense for a writable module against symlink trickery is
|
||||
`munge symlinks` (enabled by default for a writable, non-chrooted module), **not**
|
||||
the filter. Do not rely on `exclude`/`filter` to confine a peer who can introduce
|
||||
or traverse a symlink; see `rsyncd.conf(5)` ("filter" and "munge symlinks").
|
||||
|
||||
What *is* enforced for a *peer-supplied* operator path (`--partial-dir`,
|
||||
`--backup-dir`, the alt-dest basis) is confinement to the **module root**: the
|
||||
ownership walk refuses a foreign-uid symlink (the symlink-race defense) and
|
||||
refuses a resolved target *outside* the module. That module-boundary confinement
|
||||
is independent of `exclude`/`filter` — it holds whether or not the module sets an
|
||||
exclude — and is what the operator-path tests cover.
|
||||
|
||||
**`--insecure-links`.** This flag is a **local** opt-out that restores the legacy
|
||||
follow-any-symlink behaviour for the paths above. It is **not forwarded** to the
|
||||
remote (a remote-shell peer that wants the opt-out must set it on its own side,
|
||||
e.g. via `--rsync-path`), and a **daemon never honors it**: the opt-out predicate
|
||||
reads the client-controllable flag only off a daemon, so a peer-forwarded or
|
||||
`-M`-injected `--insecure-links` cannot weaken a daemon's confinement — the daemon
|
||||
additionally hard-refuses it (drops the connection) via the refused-options path.
|
||||
A daemon admin who wants the legacy behaviour for one isolated/trusted module
|
||||
sets `insecure links = yes` in that module's `rsyncd.conf` stanza (see
|
||||
`rsyncd.conf(5)`); this is admin-only and re-opens the symlink-escape
|
||||
vulnerabilities for that module on purpose. The `operator-path-*` and
|
||||
`insecure-links-*` tests enforce this consistency across every path-taking
|
||||
option and across absolute/relative, leaf/parent, and same-uid/cross-uid plants.
|
||||
|
||||
For `support/rrsync` (the SSH-restricted-rsync wrapper), the same TOCTOU
|
||||
class is closed in Python by opening each validated path component with
|
||||
`O_RDONLY|O_NOFOLLOW`, verifying via `readlink('/proc/self/fd/N')` that the
|
||||
pinned inode is still in-tree, and passing `/proc/self/fd/N` as the exec'd
|
||||
rsync's argument (so the kernel routes the child's open through the pinned
|
||||
inode rather than re-resolving the path). A receiver-side new destination
|
||||
has no inode of its own yet, so its existing parent directory is pinned the
|
||||
same way and the leaf is created at `/proc/self/fd/<parent>/<leaf>`. This pin
|
||||
relies on an fdescfs-style magic symlink and is not available on every
|
||||
platform -- see the rrsync residual below.
|
||||
|
||||
### Known residuals
|
||||
|
||||
The following are documented as out of scope for this release:
|
||||
|
||||
* The source-directory *enumeration* confinement needs `fdopendir()` (to form a
|
||||
`DIR*` from the securely-resolved held fd) and `dirfd()`; on a platform lacking
|
||||
either, `send_directory()` falls back to the legacy `opendir()` on the path, so
|
||||
the scan is unconfined there — the same resolver-fallback shape as the other
|
||||
`*at()`-less residuals. Every current target provides both; the per-entry
|
||||
operations and the content open remain confined regardless.
|
||||
|
||||
* On **Cygwin**, the per-component held-fd walk does not provide the same
|
||||
inode-pinning guarantee as on a POSIX kernel: Cygwin tracks a process's
|
||||
current directory and resolves directory descriptors by path name rather than
|
||||
by a pinned inode, and emulates symlinks as special files. Static out-of-tree
|
||||
symlinks are still refused (the walk sees and rejects them), and a daemon
|
||||
module path anchored at an absolute `module_dir` is confined; but an entry
|
||||
whose parent component is *raced* from a directory to a symlink mid-resolution
|
||||
can still slip past confinement that is anchored at the process CWD (e.g. the
|
||||
sender's content open), because the descriptor is not bound to the original
|
||||
inode. The parent-component symlink-race tests are therefore not enforced on
|
||||
Cygwin (see `RSYNC_EXPECT_SKIPPED` in `.github/workflows/cygwin-build.yml` and
|
||||
the Cygwin-only xfail in `symlink-race-source_test.py`). Cygwin is a
|
||||
development/interoperability target, not a privilege boundary host, so this is
|
||||
accepted for this release.
|
||||
|
||||
* On a platform with no `mknodat()` at all -- macOS before 13 is the
|
||||
supported example, where `mknod()` and `mkfifo()` exist but neither
|
||||
`mknodat()` nor `mkfifoat()` does -- creating a device node or FIFO
|
||||
falls back to plain `do_mknod()`, which resolves the whole path by name.
|
||||
What is lost is the *pinned parent*: the directory components are
|
||||
re-resolved by the kernel at create time, so an attacker who can swap a
|
||||
parent component races the create and can place the node outside the
|
||||
transfer. The final component is not at risk -- `mknod()` and
|
||||
`mkfifo()` do not follow a symlink at the leaf, they fail `EEXIST`.
|
||||
Where `AT_FDCWD` exists -- which is every platform rsync 3.5.0 supports,
|
||||
macOS 10.13 included -- fake-super placeholders still return through
|
||||
`openat(..., O_NOFOLLOW)`, reached before either `*at` primitive is
|
||||
tested, so ordinary in-tree placeholder creation stays confined;
|
||||
fake-super loses parent confinement and the `O_NOFOLLOW` leaf only on the
|
||||
paths that reach plain `do_mknod()` (the cache-declined/cross-tree
|
||||
wrapper and the backup paths). On a build with no `AT_FDCWD` at all
|
||||
there is no fd-relative primitive of any kind, so nothing above applies
|
||||
and every special-file create, fake-super included, is unconfined. Transferring specials there (`--devices`, `--specials`) carries
|
||||
the parent-component race. `symlink-mknod-fakesuper-symlink-race` skips
|
||||
itself on such a build, since the property it asserts is one the build
|
||||
deliberately does not have.
|
||||
|
||||
* On platforms where `mknod()`/`mknodat()` cannot create a socket inode
|
||||
(the BSDs, macOS, Solaris), a transferred socket is recreated with
|
||||
`socket()` + `unlink` + `bind(path)`, which cannot be confined (there is
|
||||
no portable `bindat()`). Linux creates it race-safely with `mknodat()`
|
||||
on a held dirfd; on the others a *nested* socket is skipped with a
|
||||
warning rather than bound on an unconfined path, leaving only a
|
||||
top-level, operator-named socket binding by path.
|
||||
|
||||
* `support/rrsync`'s race-free inode-pin -- of both existing path
|
||||
components and a new destination's parent -- depends on materialising a
|
||||
held fd as a path that the exec'd rsync re-resolves to the same inode.
|
||||
rrsync validates and pins in its own process, but it then *exec*s a
|
||||
separate rsync that re-resolves the paths from `argv`, so the confining
|
||||
reference must be expressible as an argument. A held dirfd is not: it is
|
||||
usable as a path only through an fdescfs-style magic symlink. rrsync
|
||||
implements this for Linux only, via `/proc/self/fd/N`; it does not use the
|
||||
`/dev/fd/N` equivalent that macOS/FreeBSD expose with `fdescfs` mounted. So on
|
||||
every non-Linux platform (the BSDs, macOS, Solaris -- whose `/proc/self/fd`
|
||||
entries are not magic symlinks -- and Cygwin), and on a `/proc`-less Linux
|
||||
namespace, rrsync falls through to the realpath-validated path unpinned,
|
||||
so a parent-component or between-pin-and-exec flip remains possible there;
|
||||
a deeper `-R` new path whose parent does not exist yet is likewise
|
||||
unpinned. The portable closure is an rsync-side fd-passing API -- rrsync
|
||||
hands rsync the confined dirfd (inherited across `exec`) and rsync
|
||||
resolves that argument relative to it with the same `secure_relative_open`
|
||||
resolver the daemon uses, needing no magic-symlink filesystem -- a
|
||||
protocol/CLI addition under discussion on the rsync-security list.
|
||||
|
||||
* The operator-directory ownership walk refuses a foreign-owned symlink on a
|
||||
`--backup-dir`/`--temp-dir`/`--partial-dir`/`--link/compare/copy-dest` path, so
|
||||
a *statically planted* symlink is rejected and the dependent operation does not
|
||||
escape. Both the data writes and the *source-metadata reads* of those
|
||||
operations are now confined to held no-follow fds: the `--copy-dest`
|
||||
`copy_file()`/`copy_xattrs()` source read goes through the held basis content fd
|
||||
(`sys_fgetxattr`), and `make_backup()` reads the backed-up file's ACL/xattrs
|
||||
through a `backup_source_fd()`-pinned fd -- so a parent-component flip can no
|
||||
longer redirect them to disclose an out-of-module value. The cross-tree
|
||||
metadata *apply* on those leaves (the `%stat`/ACL/xattr write on a
|
||||
`--temp-dir`/`--backup-dir` staging file) is fd-pinned the same way, now
|
||||
including under `--fake-super`: the `set_file_attrs()` no-follow leaf fd was
|
||||
previously opened only when `am_root >= 0`, so a `fake super = yes` daemon fell
|
||||
back to a path-based `sys_lsetxattr()`/chmod a raced parent could redirect; the
|
||||
pin is now opened for fake-super too (a raced leaf is refused, not redirected).
|
||||
Two narrow follow-ons
|
||||
re-resolve the (now-validated) operator path by name and remain a
|
||||
*post-validation* parent-component race:
|
||||
* the in-place backup (`--inplace --backup`) writes the backup file's data
|
||||
through a confined create, but its `set_file_attrs()` metadata set
|
||||
(chmod/chown/times) re-resolves the `--backup-dir` path by name afterwards
|
||||
(it is not placed under operator mode, which would force the shared
|
||||
`set_file_attrs()` path off its held-O_NOFOLLOW-fd xattr write and re-open
|
||||
the very parent-symlink xattr race `copy-xattrs-symlink-race` pins closed); and
|
||||
* the abbreviated-xattr optimisation reuses a basis xattr value for the
|
||||
destination only when its checksum matches the digest the sender sent; that
|
||||
basis read (`rsync_xal_set()`) re-resolves the basis path by name. This is a
|
||||
*constrained checksum-oracle*, not a disclosure: it confirms that some raced
|
||||
out-of-module xattr hashes to a value the sender already chose, rather than
|
||||
copying an unknown value onto a readable file, and needs a colluding sender
|
||||
plus a local racer.
|
||||
An attacker who flips a parent component in the window *after* the confined data
|
||||
write/stat can thus still affect those narrow metadata/oracle operations. This
|
||||
is the same local-attacker post-confinement TOCTOU class as the ACL/crtimes
|
||||
residuals below; the data-write and direct source-read escapes are closed, and
|
||||
`--insecure-links` (or a module's `insecure links = yes`) is orthogonal to it.
|
||||
|
||||
* POSIX ACL application (`-A`/`--acls`) is race-safe on every Linux kernel —
|
||||
6.13+ via the `*xattrat` syscalls (or a patched libacl's `*_at` bindings), and
|
||||
older kernels via the `/proc/self/fd` compat that pins the same inode, provided
|
||||
`procfs` is mounted — and a transferred file/dir/FIFO has its xattrs (`-X`)
|
||||
applied through the held no-follow fd, so the apply cannot be redirected by a
|
||||
raced parent component. Where neither primitive is available — the BSDs,
|
||||
Solaris and macOS (no `*xattrat` syscalls and no `/proc/self/fd` magic
|
||||
symlinks), plus the edge case of a Linux instance with no usable `/proc` (a
|
||||
`/proc`-less container/namespace) — the ACL apply falls back to the path-based
|
||||
`acl_set_file()` /
|
||||
`sys_acl_*file()` calls — the long-standing 3.4.x behaviour — to keep `--acls`
|
||||
functional rather than silently skipping it, so a parent-component flip can
|
||||
have the received ACL written onto an object outside the module/destination
|
||||
boundary (and, because the attacker controls the ACL bytes, granted to a chosen
|
||||
uid). As with the macOS crtime tier below, this is an accepted residual under
|
||||
the functionality-over-refusal policy; a daemon operator who does not want it
|
||||
can disable the feature with `refuse options = acls`.
|
||||
|
||||
* macOS creation-time (`--crtimes`) preservation uses the path-based
|
||||
`setattrlist()`/`getattrlist()` with `FSOPT_NOFOLLOW`, which protects only
|
||||
the final component; there is no `setattrlistat()` targeting
|
||||
`ATTR_CMN_CRTIME`. As with POSIX ACLs where the OS offers no race-safe
|
||||
primitive, `--crtimes` is kept functional (daemon and non-daemon) and the
|
||||
parent-component symlink race is an accepted residual: an attacker who
|
||||
flips a parent component can have a crtime read/write target an object
|
||||
outside the module/destination boundary. The mtime/atime path is *not*
|
||||
affected -- `set_times()` resolves it race-safely through `utimensat()` on a
|
||||
held dirfd in hardened mode. A daemon operator who does not want the crtime
|
||||
residual can disable the feature with `refuse options = crtimes` in
|
||||
`rsyncd.conf`.
|
||||
|
||||
* Pulling with `-o`/`-g` (or `-a`) **as root from an untrusted sender** is by
|
||||
design a trust relationship, not a confinement boundary: the sender dictates
|
||||
each received file's owner/group, including uid/gid 0. rsync maps the
|
||||
sender's id/name pairs through the local id database; an empty or unknown
|
||||
sender name falls back to the sender's numeric id (the value `--numeric-ids`
|
||||
would use), and a sender can equally request root via the literal name
|
||||
`root`. A root receiver must therefore only pull with `-o`/`-g` from a
|
||||
trusted source (or use a non-root receiver / a uid-gid policy). The daemon
|
||||
*name-converter* path is guarded separately — an unknown name there maps to
|
||||
the sender's numeric id rather than 0 (see `clientserver.c`).
|
||||
|
||||
## Daemon authentication digest
|
||||
|
||||
Daemon authentication is a secret-prefix challenge-response: the client returns
|
||||
`base64(H(secret || challenge))`, where `H` is a digest the two sides negotiate.
|
||||
The negotiation is unauthenticated and ordered by the connecting side, and the
|
||||
`md5`/`md4` digests remain available for backward compatibility, so a peer that
|
||||
sends no digest list (any rsync before 3.2.0, including the openrsync that ships
|
||||
with macOS) falls back to `md5` (or `md4` below protocol 30), and an on-path
|
||||
attacker can rewrite the negotiation to force `md5`/`md4` even between two modern
|
||||
peers. This is **not** an authentication bypass — `md4`/`md5` have no practical
|
||||
preimage break — but a weak digest makes a *captured* `(challenge, response)`
|
||||
pair far cheaper to brute-force offline, recovering a guessable shared secret.
|
||||
|
||||
The challenge itself is seeded from the kernel CSPRNG (`/dev/urandom`), so it is
|
||||
an unpredictable per-connection nonce. An earlier time/pid-based challenge was
|
||||
low-entropy enough (~35 bits) that recovering the `(sec, usec, pid)` tuple from
|
||||
one observed challenge let an on-path observer predict every subsequent challenge
|
||||
from that daemon process and pre-compute a dictionary against a captured
|
||||
response. (If `/dev/urandom` is unavailable the daemon logs a warning and falls
|
||||
back to the legacy time-based challenge rather than a constant.)
|
||||
|
||||
A daemon operator whose clients are all modern (rsync 3.2.7+ built with openssl,
|
||||
when the SHA digests were added) can require a strong digest with the `auth
|
||||
digest` module parameter, e.g. `auth digest = sha256`, which refuses any
|
||||
connection that negotiates — or falls back to — a weaker digest (see
|
||||
`rsyncd.conf`).
|
||||
|
||||
Residual: there is **no default floor**, because requiring one would break every
|
||||
pre-3.2.0 client (notably the macOS-bundled openrsync, which authenticates only
|
||||
with `md4`). An operator who cannot raise the floor should run the daemon behind
|
||||
a verified TLS transport (`rsync-ssl`/stunnel) or over ssh — which removes the
|
||||
on-path capture/downgrade vector at the transport layer — and should use a
|
||||
high-entropy shared secret, which is infeasible to brute-force regardless of the
|
||||
digest.
|
||||
@@ -15,7 +15,6 @@ Create more granular verbosity 2003/05/15
|
||||
|
||||
DOCUMENTATION --------------------------------------------------------
|
||||
Keep list of open issues and todos on the web site
|
||||
Perhaps redo manual as SGML
|
||||
|
||||
LOGGING --------------------------------------------------------------
|
||||
Memory accounting
|
||||
@@ -213,16 +212,6 @@ DOCUMENTATION --------------------------------------------------------
|
||||
|
||||
Keep list of open issues and todos on the web site
|
||||
|
||||
-- --
|
||||
|
||||
|
||||
Perhaps redo manual as SGML
|
||||
|
||||
The man page is getting rather large, and there is more information
|
||||
that ought to be added.
|
||||
|
||||
TexInfo source is probably a dying format.
|
||||
|
||||
Linuxdoc looks like the most likely contender. I know DocBook is
|
||||
favoured by some people, but it's so bloody verbose, even with emacs
|
||||
support.
|
||||
|
||||
@@ -28,7 +28,7 @@ static int allow_forward_dns;
|
||||
|
||||
extern const char undetermined_hostname[];
|
||||
|
||||
static int match_hostname(const char **host_ptr, const char *addr, const char *tok)
|
||||
static int match_hostname(const char **host_ptr, const char *addr, const char *tok, int deny)
|
||||
{
|
||||
struct hostent *hp;
|
||||
unsigned int i;
|
||||
@@ -54,8 +54,14 @@ static int match_hostname(const char **host_ptr, const char *addr, const char *t
|
||||
return 0;
|
||||
|
||||
/* Now try forward-DNS on the token (config-specified hostname) and see if the IP matches. */
|
||||
if (!(hp = gethostbyname(tok)))
|
||||
return 0;
|
||||
if (!(hp = gethostbyname(tok))) {
|
||||
/* A deny-list hostname token we cannot resolve must fail CLOSED:
|
||||
* we can't prove the peer isn't the denied host, so treat the
|
||||
* unresolvable token as a match (deny). Allow-list tokens keep
|
||||
* failing as a non-match. Sibling of CVE-2026-43617, which fixed
|
||||
* only the reverse-lookup path. */
|
||||
return deny;
|
||||
}
|
||||
|
||||
for (i = 0; hp->h_addr_list[i] != NULL; i++) {
|
||||
if (strcmp(addr, inet_ntoa(*(struct in_addr*)(hp->h_addr_list[i]))) == 0) {
|
||||
@@ -243,7 +249,7 @@ static int match_address(const char *addr, char *tok)
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int access_match(const char *list, const char *addr, const char **host_ptr)
|
||||
static int access_match(const char *list, const char *addr, const char **host_ptr, int deny)
|
||||
{
|
||||
char *tok;
|
||||
char *list2 = strdup(list);
|
||||
@@ -251,7 +257,7 @@ static int access_match(const char *list, const char *addr, const char **host_pt
|
||||
strlower(list2);
|
||||
|
||||
for (tok = strtok(list2, " ,\t"); tok; tok = strtok(NULL, " ,\t")) {
|
||||
if (match_hostname(host_ptr, addr, tok) || match_address(addr, tok)) {
|
||||
if (match_hostname(host_ptr, addr, tok, deny) || match_address(addr, tok)) {
|
||||
free(list2);
|
||||
return 1;
|
||||
}
|
||||
@@ -275,7 +281,7 @@ int allow_access(const char *addr, const char **host_ptr, int i)
|
||||
|
||||
/* If we match an allow-list item, we always allow access. */
|
||||
if (allow_list) {
|
||||
if (access_match(allow_list, addr, host_ptr))
|
||||
if (access_match(allow_list, addr, host_ptr, 0))
|
||||
return 1;
|
||||
/* For an allow-list w/o a deny-list, disallow non-matches. */
|
||||
if (!deny_list)
|
||||
@@ -284,9 +290,17 @@ int allow_access(const char *addr, const char **host_ptr, int i)
|
||||
|
||||
/* If we match a deny-list item (and got past any allow-list
|
||||
* items), we always disallow access. */
|
||||
if (deny_list && access_match(deny_list, addr, host_ptr))
|
||||
if (deny_list && access_match(deny_list, addr, host_ptr, 1))
|
||||
return 0;
|
||||
|
||||
/* Allow all other access. */
|
||||
return 1;
|
||||
}
|
||||
|
||||
int allow_proxy_protocol_peer(const char *list, const char *addr, const char **host_ptr)
|
||||
{
|
||||
if (!list || !*list)
|
||||
return 0;
|
||||
allow_forward_dns = 0;
|
||||
return access_match(list, addr, host_ptr, 0);
|
||||
}
|
||||
@@ -21,6 +21,10 @@
|
||||
|
||||
#include "rsync.h"
|
||||
#include "lib/sysacls.h"
|
||||
#include "lib/acl.h"
|
||||
#ifdef HAVE_LIBACL_AT
|
||||
#include <fcntl.h> /* AT_EMPTY_PATH / AT_SYMLINK_NOFOLLOW */
|
||||
#endif
|
||||
|
||||
#ifdef SUPPORT_ACLS
|
||||
|
||||
@@ -469,11 +473,129 @@ static int find_matching_rsync_acl(const rsync_acl *racl, SMB_ACL_TYPE_T type,
|
||||
return *match;
|
||||
}
|
||||
|
||||
static int get_rsync_acl(const char *fname, rsync_acl *racl,
|
||||
SMB_ACL_TYPE_T type, mode_t mode)
|
||||
/* These two bridge lib/acl.c's neutral (tag,perm,id) entry array; with
|
||||
* HAVE_LIBACL_AT the libacl *_at path uses unpack_smb_acl/pack_smb_acl directly,
|
||||
* so they are unused there. */
|
||||
#if defined(SUPPORT_ACL_FD) && !defined(HAVE_LIBACL_AT)
|
||||
/* Convert a packed system ACL into the neutral (tag,perm,id) entry array that
|
||||
* lib/acl.c serializes. Reuses pack_smb_acl()+change_sacl_perms() output so
|
||||
* the bytes we write match exactly what acl_set_file() would have written.
|
||||
* Returns the entry count and a malloc'd array in *ents_p, or -1 on error. */
|
||||
static int sacl_to_entries(SMB_ACL_T sacl, rsync_acl_ent **ents_p)
|
||||
{
|
||||
static item_list ent_list = EMPTY_ITEM_LIST;
|
||||
SMB_ACL_ENTRY_T entry;
|
||||
rsync_acl_ent *out;
|
||||
int rc;
|
||||
|
||||
ent_list.count = 0;
|
||||
for (rc = sys_acl_get_entry(sacl, SMB_ACL_FIRST_ENTRY, &entry); rc == 1;
|
||||
rc = sys_acl_get_entry(sacl, SMB_ACL_NEXT_ENTRY, &entry)) {
|
||||
SMB_ACL_TAG_T tag_type;
|
||||
uint32 access;
|
||||
id_t g_u_id;
|
||||
rsync_acl_ent *e;
|
||||
uint16_t tag;
|
||||
|
||||
if ((rc = sys_acl_get_info(entry, &tag_type, &access, &g_u_id)) != 0)
|
||||
break;
|
||||
switch (tag_type) {
|
||||
case SMB_ACL_USER_OBJ: tag = RACL_USER_OBJ; break;
|
||||
case SMB_ACL_USER: tag = RACL_USER; break;
|
||||
case SMB_ACL_GROUP_OBJ: tag = RACL_GROUP_OBJ; break;
|
||||
case SMB_ACL_GROUP: tag = RACL_GROUP; break;
|
||||
case SMB_ACL_MASK: tag = RACL_MASK; break;
|
||||
case SMB_ACL_OTHER: tag = RACL_OTHER; break;
|
||||
default: continue; /* skip an unrecognized tag */
|
||||
}
|
||||
e = EXPAND_ITEM_LIST(&ent_list, rsync_acl_ent, -10);
|
||||
e->tag = tag;
|
||||
e->perm = access & 7;
|
||||
e->id = (tag == RACL_USER || tag == RACL_GROUP) ? (uint32_t)g_u_id : RACL_UNDEFINED_ID;
|
||||
}
|
||||
if (rc) {
|
||||
rsyserr(FERROR_XFER, errno, "sacl_to_entries: sys_acl_get_entry/info()");
|
||||
return -1;
|
||||
}
|
||||
|
||||
out = new_array(rsync_acl_ent, ent_list.count ? ent_list.count : 1);
|
||||
if (ent_list.count)
|
||||
memcpy(out, ent_list.items, ent_list.count * sizeof (rsync_acl_ent));
|
||||
*ents_p = out;
|
||||
return ent_list.count;
|
||||
}
|
||||
|
||||
/* Unpack a neutral entry array (from lib/acl.c) into an rsync_acl, mirroring
|
||||
* unpack_smb_acl()'s tag handling. */
|
||||
static BOOL unpack_acl_entries(const rsync_acl_ent *ents, int n, rsync_acl *racl)
|
||||
{
|
||||
static item_list temp_ida_list = EMPTY_ITEM_LIST;
|
||||
int i;
|
||||
|
||||
temp_ida_list.count = 0;
|
||||
for (i = 0; i < n; i++) {
|
||||
uint32 access = ents[i].perm & 7;
|
||||
id_access *ida;
|
||||
|
||||
switch (ents[i].tag) {
|
||||
case RACL_USER_OBJ:
|
||||
if (racl->user_obj == NO_ENTRY)
|
||||
racl->user_obj = access;
|
||||
continue;
|
||||
case RACL_GROUP_OBJ:
|
||||
if (racl->group_obj == NO_ENTRY)
|
||||
racl->group_obj = access;
|
||||
continue;
|
||||
case RACL_MASK:
|
||||
if (racl->mask_obj == NO_ENTRY)
|
||||
racl->mask_obj = access;
|
||||
continue;
|
||||
case RACL_OTHER:
|
||||
if (racl->other_obj == NO_ENTRY)
|
||||
racl->other_obj = access;
|
||||
continue;
|
||||
case RACL_USER:
|
||||
access |= NAME_IS_USER;
|
||||
break;
|
||||
case RACL_GROUP:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
ida = EXPAND_ITEM_LIST(&temp_ida_list, id_access, -10);
|
||||
ida->id = ents[i].id;
|
||||
ida->access = access;
|
||||
}
|
||||
|
||||
if (temp_ida_list.count) {
|
||||
#ifdef SMB_ACL_NEED_SORT
|
||||
if (temp_ida_list.count > 1)
|
||||
qsort(temp_ida_list.items, temp_ida_list.count, sizeof (id_access), id_access_sorter);
|
||||
#endif
|
||||
racl->names.idas = new_array(id_access, temp_ida_list.count);
|
||||
memcpy(racl->names.idas, temp_ida_list.items, temp_ida_list.count * sizeof (id_access));
|
||||
} else
|
||||
racl->names.idas = NULL;
|
||||
racl->names.count = temp_ida_list.count;
|
||||
temp_ida_list.count = 0;
|
||||
|
||||
return True;
|
||||
}
|
||||
#endif /* SUPPORT_ACL_FD */
|
||||
|
||||
static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
|
||||
rsync_acl *racl, SMB_ACL_TYPE_T type, mode_t mode)
|
||||
{
|
||||
SMB_ACL_T sacl;
|
||||
|
||||
#ifndef SUPPORT_ACL_FD
|
||||
#ifndef HAVE_SOLARIS_ACLS
|
||||
(void)fd; /* Solaris drives the ACL via facl(2) on fd but has no SUPPORT_ACL_FD. */
|
||||
#endif
|
||||
(void)dirfd;
|
||||
(void)leaf;
|
||||
#endif
|
||||
|
||||
#ifdef SUPPORT_XATTRS
|
||||
/* --fake-super support: load ACLs from an xattr. */
|
||||
if (am_root < 0) {
|
||||
@@ -481,7 +603,7 @@ static int get_rsync_acl(const char *fname, rsync_acl *racl,
|
||||
size_t len;
|
||||
int cnt;
|
||||
|
||||
if ((buf = get_xattr_acl(fname, type == SMB_ACL_TYPE_ACCESS, &len)) == NULL)
|
||||
if ((buf = get_xattr_acl(fname, fd, type == SMB_ACL_TYPE_ACCESS, &len)) == NULL)
|
||||
return 0;
|
||||
cnt = (len - 4*4) / (4+4);
|
||||
if (len < 4*4 || len != (size_t)cnt*(4+4) + 4*4) {
|
||||
@@ -514,6 +636,107 @@ static int get_rsync_acl(const char *fname, rsync_acl *racl,
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SOLARIS_ACLS
|
||||
/* Solaris has no libacl *_at; read the ACL through the held fd via facl(2)
|
||||
* when we have one. With no held fd this branch is skipped and the path-based
|
||||
* call below reads the ACL (acceptable: a read can't redirect a write out of
|
||||
* the tree). */
|
||||
if (fd >= 0) {
|
||||
if ((sacl = sys_acl_get_fd_type(fd, type)) != 0) {
|
||||
BOOL ok = unpack_smb_acl(sacl, racl);
|
||||
|
||||
sys_acl_free_acl(sacl);
|
||||
if (!ok) {
|
||||
rsyserr(FERROR_XFER, errno, "get_acl: unpack_smb_acl(%s)", fname);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (no_acl_syscall_error(errno)) {
|
||||
if (type == SMB_ACL_TYPE_ACCESS)
|
||||
rsync_acl_fake_perms(racl, mode);
|
||||
return 0;
|
||||
}
|
||||
rsyserr(FERROR_XFER, errno, "get_acl: sys_acl_get_fd_type(%s, %s)",
|
||||
fname, str_acl_type(type));
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SUPPORT_ACL_FD
|
||||
#ifdef HAVE_LIBACL_AT
|
||||
/* Read the ACL via the new libacl *_at calls; fd<0 && dirfd<0
|
||||
* (e.g. a synthetic dir) falls through to the path-based call below. */
|
||||
if (fd >= 0 || dirfd >= 0) {
|
||||
if (fd >= 0)
|
||||
sacl = sys_acl_get_file_at(fd, "", AT_EMPTY_PATH, type);
|
||||
else
|
||||
sacl = sys_acl_get_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type);
|
||||
if (sacl != 0) {
|
||||
BOOL ok = unpack_smb_acl(sacl, racl);
|
||||
sys_acl_free_acl(sacl);
|
||||
if (!ok) {
|
||||
rsyserr(FERROR_XFER, errno, "get_acl: unpack_smb_acl(%s)", fname);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (no_acl_syscall_error(errno)) {
|
||||
if (type == SMB_ACL_TYPE_ACCESS)
|
||||
rsync_acl_fake_perms(racl, mode);
|
||||
return 0;
|
||||
}
|
||||
rsyserr(FERROR_XFER, errno, "get_acl: acl_get_file_at(%s, %s)",
|
||||
fname, str_acl_type(type));
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
/* Race-safe path: read the ACL through the held O_NOFOLLOW fd, or via
|
||||
* setxattrat(AT_SYMLINK_NOFOLLOW) on dirfd+leaf, instead of re-resolving
|
||||
* fname. Only for real-root ACLs (am_root >= 0; the fake-super branch
|
||||
* above already returned). */
|
||||
if (fd >= 0 || (dirfd >= 0 && xacl_at_available())) {
|
||||
int is_def = type == SMB_ACL_TYPE_DEFAULT;
|
||||
rsync_acl_ent *ents = NULL;
|
||||
int n = 0, rc;
|
||||
|
||||
if (fd >= 0)
|
||||
rc = xacl_get_fd(fd, is_def, &ents, &n);
|
||||
else
|
||||
rc = xacl_get_at(dirfd, leaf, is_def, &ents, &n);
|
||||
if (rc < 0) {
|
||||
if (no_acl_syscall_error(errno)) {
|
||||
if (type == SMB_ACL_TYPE_ACCESS)
|
||||
rsync_acl_fake_perms(racl, mode);
|
||||
return 0;
|
||||
}
|
||||
rsyserr(FERROR_XFER, errno, "get_acl: xacl_get(%s, %s)",
|
||||
fname, str_acl_type(type));
|
||||
return -1;
|
||||
}
|
||||
if (n == 0) {
|
||||
/* No explicit ACL: mirror libacl's mode-derived access ACL
|
||||
* (an absent default ACL stays empty). */
|
||||
if (type == SMB_ACL_TYPE_ACCESS)
|
||||
rsync_acl_fake_perms(racl, mode);
|
||||
} else if (!unpack_acl_entries(ents, n, racl)) {
|
||||
if (ents)
|
||||
free(ents);
|
||||
rsyserr(FERROR_XFER, errno, "get_acl: unpack_acl_entries(%s)", fname);
|
||||
return -1;
|
||||
}
|
||||
if (ents)
|
||||
free(ents);
|
||||
return 0;
|
||||
}
|
||||
/* Neither a held fd nor a usable dirfd path (xacl_at_available() covers the
|
||||
* *xattrat syscalls AND the pre-6.13 /proc/self/fd compat, so this is the
|
||||
* BSDs / a /proc-less namespace / an un-pinnable entry): read the real
|
||||
* destination ACL via the path-based call rather than a mode-only fake, so
|
||||
* --acls stays functional where the race-safe primitive is unavailable. */
|
||||
#endif /* HAVE_LIBACL_AT */
|
||||
#endif
|
||||
|
||||
if ((sacl = sys_acl_get_file(fname, type)) != 0) {
|
||||
BOOL ok = unpack_smb_acl(sacl, racl);
|
||||
|
||||
@@ -535,8 +758,10 @@ static int get_rsync_acl(const char *fname, rsync_acl *racl,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Return the Access Control List for the given filename. */
|
||||
int get_acl(const char *fname, stat_x *sxp)
|
||||
/* Return the Access Control List for the given filename. When a held
|
||||
* O_NOFOLLOW fd (or a dirfd+leaf) is available, the ACL is read race-safely
|
||||
* through it; otherwise (fd < 0 && dirfd < 0) the path-based fallback is used. */
|
||||
int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x *sxp)
|
||||
{
|
||||
sxp->acc_acl = create_racl();
|
||||
|
||||
@@ -557,7 +782,7 @@ int get_acl(const char *fname, stat_x *sxp)
|
||||
} else if (IS_MISSING_FILE(sxp->st))
|
||||
return 0;
|
||||
|
||||
if (get_rsync_acl(fname, sxp->acc_acl, SMB_ACL_TYPE_ACCESS,
|
||||
if (get_rsync_acl(fd, dirfd, leaf, fname, sxp->acc_acl, SMB_ACL_TYPE_ACCESS,
|
||||
sxp->st.st_mode) < 0) {
|
||||
free_acl(sxp);
|
||||
return -1;
|
||||
@@ -565,7 +790,7 @@ int get_acl(const char *fname, stat_x *sxp)
|
||||
|
||||
if (S_ISDIR(sxp->st.st_mode)) {
|
||||
sxp->def_acl = create_racl();
|
||||
if (get_rsync_acl(fname, sxp->def_acl, SMB_ACL_TYPE_DEFAULT,
|
||||
if (get_rsync_acl(fd, dirfd, leaf, fname, sxp->def_acl, SMB_ACL_TYPE_DEFAULT,
|
||||
sxp->st.st_mode) < 0) {
|
||||
free_acl(sxp);
|
||||
return -1;
|
||||
@@ -575,6 +800,11 @@ int get_acl(const char *fname, stat_x *sxp)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_acl(const char *fname, stat_x *sxp)
|
||||
{
|
||||
return get_acl_fdat(-1, -1, NULL, fname, sxp);
|
||||
}
|
||||
|
||||
/* === Send functions === */
|
||||
|
||||
/* Send the ida list over the file descriptor. */
|
||||
@@ -933,17 +1163,60 @@ static mode_t change_sacl_perms(SMB_ACL_T sacl, rsync_acl *racl, mode_t old_mode
|
||||
}
|
||||
#endif
|
||||
|
||||
static int set_rsync_acl(const char *fname, acl_duo *duo_item,
|
||||
SMB_ACL_TYPE_T type, stat_x *sxp, mode_t mode)
|
||||
static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
|
||||
acl_duo *duo_item, SMB_ACL_TYPE_T type, stat_x *sxp, mode_t mode)
|
||||
{
|
||||
#ifndef SUPPORT_ACL_FD
|
||||
#ifndef HAVE_SOLARIS_ACLS
|
||||
(void)fd; /* Solaris drives the ACL via facl(2) on fd but has no SUPPORT_ACL_FD. */
|
||||
#endif
|
||||
(void)dirfd;
|
||||
(void)leaf;
|
||||
#endif
|
||||
if (type == SMB_ACL_TYPE_DEFAULT
|
||||
&& duo_item->racl.user_obj == NO_ENTRY) {
|
||||
int rc;
|
||||
#ifdef SUPPORT_XATTRS
|
||||
/* --fake-super support: delete default ACL from xattrs. */
|
||||
if (am_root < 0)
|
||||
rc = del_def_xattr_acl(fname);
|
||||
rc = del_def_xattr_acl(fd, fname);
|
||||
else
|
||||
#endif
|
||||
#ifdef SUPPORT_ACL_FD
|
||||
#ifdef HAVE_LIBACL_AT
|
||||
/* Race-safe default-ACL delete via the new libacl *_at
|
||||
* calls (held fd via AT_EMPTY_PATH, dirfd+leaf via AT_SYMLINK_NOFOLLOW)
|
||||
* -- race-safe on every Linux kernel. fd<0 && dirfd<0 falls to path. */
|
||||
if (fd >= 0)
|
||||
rc = sys_acl_delete_def_file_at(fd, "", AT_EMPTY_PATH);
|
||||
else if (dirfd >= 0)
|
||||
rc = sys_acl_delete_def_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW);
|
||||
else
|
||||
#else
|
||||
/* Race-safe default-ACL delete via the held fd or dirfd+leaf. Where
|
||||
* neither is available (xacl_at_available() is false -- the BSDs, a
|
||||
* /proc-less namespace, an un-pinnable entry; every Linux with procfs
|
||||
* takes the dirfd path via *xattrat or the /proc/self/fd compat) -- fall
|
||||
* back to the path-based call, preferring the documented --acls behaviour
|
||||
* over refusing it where the race-safe primitive is unavailable. */
|
||||
if (fd >= 0)
|
||||
rc = xacl_del_default_fd(fd);
|
||||
else if (dirfd >= 0 && xacl_at_available())
|
||||
rc = xacl_del_default_at(dirfd, leaf);
|
||||
else
|
||||
#endif /* HAVE_LIBACL_AT */
|
||||
#endif
|
||||
#ifdef HAVE_SOLARIS_ACLS
|
||||
/* Solaris: delete the default ACL through the held fd via facl(2). For a
|
||||
* root receiver a missing held fd means the leaf was raced, so refuse rather
|
||||
* than let the path-based delete follow it; a plain non-root receiver keeps
|
||||
* the legacy path fallback (op_pin am_root != 0 rule). */
|
||||
if (fd >= 0)
|
||||
rc = sys_acl_delete_def_fd(fd);
|
||||
else if (secure_relpath_active() && am_root) {
|
||||
errno = ELOOP;
|
||||
rc = -1;
|
||||
} else
|
||||
#endif
|
||||
rc = sys_acl_delete_def_file(fname);
|
||||
if (rc < 0) {
|
||||
@@ -972,7 +1245,7 @@ static int set_rsync_acl(const char *fname, acl_duo *duo_item,
|
||||
SIVAL(bp, 4, ida->access);
|
||||
}
|
||||
}
|
||||
rc = set_xattr_acl(fname, type == SMB_ACL_TYPE_ACCESS, buf, len);
|
||||
rc = set_xattr_acl(fd, fname, type == SMB_ACL_TYPE_ACCESS, buf, len);
|
||||
free(buf);
|
||||
return rc;
|
||||
#endif
|
||||
@@ -989,6 +1262,92 @@ static int set_rsync_acl(const char *fname, acl_duo *duo_item,
|
||||
if (cur_mode == (mode_t)-1)
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
#ifdef SUPPORT_ACL_FD
|
||||
#ifdef HAVE_LIBACL_AT
|
||||
/* Apply the packed/perm-reconciled ACL (duo_item->sacl)
|
||||
* through the new libacl *_at calls -- held fd via AT_EMPTY_PATH,
|
||||
* dirfd+leaf via AT_SYMLINK_NOFOLLOW -- race-safe on every Linux kernel,
|
||||
* and byte-identical to the path-based sys_acl_set_file() below. */
|
||||
if (fd >= 0 || dirfd >= 0) {
|
||||
int rc;
|
||||
|
||||
if (fd >= 0)
|
||||
rc = sys_acl_set_file_at(fd, "", AT_EMPTY_PATH, type, duo_item->sacl);
|
||||
else
|
||||
rc = sys_acl_set_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type, duo_item->sacl);
|
||||
if (rc < 0) {
|
||||
rsyserr(FERROR_XFER, errno, "set_acl: acl_set_file_at(%s, %s)",
|
||||
fname, str_acl_type(type));
|
||||
return -1;
|
||||
}
|
||||
if (type == SMB_ACL_TYPE_ACCESS)
|
||||
sxp->st.st_mode = cur_mode;
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
/* Race-safe write: serialize the packed (and perm-reconciled)
|
||||
* system ACL to the kernel xattr format and apply it through the
|
||||
* held fd or dirfd+leaf -- never re-resolving fname. This matches
|
||||
* exactly what sys_acl_set_file() would have written. */
|
||||
if (fd >= 0 || (dirfd >= 0 && xacl_at_available())) {
|
||||
int is_def = type == SMB_ACL_TYPE_DEFAULT;
|
||||
rsync_acl_ent *ents;
|
||||
int n = sacl_to_entries(duo_item->sacl, &ents);
|
||||
int rc;
|
||||
|
||||
if (n < 0)
|
||||
return -1;
|
||||
if (fd >= 0)
|
||||
rc = xacl_set_fd(fd, is_def, ents, n);
|
||||
else
|
||||
rc = xacl_set_at(dirfd, leaf, is_def, ents, n);
|
||||
free(ents);
|
||||
if (rc < 0) {
|
||||
rsyserr(FERROR_XFER, errno, "set_acl: xacl_set(%s, %s)",
|
||||
fname, str_acl_type(type));
|
||||
return -1;
|
||||
}
|
||||
if (type == SMB_ACL_TYPE_ACCESS)
|
||||
sxp->st.st_mode = cur_mode;
|
||||
return 0;
|
||||
}
|
||||
/* No held fd and no usable dirfd path (xacl_at_available() is false --
|
||||
* the BSDs, a /proc-less namespace, an un-pinnable entry; every Linux
|
||||
* with procfs took xacl_set_at() above via *xattrat or the /proc/self/fd
|
||||
* compat): prefer the documented --acls behaviour over refusing it and
|
||||
* fall back to the path-based set. This re-resolves fname, so it still
|
||||
* carries the parent-symlink-race exposure on those remaining platforms;
|
||||
* it is the only way to honour --acls where no race-safe primitive
|
||||
* exists. */
|
||||
#endif /* HAVE_LIBACL_AT */
|
||||
#endif
|
||||
#ifdef HAVE_SOLARIS_ACLS
|
||||
/* Solaris: apply the ACL through the held fd via facl(2). */
|
||||
if (fd >= 0) {
|
||||
if (sys_acl_set_fd_type(fd, type, duo_item->sacl) < 0) {
|
||||
rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_set_fd_type(%s, %s)",
|
||||
fname, str_acl_type(type));
|
||||
return -1;
|
||||
}
|
||||
if (type == SMB_ACL_TYPE_ACCESS)
|
||||
sxp->st.st_mode = cur_mode;
|
||||
return 0;
|
||||
}
|
||||
if (secure_relpath_active() && am_root) {
|
||||
/* Real root always can open its own freshly-staged reg/dir/fifo leaf,
|
||||
* so a missing held fd on a confined receiver means the leaf was raced
|
||||
* to a symlink; sys_acl_set_file() follows the leaf, so refuse rather
|
||||
* than write the attacker-supplied ACL onto a redirected inode (covers
|
||||
* the top-level no-slash entry the caller's slashed-path xattr_refuse
|
||||
* gate misses). A plain non-root receiver keeps the path-based fallback
|
||||
* for a legitimately un-pinnable owned leaf (e.g. a 0300 dir), matching
|
||||
* the operator-path op_pin rule (am_root != 0). */
|
||||
errno = ELOOP;
|
||||
rsyserr(FERROR_XFER, errno, "set_acl: refusing path-based ACL on %s (no held fd)",
|
||||
fname);
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
if (sys_acl_set_file(fname, type, duo_item->sacl) < 0) {
|
||||
rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_set_file(%s, %s)",
|
||||
@@ -1006,11 +1365,16 @@ static int set_rsync_acl(const char *fname, acl_duo *duo_item,
|
||||
* dir), and the regular mode bits on the file. Call this with fname set to
|
||||
* NULL to just check if the ACL is different.
|
||||
*
|
||||
* When a held O_NOFOLLOW fd (or a dirfd+leaf) is supplied, the ACL is applied
|
||||
* race-safely through it; otherwise (fd < 0 && dirfd < 0) the path-based
|
||||
* fallback is used.
|
||||
*
|
||||
* If the ACL operation has a side-effect of changing the file's mode, the
|
||||
* sxp->st.st_mode value will be changed to match.
|
||||
*
|
||||
* Returns 0 for an unchanged ACL, 1 for changed, -1 for failed. */
|
||||
int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode_t new_mode)
|
||||
int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,
|
||||
const struct file_struct *file, stat_x *sxp, mode_t new_mode)
|
||||
{
|
||||
int changed = 0;
|
||||
int32 ndx;
|
||||
@@ -1030,7 +1394,7 @@ int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode
|
||||
if (!eq) {
|
||||
changed = 1;
|
||||
if (!dry_run && fname
|
||||
&& set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_ACCESS,
|
||||
&& set_rsync_acl(fd, dirfd, leaf, fname, duo_item, SMB_ACL_TYPE_ACCESS,
|
||||
sxp, new_mode) < 0)
|
||||
return -1;
|
||||
}
|
||||
@@ -1047,7 +1411,7 @@ int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode
|
||||
if (!eq) {
|
||||
changed = 1;
|
||||
if (!dry_run && fname
|
||||
&& set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_DEFAULT,
|
||||
&& set_rsync_acl(fd, dirfd, leaf, fname, duo_item, SMB_ACL_TYPE_DEFAULT,
|
||||
sxp, new_mode) < 0)
|
||||
return -1;
|
||||
}
|
||||
@@ -1056,6 +1420,11 @@ int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode
|
||||
return changed;
|
||||
}
|
||||
|
||||
int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode_t new_mode)
|
||||
{
|
||||
return set_acl_fdat(-1, -1, NULL, fname, file, sxp, new_mode);
|
||||
}
|
||||
|
||||
/* Non-incremental recursion needs to convert all the received IDs.
|
||||
* This is done in a single pass after receiving the whole file-list. */
|
||||
static void match_racl_ids(const item_list *racl_list)
|
||||
|
||||
+103
-5
@@ -22,6 +22,13 @@
|
||||
#include "itypes.h"
|
||||
#include "ifuncs.h"
|
||||
|
||||
/* O_CLOEXEC is absent on some still-supported targets. The random-source fd
|
||||
* is read and closed synchronously, so the established zero-value fallback is
|
||||
* sufficient without adding a configure dependency. */
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC 0
|
||||
#endif
|
||||
|
||||
extern int read_only;
|
||||
extern char *password_file;
|
||||
extern struct name_num_obj valid_auth_checksums;
|
||||
@@ -57,10 +64,31 @@ void base64_encode(const char *buf, int len, char *out, int pad)
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
/* Fill buf with len bytes from the kernel CSPRNG. Returns 1 on success.
|
||||
* We read /dev/urandom directly rather than depending on getrandom()/
|
||||
* arc4random_buf() availability so this works on every platform rsync
|
||||
* targets without new configure probes. */
|
||||
static int get_random_bytes(char *buf, int len)
|
||||
{
|
||||
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
|
||||
int got = 0;
|
||||
if (fd < 0)
|
||||
return 0;
|
||||
while (got < len) {
|
||||
int n = read(fd, buf + got, len - got);
|
||||
if (n <= 0)
|
||||
break;
|
||||
got += n;
|
||||
}
|
||||
close(fd);
|
||||
return got == len;
|
||||
}
|
||||
|
||||
/* Generate a challenge buffer and return it base64-encoded. */
|
||||
static void gen_challenge(const char *addr, char *challenge)
|
||||
{
|
||||
char input[32];
|
||||
char rnd[32];
|
||||
char digest[MAX_DIGEST_LEN];
|
||||
struct timeval tv;
|
||||
int len;
|
||||
@@ -74,6 +102,16 @@ static void gen_challenge(const char *addr, char *challenge)
|
||||
SIVAL(input, 24, getpid());
|
||||
|
||||
len = sum_init(valid_auth_checksums.negotiated_nni, 0);
|
||||
/* The challenge must be unpredictable to a network observer; addr+time
|
||||
* +pid alone is ~35 bits and lets an attacker enumerate the preimage
|
||||
* offline. Hash 32 bytes from the kernel RNG first so the digest
|
||||
* carries full entropy, keeping the legacy inputs as a mix-in so a
|
||||
* urandom failure degrades to (never below) the old behaviour. */
|
||||
if (get_random_bytes(rnd, sizeof rnd))
|
||||
sum_update(rnd, sizeof rnd);
|
||||
else
|
||||
rprintf(FWARNING, "gen_challenge: /dev/urandom unavailable, "
|
||||
"falling back to time-based challenge\n");
|
||||
sum_update(input, sizeof input);
|
||||
sum_end(digest);
|
||||
|
||||
@@ -110,8 +148,23 @@ static const char *check_secret(int module, const char *user, const char *group,
|
||||
char *err;
|
||||
FILE *fh;
|
||||
|
||||
if (!fname || !*fname || (fh = fopen(fname, "r")) == NULL)
|
||||
/* Daemon 'secrets file = PATH' open. A planted symlink would be
|
||||
* followed and the strict-modes fstat() check below runs on the target
|
||||
* inode, so a symlink to /etc/shadow (0640 root:shadow) would pass and
|
||||
* the daemon would auth against shadow hashes. Refuse symlinks not
|
||||
* owned by uid 0 or our euid. */
|
||||
if (!fname || !*fname)
|
||||
return "no secrets file";
|
||||
{
|
||||
int fd = open_no_attacker_symlinks(fname, O_RDONLY, 0);
|
||||
if (fd < 0)
|
||||
return "no secrets file";
|
||||
fh = fdopen(fd, "r");
|
||||
if (!fh) {
|
||||
close(fd);
|
||||
return "no secrets file";
|
||||
}
|
||||
}
|
||||
|
||||
if (do_fstat(fileno(fh), &st) == -1) {
|
||||
rsyserr(FLOG, errno, "fstat(%s)", fname);
|
||||
@@ -184,13 +237,23 @@ static const char *getpassf(const char *filename)
|
||||
} else {
|
||||
int fd;
|
||||
|
||||
if ((fd = open(filename,O_RDONLY)) < 0) {
|
||||
/* --password-file=PATH client open. Its first line is sent as the
|
||||
* auth response, so a planted symlink leaks the target's content
|
||||
* (e.g. shadow hashes) to a malicious daemon; the do_stat()
|
||||
* other-access check runs on the target mode and passes 0640
|
||||
* root:shadow. Refuse symlinks not owned by uid 0 or our euid. */
|
||||
if ((fd = open_no_attacker_symlinks(filename, O_RDONLY, 0)) < 0) {
|
||||
rsyserr(FERROR, errno, "could not open password file %s", filename);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
}
|
||||
|
||||
if (do_stat(filename, &st) == -1) {
|
||||
rsyserr(FERROR, errno, "stat(%s)", filename);
|
||||
/* fstat the opened fd, not the pathname: a same-object check
|
||||
* (matching check_secret() above) so an attacker who swaps the
|
||||
* path between open and check can't make the owner/mode test
|
||||
* validate a different inode than the one we read the password
|
||||
* from. */
|
||||
if (do_fstat(fd, &st) == -1) {
|
||||
rsyserr(FERROR, errno, "fstat(%s)", filename);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
}
|
||||
if ((st.st_mode & 06) != 0) {
|
||||
@@ -240,6 +303,35 @@ char *auth_server(int f_in, int f_out, int module, const char *host,
|
||||
return "";
|
||||
|
||||
negotiate_daemon_auth(f_out, 0);
|
||||
|
||||
/* Enforce a configured minimum auth digest (default: none). This refuses
|
||||
* a peer that negotiated -- or, via an omitted digest list / old protocol,
|
||||
* fell back to -- a digest weaker than the operator-required floor, e.g. a
|
||||
* client downgraded to md5/md4. Lower rank == stronger (the auth list is
|
||||
* ordered strongest-first), so a higher rank than the floor is too weak. */
|
||||
{
|
||||
const char *min_digest = lp_auth_digest(module);
|
||||
if (min_digest && *min_digest) {
|
||||
int floor_rank = auth_digest_rank(min_digest);
|
||||
int got_rank = auth_digest_rank(valid_auth_checksums.negotiated_nni->name);
|
||||
if (floor_rank < 0) {
|
||||
rprintf(FLOG, "auth failed on module %s from %s (%s): the "
|
||||
"configured 'auth digest = %s' is not a supported digest "
|
||||
"on this build\n",
|
||||
lp_name(module), host, addr, min_digest);
|
||||
return NULL;
|
||||
}
|
||||
if (got_rank < 0 || got_rank > floor_rank) {
|
||||
rprintf(FLOG, "auth failed on module %s from %s (%s): negotiated "
|
||||
"auth digest %s is weaker than the required "
|
||||
"'auth digest = %s'\n",
|
||||
lp_name(module), host, addr,
|
||||
valid_auth_checksums.negotiated_nni->name, min_digest);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gen_challenge(addr, challenge);
|
||||
|
||||
io_printf(f_out, "%s%s\n", leader, challenge);
|
||||
@@ -255,7 +347,13 @@ char *auth_server(int f_in, int f_out, int module, const char *host,
|
||||
|
||||
users = strdup(users);
|
||||
|
||||
for (tok = strtok(users, " ,\t"); tok; tok = strtok(NULL, " ,\t")) {
|
||||
/* conf_strtok() honours the documented leading-comma form: a value that
|
||||
* starts with a comma splits on commas ALONE, so an entry may contain
|
||||
* spaces -- which is how a group name with a space is written. Splitting
|
||||
* on whitespace here tore such an entry apart, so the rule the admin wrote
|
||||
* never matched and a rule they never wrote appeared from its tail. The
|
||||
* daemon's gid field already uses this parser (clientserver.c). */
|
||||
for (tok = conf_strtok(users); tok; tok = conf_strtok(NULL)) {
|
||||
char *opts;
|
||||
/* See if the user appended :deny, :ro, or :rw. */
|
||||
if ((opts = strchr(tok, ':')) != NULL) {
|
||||
|
||||
@@ -30,10 +30,37 @@ extern int preserve_links;
|
||||
extern int safe_symlinks;
|
||||
extern int backup_dir_len;
|
||||
extern unsigned int backup_dir_remainder;
|
||||
extern int operator_path_resolve;
|
||||
extern char backup_dir_buf[MAXPATHLEN];
|
||||
extern char *backup_suffix;
|
||||
extern char *backup_dir;
|
||||
|
||||
/* Pin a backup SOURCE leaf with a confined O_NOFOLLOW fd (via the operator
|
||||
* owner-walk resolver, like set_file_attrs's op_leaf_fd) so the ACL/xattr the
|
||||
* backup caches off it are read through the held fd -- a parent-symlink race
|
||||
* can't redirect the read out of the module. Returns -1 for a non-hardened
|
||||
* receiver (caller path-reads) or for a raced/absent leaf on a hardened one
|
||||
* (caller skips the cache rather than read through a flippable path; use
|
||||
* backup_metadata_hardened() to tell the two -1 cases apart). */
|
||||
int backup_metadata_hardened(void)
|
||||
{
|
||||
return secure_relpath_active() && !symlink_optout_allowed();
|
||||
}
|
||||
|
||||
int backup_source_fd(const char *path)
|
||||
{
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW
|
||||
if (backup_metadata_hardened() && path && *path) {
|
||||
int save = operator_path_resolve, fd;
|
||||
operator_path_resolve = 1;
|
||||
fd = do_open_at(path, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0);
|
||||
operator_path_resolve = save;
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Returns -1 on error, 0 on missing dir, and 1 on present dir. */
|
||||
static int validate_backup_dir(void)
|
||||
{
|
||||
@@ -120,18 +147,27 @@ static BOOL copy_valid_path(const char *fname)
|
||||
struct file_struct *file;
|
||||
if (!(file = make_file(rel, NULL, NULL, 0, NO_FILTERS)))
|
||||
continue;
|
||||
#ifdef SUPPORT_ACLS
|
||||
if (preserve_acls && !S_ISLNK(file->mode)) {
|
||||
get_acl(rel, &sx);
|
||||
cache_tmp_acl(file, &sx);
|
||||
free_acl(&sx);
|
||||
#if defined SUPPORT_ACLS || defined SUPPORT_XATTRS
|
||||
{ /* read the source dir's ACL/xattr through a confined fd */
|
||||
int bfd = backup_source_fd(rel);
|
||||
if (!backup_metadata_hardened() || bfd >= 0) {
|
||||
# ifdef SUPPORT_ACLS
|
||||
if (preserve_acls && !S_ISLNK(file->mode)) {
|
||||
get_acl_fdat(bfd, -1, NULL, rel, &sx);
|
||||
cache_tmp_acl(file, &sx);
|
||||
free_acl(&sx);
|
||||
}
|
||||
# endif
|
||||
# ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs) {
|
||||
get_xattr(rel, bfd, &sx);
|
||||
cache_tmp_xattr(file, &sx);
|
||||
free_xattr(&sx);
|
||||
}
|
||||
# endif
|
||||
}
|
||||
#endif
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs) {
|
||||
get_xattr(rel, &sx);
|
||||
cache_tmp_xattr(file, &sx);
|
||||
free_xattr(&sx);
|
||||
if (bfd >= 0)
|
||||
close(bfd);
|
||||
}
|
||||
#endif
|
||||
set_file_attrs(backup_dir_buf, file, NULL, NULL, 0);
|
||||
@@ -159,12 +195,15 @@ char *get_backup_name(const char *fname)
|
||||
if (backup_dir) {
|
||||
static int initialized = 0;
|
||||
if (!initialized) {
|
||||
char dirbuf[MAXPATHLEN];
|
||||
int ret;
|
||||
if (strlcpy(dirbuf, backup_dir_buf, sizeof dirbuf) >= sizeof dirbuf) {
|
||||
errno = ENAMETOOLONG;
|
||||
return NULL;
|
||||
}
|
||||
if (backup_dir_len > 1)
|
||||
backup_dir_buf[backup_dir_len-1] = '\0';
|
||||
ret = make_path(backup_dir_buf, 0);
|
||||
if (backup_dir_len > 1)
|
||||
backup_dir_buf[backup_dir_len-1] = '/';
|
||||
dirbuf[backup_dir_len-1] = '\0';
|
||||
ret = make_path(dirbuf, 0);
|
||||
if (ret < 0)
|
||||
return NULL;
|
||||
initialized = 1;
|
||||
@@ -223,7 +262,7 @@ static inline int link_or_rename(const char *from, const char *to,
|
||||
/* Hard-link, rename, or copy an item to the backup name. Returns 0 for
|
||||
* failure, 1 if item was moved, 2 if item was duplicated or hard linked
|
||||
* into backup area, or 3 if item doesn't exist or isn't a regular file. */
|
||||
int make_backup(const char *fname, BOOL prefer_rename)
|
||||
static int make_backup_inner(const char *fname, BOOL prefer_rename)
|
||||
{
|
||||
stat_x sx;
|
||||
struct file_struct *file;
|
||||
@@ -239,6 +278,38 @@ int make_backup(const char *fname, BOOL prefer_rename)
|
||||
if (!(buf = get_backup_name(fname)))
|
||||
return 0;
|
||||
|
||||
#ifdef SUPPORT_LINKS
|
||||
/* Honor --safe-links BEFORE the hard-link / rename fast path. When
|
||||
* CAN_HARDLINK_SYMLINK is defined, link_or_rename() would otherwise
|
||||
* hard-link an escaping symlink (e.g. ../../etc/passwd) into the backup
|
||||
* area and "goto success", skipping the safe_symlinks check in the
|
||||
* copy-fallback path below -- silently preserving an unsafe link that
|
||||
* --safe-links was meant to drop. Match the copy path: don't back up an
|
||||
* unsafe symlink. */
|
||||
if (preserve_links && S_ISLNK(sx.st.st_mode) && safe_symlinks) {
|
||||
char lnkbuf[MAXPATHLEN];
|
||||
int llen = do_readlink(fname, lnkbuf, MAXPATHLEN - 1);
|
||||
/* A failed readlink means we can't verify the target, so fail
|
||||
* closed: skip the backup rather than let the hard-link fast path
|
||||
* preserve a possibly-unsafe symlink unchecked. */
|
||||
if (llen <= 0) {
|
||||
if (INFO_GTE(SYMSAFE, 1))
|
||||
rprintf(FINFO, "not backing up symlink with unreadable target \"%s\"\n", fname);
|
||||
ret = 2;
|
||||
goto success;
|
||||
}
|
||||
lnkbuf[llen] = '\0';
|
||||
if (unsafe_symlink(lnkbuf, fname)) {
|
||||
if (INFO_GTE(SYMSAFE, 1)) {
|
||||
rprintf(FINFO, "not backing up unsafe symlink \"%s\" -> \"%s\"\n",
|
||||
fname, lnkbuf);
|
||||
}
|
||||
ret = 2;
|
||||
goto success;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Try a hard-link or a rename first. Using rename is not atomic, but
|
||||
* is more efficient than forcing a copy for larger files when no hard-
|
||||
* linking is possible. */
|
||||
@@ -259,18 +330,27 @@ int make_backup(const char *fname, BOOL prefer_rename)
|
||||
if (!(file = make_file(fname, NULL, &sx.st, 0, NO_FILTERS)))
|
||||
return 3; /* the file could have disappeared */
|
||||
|
||||
#ifdef SUPPORT_ACLS
|
||||
if (preserve_acls && !S_ISLNK(file->mode)) {
|
||||
get_acl(fname, &sx);
|
||||
cache_tmp_acl(file, &sx);
|
||||
free_acl(&sx);
|
||||
#if defined SUPPORT_ACLS || defined SUPPORT_XATTRS
|
||||
{ /* read the source file's ACL/xattr through a confined fd */
|
||||
int bfd = backup_source_fd(fname);
|
||||
if (!backup_metadata_hardened() || bfd >= 0) {
|
||||
# ifdef SUPPORT_ACLS
|
||||
if (preserve_acls && !S_ISLNK(file->mode)) {
|
||||
get_acl_fdat(bfd, -1, NULL, fname, &sx);
|
||||
cache_tmp_acl(file, &sx);
|
||||
free_acl(&sx);
|
||||
}
|
||||
# endif
|
||||
# ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs) {
|
||||
get_xattr(fname, bfd, &sx);
|
||||
cache_tmp_xattr(file, &sx);
|
||||
free_xattr(&sx);
|
||||
}
|
||||
# endif
|
||||
}
|
||||
#endif
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs) {
|
||||
get_xattr(fname, &sx);
|
||||
cache_tmp_xattr(file, &sx);
|
||||
free_xattr(&sx);
|
||||
if (bfd >= 0)
|
||||
close(bfd);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -353,3 +433,17 @@ int make_backup(const char *fname, BOOL prefer_rename)
|
||||
rprintf(FINFO, "backed up %s to %s\n", fname, buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int make_backup(const char *fname, BOOL prefer_rename)
|
||||
{
|
||||
int ret;
|
||||
/* The --backup-dir is an operator-supplied path: resolve it (and the
|
||||
* tail/rename beneath it) with the ownership walk so a foreign-owned
|
||||
* symlink component is refused while the operator's own is followed --
|
||||
* absolute and relative alike. --insecure-links / "insecure links ="
|
||||
* restores legacy following. */
|
||||
operator_path_resolve = 1;
|
||||
ret = make_backup_inner(fname, prefer_rename);
|
||||
operator_path_resolve = 0;
|
||||
return ret;
|
||||
}
|
||||
@@ -166,25 +166,33 @@ static int write_arg(const char *arg)
|
||||
const char *x, *s;
|
||||
int len, err = 0;
|
||||
|
||||
/* Emit a "--opt=" prefix unquoted only when it is a plain option token;
|
||||
* a metacharacter before '=' (an attacker-shaped arg) must be quoted
|
||||
* along with the rest, or it would run raw in the replay script. */
|
||||
if (*arg == '-' && (x = strchr(arg, '=')) != NULL) {
|
||||
err |= write(batch_sh_fd, arg, x - arg + 1) != x - arg + 1;
|
||||
arg += x - arg + 1;
|
||||
}
|
||||
|
||||
if (strpbrk(arg, " \"'&;|[]()$#!*?^\\") != NULL) {
|
||||
err |= write(batch_sh_fd, "'", 1) != 1;
|
||||
for (s = arg; (x = strchr(s, '\'')) != NULL; s = x + 1) {
|
||||
err |= write(batch_sh_fd, s, x - s + 1) != x - s + 1;
|
||||
err |= write(batch_sh_fd, "'", 1) != 1;
|
||||
const char *p = arg;
|
||||
while (p < x && (*p == '-' || *p == '_'
|
||||
|| (*p >= '0' && *p <= '9')
|
||||
|| (*p >= 'A' && *p <= 'Z')
|
||||
|| (*p >= 'a' && *p <= 'z')))
|
||||
p++;
|
||||
if (p == x) {
|
||||
err |= write(batch_sh_fd, arg, x - arg + 1) != x - arg + 1;
|
||||
arg += x - arg + 1;
|
||||
}
|
||||
len = strlen(s);
|
||||
err |= write(batch_sh_fd, s, len) != len;
|
||||
err |= write(batch_sh_fd, "'", 1) != 1;
|
||||
return err;
|
||||
}
|
||||
|
||||
len = strlen(arg);
|
||||
err |= write(batch_sh_fd, arg, len) != len;
|
||||
/* Single-quote unconditionally so every shell metacharacter (backtick,
|
||||
* newline, redirection, ...) stays literal in the replay script. An
|
||||
* embedded ' is emitted as the '\'' close/escape/reopen sequence. */
|
||||
err |= write(batch_sh_fd, "'", 1) != 1;
|
||||
for (s = arg; (x = strchr(s, '\'')) != NULL; s = x + 1) {
|
||||
err |= write(batch_sh_fd, s, x - s) != x - s;
|
||||
err |= write(batch_sh_fd, "'\\''", 4) != 4;
|
||||
}
|
||||
len = strlen(s);
|
||||
err |= write(batch_sh_fd, s, len) != len;
|
||||
err |= write(batch_sh_fd, "'", 1) != 1;
|
||||
|
||||
return err;
|
||||
}
|
||||
@@ -194,7 +202,7 @@ static int write_opt(const char *opt, const char *arg)
|
||||
{
|
||||
int len = strlen(opt);
|
||||
int err = write(batch_sh_fd, " ", 1) != 1;
|
||||
err = write(batch_sh_fd, opt, len) != len ? 1 : 0;
|
||||
err |= write(batch_sh_fd, opt, len) != len;
|
||||
if (arg) {
|
||||
err |= write(batch_sh_fd, "=", 1) != 1;
|
||||
err |= write_arg(arg);
|
||||
@@ -210,6 +218,16 @@ static void write_filter_rules(int fd)
|
||||
for (ent = filter_list.head; ent; ent = ent->next) {
|
||||
unsigned int plen;
|
||||
char *p = get_rule_prefix(ent, "- ", 0, &plen);
|
||||
/* A filter pattern is one here-doc line; an embedded newline would let
|
||||
* a crafted pattern (e.g. from a dir-merge/--exclude-from file in an
|
||||
* untrusted tree) forge the "#E#" terminator on its own line and inject
|
||||
* shell commands into the generated replay script. Such a pattern also
|
||||
* can't round-trip the line-delimited here-doc, so refuse it fail-closed
|
||||
* rather than emit an injectable script. */
|
||||
if (ent->pattern && strchr(ent->pattern, '\n')) {
|
||||
rprintf(FERROR, "cannot write a filter rule containing a newline to the batch replay script\n");
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
}
|
||||
write_buf(fd, p, plen);
|
||||
write_sbuf(fd, ent->pattern);
|
||||
if (ent->rflags & FILTRULE_DIRECTORY)
|
||||
@@ -224,27 +242,45 @@ static void write_filter_rules(int fd)
|
||||
/* This sets batch_fd and (for --write-batch) batch_sh_fd. */
|
||||
void open_batch_files(void)
|
||||
{
|
||||
/* --write-batch/--read-batch are operator-supplied; a planted symlink
|
||||
* could truncate+overwrite an arbitrary file (write side) or stream
|
||||
* attacker bytes into the protocol parser (read side). Refuse symlinks
|
||||
* not owned by uid 0 or our euid anywhere in the path. */
|
||||
if (write_batch) {
|
||||
char filename[MAXPATHLEN];
|
||||
|
||||
stringjoin(filename, sizeof filename, batch_name, ".sh", NULL);
|
||||
|
||||
batch_sh_fd = do_open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IXUSR);
|
||||
batch_sh_fd = open_no_attacker_symlinks(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR | S_IXUSR);
|
||||
if (batch_sh_fd < 0) {
|
||||
rsyserr(FERROR, errno, "Batch file %s open error", full_fname(filename));
|
||||
exit_cleanup(RERR_FILESELECT);
|
||||
}
|
||||
|
||||
batch_fd = do_open(batch_name, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
|
||||
/* O_BINARY: the batch stream is binary protocol data; without it
|
||||
* Cygwin et al apply CRLF translation and corrupt it. Unlike
|
||||
* do_open(), open_no_attacker_symlinks passes flags verbatim. */
|
||||
batch_fd = open_no_attacker_symlinks(batch_name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR);
|
||||
} else if (strcmp(batch_name, "-") == 0)
|
||||
batch_fd = STDIN_FILENO;
|
||||
else
|
||||
batch_fd = do_open(batch_name, O_RDONLY, S_IRUSR | S_IWUSR);
|
||||
batch_fd = open_no_attacker_symlinks(batch_name, O_RDONLY | O_BINARY, S_IRUSR | S_IWUSR);
|
||||
|
||||
if (batch_fd < 0) {
|
||||
rsyserr(FERROR, errno, "Batch file %s open error", full_fname(batch_name));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
|
||||
/* --read-batch: the file's bytes drive the protocol parser, so refuse
|
||||
* non-regular files (FIFO, device, socket) at the batch path. */
|
||||
if (!write_batch && batch_fd != STDIN_FILENO) {
|
||||
STRUCT_STAT st;
|
||||
if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) {
|
||||
rprintf(FERROR, "Batch file %s is not a regular file\n",
|
||||
full_fname(batch_name));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* This routine tries to write out an equivalent --read-batch command
|
||||
|
||||
+31
@@ -68,10 +68,26 @@ SIVAL64(char *buf, int pos, int64 val)
|
||||
|
||||
#else /* !CAREFUL_ALIGNMENT */
|
||||
|
||||
/* We don't want false positives about alignment from UBSAN, see:
|
||||
https://github.com/WayneD/rsync/issues/427#issuecomment-1375132291
|
||||
*/
|
||||
|
||||
/* From https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html */
|
||||
#ifndef GCC_VERSION
|
||||
#define GCC_VERSION (__GNUC__ * 10000 \
|
||||
+ __GNUC_MINOR__ * 100 \
|
||||
+ __GNUC_PATCHLEVEL__)
|
||||
#endif
|
||||
|
||||
/* This handles things for architectures like the 386 that can handle alignment errors.
|
||||
* WARNING: This section is dependent on the length of an int32 (and thus a uint32)
|
||||
* being correct (4 bytes)! Set CAREFUL_ALIGNMENT if it is not. */
|
||||
|
||||
#ifdef __clang__
|
||||
__attribute__((no_sanitize("undefined")))
|
||||
#elif GCC_VERSION >= 409
|
||||
__attribute__((no_sanitize_undefined))
|
||||
#endif
|
||||
static inline uint32
|
||||
IVALu(const uchar *buf, int pos)
|
||||
{
|
||||
@@ -83,6 +99,11 @@ IVALu(const uchar *buf, int pos)
|
||||
return *u.num;
|
||||
}
|
||||
|
||||
#ifdef __clang__
|
||||
__attribute__((no_sanitize("undefined")))
|
||||
#elif GCC_VERSION >= 409
|
||||
__attribute__((no_sanitize_undefined))
|
||||
#endif
|
||||
static inline void
|
||||
SIVALu(uchar *buf, int pos, uint32 val)
|
||||
{
|
||||
@@ -94,6 +115,11 @@ SIVALu(uchar *buf, int pos, uint32 val)
|
||||
*u.num = val;
|
||||
}
|
||||
|
||||
#ifdef __clang__
|
||||
__attribute__((no_sanitize("undefined")))
|
||||
#elif GCC_VERSION >= 409
|
||||
__attribute__((no_sanitize_undefined))
|
||||
#endif
|
||||
static inline int64
|
||||
IVAL64(const char *buf, int pos)
|
||||
{
|
||||
@@ -105,6 +131,11 @@ IVAL64(const char *buf, int pos)
|
||||
return *u.num;
|
||||
}
|
||||
|
||||
#ifdef __clang__
|
||||
__attribute__((no_sanitize("undefined")))
|
||||
#elif GCC_VERSION >= 409
|
||||
__attribute__((no_sanitize_undefined))
|
||||
#endif
|
||||
static inline void
|
||||
SIVAL64(char *buf, int pos, int64 val)
|
||||
{
|
||||
|
||||
+18
@@ -87,6 +87,24 @@ struct name_num_obj valid_auth_checksums = {
|
||||
"daemon auth checksum", NULL, 0, 0, valid_auth_checksums_items
|
||||
};
|
||||
|
||||
/* Return the strength rank (0 = strongest) of a daemon-auth digest by name in
|
||||
* valid_auth_checksums_items[], which is listed strongest-first; -1 if the name
|
||||
* is not a supported auth digest on this build. Used by the daemon's
|
||||
* "auth digest" floor to compare the negotiated digest against the minimum. */
|
||||
int auth_digest_rank(const char *name)
|
||||
{
|
||||
struct name_num_item *nni;
|
||||
int rank = 0;
|
||||
|
||||
if (!name || !*name)
|
||||
return -1;
|
||||
for (nni = valid_auth_checksums_items; nni->name; nni++, rank++) {
|
||||
if (strcasecmp(nni->name, name) == 0)
|
||||
return rank;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* These cannot make use of openssl, so they're marked just as built-in */
|
||||
struct name_num_item implied_checksum_md4 =
|
||||
{ CSUM_MD4, NNI_BUILTIN, "md4", NULL };
|
||||
|
||||
@@ -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) {
|
||||
@@ -132,6 +164,7 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
break;
|
||||
case 'a':
|
||||
where |= 0111;
|
||||
topbits |= 06000; /* a+s sets BOTH setuid and setgid (like chmod(1)) */
|
||||
break;
|
||||
case '+':
|
||||
op = CHMOD_ADD;
|
||||
@@ -159,26 +192,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 +272,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 +293,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;
|
||||
|
||||
@@ -269,8 +269,16 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)
|
||||
break;
|
||||
}
|
||||
|
||||
if (called_from_signal_handler)
|
||||
if (called_from_signal_handler) {
|
||||
#ifdef GCOV_COVERAGE
|
||||
/* _exit() bypasses the gcov atexit flush; rsync's generator (and
|
||||
* other processes) normally finish via the signal handler, so
|
||||
* without this they would write no .gcda. Harmless otherwise. */
|
||||
extern void __gcov_dump(void);
|
||||
__gcov_dump();
|
||||
#endif
|
||||
_exit(exit_code);
|
||||
}
|
||||
exit(exit_code);
|
||||
}
|
||||
|
||||
|
||||
+215
-25
@@ -42,6 +42,7 @@ extern int munge_symlinks;
|
||||
extern int use_secure_symlinks;
|
||||
extern int open_noatime;
|
||||
extern int sanitize_paths;
|
||||
extern int daemon_config_filter_file;
|
||||
extern int numeric_ids;
|
||||
extern int filesfrom_fd;
|
||||
extern int remote_protocol;
|
||||
@@ -70,6 +71,8 @@ extern gid_t our_gid;
|
||||
|
||||
char *auth_user;
|
||||
char *daemon_auth_choices;
|
||||
/* read_args() enforces MAX_DAEMON_ARGS and reports "too many daemon arguments"
|
||||
* before a daemon client can grow argv without bound. */
|
||||
int read_only = 0;
|
||||
int module_id = -1;
|
||||
int pid_file_fd = -1;
|
||||
@@ -81,11 +84,32 @@ struct chmod_mode_struct *daemon_chmod_modes;
|
||||
#define EARLY_INPUT_CMD "#early_input="
|
||||
#define EARLY_INPUT_CMDLEN (sizeof EARLY_INPUT_CMD - 1)
|
||||
|
||||
/* Fallback bound on each peer-driven daemon handshake phase when no positive
|
||||
* "timeout" is configured. A module value can shorten the pre-auth and
|
||||
* argument-read phases, but cannot extend either beyond this limit. */
|
||||
#define DAEMON_HANDSHAKE_TIMEOUT 60
|
||||
|
||||
static int daemon_handshake_timeout(int module)
|
||||
{
|
||||
int timeout = lp_timeout(module);
|
||||
|
||||
/* "timeout" is parsed with atoi(), so negative values are possible. */
|
||||
if (timeout <= 0 || timeout > DAEMON_HANDSHAKE_TIMEOUT)
|
||||
timeout = DAEMON_HANDSHAKE_TIMEOUT;
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/* module_dirlen is the length of the module_dir string when in daemon
|
||||
* mode and module_dir is not "/"; otherwise 0. (Note that a chroot-
|
||||
* enabled module can have a non-"/" module_dir these days.) */
|
||||
char *module_dir = NULL;
|
||||
unsigned int module_dirlen = 0;
|
||||
/* An fd held open on the served module root, captured while the daemon is still
|
||||
* positioned there (and privileged) -- so the sender's directory scan can be
|
||||
* confined beneath the module by resolving module-relative paths against this fd,
|
||||
* without re-walking (and re-permission-checking) the absolute module path as the
|
||||
* dropped-privilege module uid. -1 when not a daemon or not yet captured. */
|
||||
int module_dirfd = -1;
|
||||
|
||||
char *full_module_path;
|
||||
|
||||
@@ -158,7 +182,12 @@ static int exchange_protocols(int f_in, int f_out, char *buf, size_t bufsiz, int
|
||||
if (!am_client) {
|
||||
char *motd = lp_motd_file();
|
||||
if (motd && *motd) {
|
||||
FILE *f = fopen(motd, "r");
|
||||
/* 'motd file = PATH': motd content is sent to every client, so
|
||||
* a planted symlink would leak the target's bytes. Refuse
|
||||
* symlinks not owned by uid 0 or our euid. */
|
||||
int motd_fd = open_no_attacker_symlinks(motd, O_RDONLY, 0);
|
||||
FILE *f = motd_fd >= 0 ? fdopen(motd_fd, "r") : NULL;
|
||||
if (!f && motd_fd >= 0) close(motd_fd);
|
||||
while (f && !feof(f)) {
|
||||
int len = fread(buf, 1, bufsiz - 1, f);
|
||||
if (len > 0)
|
||||
@@ -262,19 +291,30 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
|
||||
if (!user)
|
||||
user = getenv("LOGNAME");
|
||||
|
||||
if (exchange_protocols(f_in, f_out, line, sizeof line, 1) < 0)
|
||||
if (exchange_protocols(f_in, f_out, line, sizeof line, 1) < 0) {
|
||||
free(modname);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (early_input_file) {
|
||||
STRUCT_STAT st;
|
||||
FILE *f = fopen(early_input_file, "rb");
|
||||
/* --early-input-file=PATH: refuse symlinks not owned by uid 0 or
|
||||
* our euid anywhere in the path. */
|
||||
int ei_fd = open_no_attacker_symlinks(early_input_file, O_RDONLY, 0);
|
||||
FILE *f = ei_fd >= 0 ? fdopen(ei_fd, "rb") : NULL;
|
||||
if (!f && ei_fd >= 0) close(ei_fd);
|
||||
if (!f || do_fstat(fileno(f), &st) < 0) {
|
||||
rsyserr(FERROR, errno, "failed to open %s", early_input_file);
|
||||
if (f)
|
||||
fclose(f);
|
||||
free(modname);
|
||||
return -1;
|
||||
}
|
||||
early_input_len = st.st_size;
|
||||
if (early_input_len > (int)sizeof line) {
|
||||
rprintf(FERROR, "%s is > %d bytes.\n", early_input_file, (int)sizeof line);
|
||||
fclose(f);
|
||||
free(modname);
|
||||
return -1;
|
||||
}
|
||||
if (early_input_len > 0) {
|
||||
@@ -283,6 +323,8 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
|
||||
int len;
|
||||
if (feof(f)) {
|
||||
rprintf(FERROR, "Early EOF in %s\n", early_input_file);
|
||||
fclose(f);
|
||||
free(modname);
|
||||
return -1;
|
||||
}
|
||||
len = fread(line, 1, early_input_len, f);
|
||||
@@ -359,6 +401,7 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
|
||||
while (1) {
|
||||
if (!read_line_old(f_in, line, sizeof line, 0)) {
|
||||
rprintf(FERROR, "rsync: didn't get server startup line\n");
|
||||
free(modname);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -382,6 +425,7 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
|
||||
rprintf(FERROR, "%s\n", line);
|
||||
/* This is always fatal; the server will now
|
||||
* close the socket. */
|
||||
free(modname);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -543,6 +587,7 @@ static pid_t start_pre_exec(const char *cmd, int *arg_fd_ptr, int *error_fd_ptr)
|
||||
|
||||
status = shell_exec(cmd);
|
||||
|
||||
gcov_flush();
|
||||
if (!WIFEXITED(status))
|
||||
_exit(1);
|
||||
_exit(WEXITSTATUS(status));
|
||||
@@ -758,6 +803,9 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
}
|
||||
|
||||
read_only = lp_read_only(i); /* may also be overridden by auth_server() */
|
||||
/* The module is now known, so its local timeout policy can tighten the
|
||||
* absolute deadline while the claimed slot is awaiting authentication. */
|
||||
set_daemon_handshake_timeout(daemon_handshake_timeout(i));
|
||||
auth_user = auth_server(f_in, f_out, i, host, addr, "@RSYNCD: AUTHREQD ");
|
||||
|
||||
if (!auth_user) {
|
||||
@@ -765,6 +813,10 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
return -1;
|
||||
}
|
||||
set_env_str("RSYNC_USER_NAME", auth_user);
|
||||
/* Do not count local setup or operator hooks against a peer's read time.
|
||||
* In particular, the post-xfer parent and pre-xfer/name-converter children
|
||||
* are forked below and must never inherit an armed asynchronous deadline. */
|
||||
set_daemon_handshake_timeout(0);
|
||||
|
||||
module_id = i;
|
||||
|
||||
@@ -873,6 +925,11 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
} else
|
||||
set_filter_dir(module_dir, module_dirlen);
|
||||
|
||||
/* Everything loaded from here to the end of the exclude block is the
|
||||
* operator's own configuration, so it keeps the ownership walk without the
|
||||
* module-confinement parse_filter_file() applies to peer-driven merges. */
|
||||
daemon_config_filter_file = 1;
|
||||
|
||||
p = lp_filter(module_id);
|
||||
parse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT),
|
||||
XFLG_ABS_IF_SLASH | XFLG_DIR2WILD3);
|
||||
@@ -894,6 +951,8 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
parse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT),
|
||||
XFLG_ABS_IF_SLASH | XFLG_DIR2WILD3 | XFLG_OLD_PREFIXES);
|
||||
|
||||
daemon_config_filter_file = 0;
|
||||
|
||||
log_init(1);
|
||||
|
||||
#if defined HAVE_SETENV || defined HAVE_PUTENV
|
||||
@@ -927,6 +986,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
set_env_num("RSYNC_EXIT_STATUS", status);
|
||||
if (shell_exec(lp_postxfer_exec(module_id)) < 0)
|
||||
status = -1;
|
||||
gcov_flush();
|
||||
_exit(status);
|
||||
}
|
||||
}
|
||||
@@ -980,6 +1040,13 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
if (use_chroot) {
|
||||
/* Cache timezone data before chroot makes /etc/localtime inaccessible */
|
||||
tzset();
|
||||
/* Flush gcov counters now: after chroot the build-tree .gcda
|
||||
* paths are unreachable, so everything this child has executed
|
||||
* so far (the whole rsync_module() pre-chroot path) would
|
||||
* otherwise be lost. Post-chroot coverage from this child is
|
||||
* still unrecordable -- accepted, documented in
|
||||
* testsuite/COVERAGE.md. */
|
||||
gcov_flush();
|
||||
if (chroot(module_chdir)) {
|
||||
rsyserr(FLOG, errno, "chroot(\"%s\") failed", module_chdir);
|
||||
io_printf(f_out, "@ERROR: chroot failed\n");
|
||||
@@ -991,6 +1058,12 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
|
||||
if (!change_dir(module_chdir, CD_NORMAL))
|
||||
return path_failure(f_out, module_chdir, True);
|
||||
/* Pin the module root by identity now -- cwd is the served root and we are
|
||||
* still privileged -- so the sender's later directory scans resolve against
|
||||
* this fd rather than re-walking the absolute module path post-setuid. */
|
||||
#if defined HAVE_FDOPENDIR && defined O_DIRECTORY
|
||||
module_dirfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
#endif
|
||||
if (module_dirlen)
|
||||
sanitize_paths = 1;
|
||||
|
||||
@@ -1008,14 +1081,17 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
}
|
||||
}
|
||||
|
||||
/* Enable secure symlink handling for any non-chrooted daemon module.
|
||||
* This prevents TOCTOU race attacks where an attacker could switch a
|
||||
* directory to a symlink between path validation and file open.
|
||||
* Match the gate used by the do_*_at() wrappers in syscall.c
|
||||
* (am_daemon && !am_chrooted) -- the protection has nothing to do
|
||||
* with symlink munging, so a module configured with
|
||||
* "munge symlinks = false" must still get the secure-open path. */
|
||||
use_secure_symlinks = am_daemon && !am_chrooted;
|
||||
/* Enable secure symlink handling for any non-chrooted daemon module, and
|
||||
* for a chroot module with a /./ inner boundary (module_dirlen) -- there
|
||||
* the kernel chroot confines the outer path but not the inner module, so
|
||||
* the receiver finish/rename path must still resolve beneath the module
|
||||
* root. This prevents TOCTOU race attacks where an attacker could switch a
|
||||
* directory to a symlink between path validation and file open. Match the
|
||||
* gate in secure_relpath_active() (syscall.c) -- the protection has nothing
|
||||
* to do with symlink munging, so a module configured with "munge symlinks =
|
||||
* false" must still get the secure-open path. */
|
||||
use_secure_symlinks = am_daemon && (!am_chrooted || module_dirlen)
|
||||
&& !symlink_optout_allowed();
|
||||
|
||||
if (gid_list.count) {
|
||||
gid_t *gid_array = gid_list.items;
|
||||
@@ -1068,9 +1144,14 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
}
|
||||
}
|
||||
|
||||
/* This deadline is checked only in the read path, so the preceding local
|
||||
* setup and hooks can take as long as necessary. Keep one absolute bound
|
||||
* across both read_args() calls: anonymous modules must not be able to pin
|
||||
* a max-connections slot by trickling an unterminated argument forever. */
|
||||
set_daemon_handshake_timeout(daemon_handshake_timeout(module_id));
|
||||
io_printf(f_out, "@RSYNCD: OK\n");
|
||||
|
||||
read_args(f_in, name, line, sizeof line, rl_nulls, &argv, &argc, &request);
|
||||
read_args(f_in, name, line, sizeof line, rl_nulls, 1, &argv, &argc, &request);
|
||||
orig_argv = argv;
|
||||
|
||||
save_munge_symlinks = munge_symlinks;
|
||||
@@ -1080,11 +1161,12 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
if (protect_args && ret) {
|
||||
orig_early_argv = orig_argv;
|
||||
protect_args = 2;
|
||||
read_args(f_in, name, line, sizeof line, 1, &argv, &argc, &request);
|
||||
read_args(f_in, name, line, sizeof line, 1, 0, &argv, &argc, &request);
|
||||
orig_argv = argv;
|
||||
ret = parse_arguments(&argc, (const char ***) &argv);
|
||||
} else
|
||||
orig_early_argv = NULL;
|
||||
set_daemon_handshake_timeout(0);
|
||||
|
||||
/* The default is to use the user's setting unless the module sets True or False. */
|
||||
if (lp_open_noatime(module_id) >= 0)
|
||||
@@ -1224,14 +1306,20 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
return 0;
|
||||
}
|
||||
|
||||
static BOOL namecvt_safe_token(const char *s);
|
||||
|
||||
BOOL namecvt_call(const char *cmd, const char **name_p, id_t *id_p)
|
||||
{
|
||||
char buf[1024];
|
||||
int got, len;
|
||||
|
||||
if (*name_p)
|
||||
if (*name_p) {
|
||||
if (!namecvt_safe_token(*name_p)) {
|
||||
rprintf(FERROR, "invalid name-converter token: %s\n", *name_p);
|
||||
return False;
|
||||
}
|
||||
len = snprintf(buf, sizeof buf, "%s %s\n", cmd, *name_p);
|
||||
else
|
||||
} else
|
||||
len = snprintf(buf, sizeof buf, "%s %ld\n", cmd, (long)*id_p);
|
||||
if (len >= (int)sizeof buf) {
|
||||
rprintf(FERROR, "namecvt_call() request was too large.\n");
|
||||
@@ -1248,14 +1336,39 @@ BOOL namecvt_call(const char *cmd, const char **name_p, id_t *id_p)
|
||||
if (!read_line_old(namecvt_fd_ans, buf, sizeof buf, 0))
|
||||
return False;
|
||||
|
||||
if (*name_p)
|
||||
*id_p = (id_t)atol(buf);
|
||||
else
|
||||
if (*name_p) {
|
||||
/* Name-to-id: an unknown name returns an empty line and atol("")=0
|
||||
* would map it to root, so validate strictly below (all digits, no
|
||||
* ERANGE, fits id_t). */
|
||||
const char *p;
|
||||
unsigned long v;
|
||||
if (!*buf)
|
||||
return False;
|
||||
for (p = buf; *p; p++) {
|
||||
if (*p < '0' || *p > '9')
|
||||
return False;
|
||||
}
|
||||
errno = 0;
|
||||
v = strtoul(buf, NULL, 10);
|
||||
if (errno == ERANGE || v > (unsigned long)(id_t)-1)
|
||||
return False;
|
||||
*id_p = (id_t)v;
|
||||
} else
|
||||
*name_p = strdup(buf);
|
||||
|
||||
return True;
|
||||
}
|
||||
|
||||
static BOOL namecvt_safe_token(const char *s)
|
||||
{
|
||||
for (; *s; s++) {
|
||||
unsigned char ch = (unsigned char)*s;
|
||||
if (ch < ' ' || ch == 0x7f)
|
||||
return False;
|
||||
}
|
||||
return True;
|
||||
}
|
||||
|
||||
/* send a list of available modules to the client. Don't list those
|
||||
with "list = False". */
|
||||
static void send_listing(int fd)
|
||||
@@ -1272,6 +1385,18 @@ static void send_listing(int fd)
|
||||
io_printf(fd,"@RSYNCD: EXIT\n");
|
||||
}
|
||||
|
||||
static int proxy_peer_allowed(int fd)
|
||||
{
|
||||
const char *host = undetermined_hostname;
|
||||
const char *addr = client_addr(fd);
|
||||
|
||||
if (!allow_proxy_protocol_peer(lp_proxy_protocol_hosts(), addr, &host)) {
|
||||
rprintf(FLOG, "proxy protocol rejected from untrusted peer %s (%s)\n", host, addr);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int load_config(int globals_only)
|
||||
{
|
||||
if (!config_file) {
|
||||
@@ -1309,8 +1434,16 @@ int start_daemon(int f_in, int f_out)
|
||||
if (!load_config(0))
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
|
||||
if (lp_proxy_protocol() && !read_proxy_protocol_header(f_in))
|
||||
return -1;
|
||||
/* Bound the handshake before ANY peer input is read -- the PROXY-protocol
|
||||
* header below is peer-supplied too, and was previously unbounded. An
|
||||
* rsh-run daemon is not a listener and has no shared slot to exhaust. */
|
||||
if (am_daemon > 0)
|
||||
set_daemon_handshake_timeout(daemon_handshake_timeout(-1));
|
||||
|
||||
if (lp_proxy_protocol()) {
|
||||
if (!proxy_peer_allowed(f_in) || !read_proxy_protocol_header(f_in))
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Do reverse DNS lookup before chroot/setuid. The result is cached,
|
||||
* so the later client_name() call will use this cached value. This
|
||||
@@ -1397,6 +1530,7 @@ int start_daemon(int f_in, int f_out)
|
||||
set_nonblocking(f_in);
|
||||
}
|
||||
|
||||
|
||||
if (exchange_protocols(f_in, f_out, line, sizeof line, 0) < 0)
|
||||
return -1;
|
||||
|
||||
@@ -1451,21 +1585,58 @@ static void create_pid_file(void)
|
||||
char pidbuf[32];
|
||||
STRUCT_STAT st1, st2;
|
||||
char *fail = NULL;
|
||||
const char *base = pid_file;
|
||||
int pdfd = -1;
|
||||
|
||||
if (!pid_file || !*pid_file)
|
||||
return;
|
||||
|
||||
#ifdef O_NOFOLLOW
|
||||
#define SAFE_OPEN_FLAGS (O_CREAT|O_NOFOLLOW)
|
||||
#define SAFE_NOFOLLOW O_NOFOLLOW
|
||||
#else
|
||||
#define SAFE_OPEN_FLAGS (O_CREAT)
|
||||
#define SAFE_NOFOLLOW 0
|
||||
#endif
|
||||
|
||||
#ifdef AT_FDCWD
|
||||
/* Pin the parent directory so the existence check, open and re-stat below
|
||||
* all resolve the leaf against one stable directory inode, removing the
|
||||
* lstat->open path race. The parent is operator-configured and trusted, so
|
||||
* it is opened following symlinks (e.g. a /var/run -> /run); only the leaf
|
||||
* is opened/checked O_NOFOLLOW (the do_*_atfd wrappers force that). */
|
||||
{
|
||||
const char *slash = strrchr(pid_file, '/');
|
||||
char dirbuf[MAXPATHLEN];
|
||||
const char *dir = ".";
|
||||
if (slash) {
|
||||
size_t dlen = slash == pid_file ? 1 : (size_t)(slash - pid_file);
|
||||
if (dlen >= sizeof dirbuf) {
|
||||
rprintf(FLOG, "pid file path is too long: %s\n", pid_file);
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
memcpy(dirbuf, pid_file, dlen);
|
||||
dirbuf[dlen] = '\0';
|
||||
dir = dirbuf;
|
||||
base = slash + 1;
|
||||
}
|
||||
if ((pdfd = do_open(dir, O_RDONLY|O_DIRECTORY, 0)) < 0) {
|
||||
rsyserr(FLOG, errno, "failed to open pid-file directory \"%s\"", dir);
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
}
|
||||
#define PID_LSTAT(stp) do_lstat_atfd(pdfd, base, stp)
|
||||
#define PID_UNLINK() do_unlink_atfd(pdfd, base, 0)
|
||||
#define PID_OPEN() do_open_atfd(pdfd, base, O_RDWR|O_CREAT, 0664)
|
||||
#else
|
||||
#define PID_LSTAT(stp) do_lstat(base, stp)
|
||||
#define PID_UNLINK() unlink(base)
|
||||
#define PID_OPEN() do_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664)
|
||||
#endif
|
||||
|
||||
/* These tests make sure that a temp-style lock dir is handled safely. */
|
||||
st1.st_mode = 0;
|
||||
if (do_lstat(pid_file, &st1) == 0 && !S_ISREG(st1.st_mode) && unlink(pid_file) < 0)
|
||||
if (PID_LSTAT(&st1) == 0 && !S_ISREG(st1.st_mode) && PID_UNLINK() < 0)
|
||||
fail = "unlink";
|
||||
else if ((pid_file_fd = do_open(pid_file, O_RDWR|SAFE_OPEN_FLAGS, 0664)) < 0)
|
||||
else if ((pid_file_fd = PID_OPEN()) < 0)
|
||||
fail = S_ISREG(st1.st_mode) ? "open" : "create";
|
||||
else if (!lock_range(pid_file_fd, 0, 4))
|
||||
fail = "lock";
|
||||
@@ -1473,7 +1644,7 @@ static void create_pid_file(void)
|
||||
fail = "fstat opened";
|
||||
else if (st1.st_size > (int)sizeof pidbuf)
|
||||
fail = "find small";
|
||||
else if (do_lstat(pid_file, &st2) < 0)
|
||||
else if (PID_LSTAT(&st2) < 0)
|
||||
fail = "lstat";
|
||||
else if (!S_ISREG(st1.st_mode))
|
||||
fail = "avoid file overwrite race for";
|
||||
@@ -1497,6 +1668,13 @@ static void create_pid_file(void)
|
||||
cleanup_set_pid(pid); /* Mark the file for removal on exit, even if the write failed. */
|
||||
}
|
||||
|
||||
#undef PID_LSTAT
|
||||
#undef PID_UNLINK
|
||||
#undef PID_OPEN
|
||||
#undef SAFE_NOFOLLOW
|
||||
if (pdfd >= 0)
|
||||
close(pdfd);
|
||||
|
||||
if (fail) {
|
||||
char msg[1024];
|
||||
snprintf(msg, sizeof msg, "failed to %s pid file %s: %s\n",
|
||||
@@ -1520,6 +1698,7 @@ static void become_daemon(void)
|
||||
fprintf(stderr, "failed to fork: %s\n", strerror(errno));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
gcov_flush();
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
@@ -1565,6 +1744,17 @@ int daemon_main(void)
|
||||
}
|
||||
set_dparams(0);
|
||||
|
||||
/* "proxy protocol = true" with no trusted-proxy list rejects every
|
||||
* connection as an untrusted proxy peer (fail-closed). That is intended,
|
||||
* but silent at startup, so warn the operator while stderr is still open. */
|
||||
if (lp_proxy_protocol()
|
||||
&& (!lp_proxy_protocol_hosts() || !*lp_proxy_protocol_hosts())) {
|
||||
rprintf(FWARNING,
|
||||
"\"proxy protocol = true\" but \"proxy protocol hosts\" is unset:"
|
||||
" all connections will be rejected as untrusted proxy peers."
|
||||
" Set \"proxy protocol hosts\" to your trusted proxy's address.\n");
|
||||
}
|
||||
|
||||
if (no_detach)
|
||||
create_pid_file();
|
||||
else
|
||||
|
||||
@@ -351,7 +351,7 @@ static int parse_negotiate_str(struct name_num_obj *nno, char *tmpbuf)
|
||||
continue;
|
||||
ret = nni;
|
||||
best = nno->saw[nni->num];
|
||||
if (best == 1 || am_server) /* The server side stops at the first acceptable client choice */
|
||||
if (best == 1) /* Can't improve on our own #1 preference */
|
||||
break;
|
||||
}
|
||||
if (ret) {
|
||||
@@ -526,8 +526,11 @@ static void send_negotiate_str(int f_out, struct name_num_obj *nno, int ntype)
|
||||
rprintf(FINFO, "Client %s list (on client): %s\n", nno->type, tmpbuf);
|
||||
}
|
||||
|
||||
/* Each side sends their list of valid names to the other side and then both sides
|
||||
* pick the first name in the client's list that is also in the server's list. */
|
||||
/* Each side sends their list of valid names to the other side and then each
|
||||
* side picks its own most-preferred name that also appears in the peer's
|
||||
* list. Honest peers emit their list in table (strongest-first) order via
|
||||
* get_default_nno_list(), so both sides converge on the strongest mutual
|
||||
* choice; a peer that front-loads a weaker name only desyncs itself. */
|
||||
if (do_negotiated_strings)
|
||||
write_vstring(f_out, tmpbuf, len);
|
||||
}
|
||||
@@ -585,14 +588,13 @@ void setup_protocol(int f_out,int f_in)
|
||||
pathname_ndx = (file_extra_cnt += PTR_EXTRA_CNT);
|
||||
else
|
||||
depth_ndx = ++file_extra_cnt;
|
||||
if (preserve_uid)
|
||||
uid_ndx = ++file_extra_cnt;
|
||||
if (preserve_gid)
|
||||
gid_ndx = ++file_extra_cnt;
|
||||
if (preserve_acls && !am_sender)
|
||||
acls_ndx = ++file_extra_cnt;
|
||||
if (preserve_xattrs)
|
||||
xattrs_ndx = ++file_extra_cnt;
|
||||
/* uid_ndx/gid_ndx/acls_ndx/xattrs_ndx are assigned AFTER
|
||||
* check_batch_flags() below: a batch file's stream-flags can flip
|
||||
* preserve_uid/gid/acls/xattrs on, and computing the *_ndx slots
|
||||
* before that leaves e.g. preserve_xattrs=1 with xattrs_ndx=0 -- so
|
||||
* F_XATTR(file) (= REQ_EXTRA(file, 0)) writes at offset 0 of every
|
||||
* file_struct, clobbering file->dirname. Nothing between here and
|
||||
* check_batch_flags() reads file_extra_cnt or the *_ndx values. */
|
||||
|
||||
if (am_server)
|
||||
set_allow_inc_recurse();
|
||||
@@ -639,6 +641,15 @@ void setup_protocol(int f_out,int f_in)
|
||||
if (read_batch)
|
||||
check_batch_flags();
|
||||
|
||||
if (preserve_uid)
|
||||
uid_ndx = ++file_extra_cnt;
|
||||
if (preserve_gid)
|
||||
gid_ndx = ++file_extra_cnt;
|
||||
if (preserve_acls && !am_sender)
|
||||
acls_ndx = ++file_extra_cnt;
|
||||
if (preserve_xattrs)
|
||||
xattrs_ndx = ++file_extra_cnt;
|
||||
|
||||
if (!saw_stderr_opt && protocol_version <= 28 && am_server)
|
||||
msgs2stderr = 0; /* The client side may not have stderr setup for us. */
|
||||
|
||||
|
||||
+150
-47
@@ -5,7 +5,7 @@ AC_INIT([rsync],[ ],[https://rsync.samba.org/bug-tracking.html])
|
||||
AC_C_BIGENDIAN
|
||||
AC_HEADER_DIRENT
|
||||
AC_HEADER_SYS_WAIT
|
||||
AC_CHECK_HEADERS(sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \
|
||||
AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \
|
||||
unistd.h utime.h compat.h sys/param.h ctype.h sys/wait.h sys/stat.h \
|
||||
sys/ioctl.h sys/filio.h string.h stdlib.h sys/socket.h sys/mode.h grp.h \
|
||||
sys/un.h sys/attr.h arpa/inet.h arpa/nameser.h locale.h sys/types.h \
|
||||
@@ -13,7 +13,7 @@ AC_CHECK_HEADERS(sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \
|
||||
sys/acl.h acl/libacl.h attr/xattr.h sys/xattr.h sys/extattr.h dl.h \
|
||||
popt.h popt/popt.h linux/falloc.h netinet/in_systm.h netgroup.h \
|
||||
zlib.h xxhash.h openssl/md4.h openssl/md5.h zstd.h lz4.h sys/file.h \
|
||||
bsd/string.h)
|
||||
sys/resource.h bsd/string.h)
|
||||
AC_CHECK_HEADERS([netinet/ip.h], [], [], [[#include <netinet/in.h>]])
|
||||
AC_HEADER_MAJOR_FIXED
|
||||
|
||||
@@ -82,6 +82,28 @@ if test x"$enable_profile" = x"yes"; then
|
||||
CFLAGS="$CFLAGS -pg"
|
||||
fi
|
||||
|
||||
dnl Coverage build (gcov) for `make coverage`. NOTE: --enable-profile above is
|
||||
dnl gprof (-pg) and is NOT coverage. -O0 keeps branch coverage meaningful;
|
||||
dnl -fprofile-update=atomic keeps the shared .gcda counters correct while the
|
||||
dnl suite runs many rsync processes in parallel.
|
||||
AC_ARG_ENABLE(coverage,
|
||||
AS_HELP_STRING([--enable-coverage],[build with gcov instrumentation for `make coverage`]))
|
||||
if test x"$enable_coverage" = x"yes"; then
|
||||
CFLAGS="$CFLAGS --coverage -fprofile-update=atomic -O0"
|
||||
CXXFLAGS="$CXXFLAGS --coverage -fprofile-update=atomic -O0"
|
||||
LDFLAGS="$LDFLAGS --coverage"
|
||||
AC_DEFINE([GCOV_COVERAGE], 1,
|
||||
[Flush gcov counters at exit_cleanup: rsync's children exit via _exit(), which bypasses the gcov atexit handler, so without this no .gcda is written for the receiver/generator/daemon-worker processes.])
|
||||
fi
|
||||
|
||||
dnl openat2(RESOLVE_BENEATH) is used on Linux 5.6+ for the secure resolver.
|
||||
dnl --disable-openat2 forces the portable per-component O_NOFOLLOW fallback to
|
||||
dnl run as the primary resolver on ordinary Linux, so that tier is exercised
|
||||
dnl (and coverage-counted) without needing a pre-5.6 kernel. Behaviour-neutral
|
||||
dnl by default (the knob only REMOVES a tier when explicitly disabled).
|
||||
AC_ARG_ENABLE(openat2,
|
||||
AS_HELP_STRING([--disable-openat2],[do not use Linux openat2(RESOLVE_BENEATH); force the portable resolver (for exercising the fallback tier)]))
|
||||
|
||||
AC_MSG_CHECKING([if md2man can create manpages])
|
||||
if test x"$ac_cv_path_PYTHON3" = x; then
|
||||
AC_MSG_RESULT(no - python3 not found)
|
||||
@@ -331,6 +353,28 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]], [[return 0;]])],
|
||||
CFLAGS="$OLD_CFLAGS"
|
||||
AC_SUBST(NOEXECSTACK)
|
||||
|
||||
dnl We need both the SYS_openat2 syscall number and <linux/openat2.h> (for
|
||||
dnl struct open_how / RESOLVE_BENEATH); some setups have one without the other.
|
||||
AC_CACHE_CHECK([for openat2],rsync_cv_HAVE_OPENAT2,[
|
||||
AC_COMPILE_IFELSE([
|
||||
AC_LANG_PROGRAM([[
|
||||
#include <sys/syscall.h>
|
||||
#include <linux/openat2.h>
|
||||
]], [[
|
||||
struct open_how how;
|
||||
how.resolve = RESOLVE_BENEATH;
|
||||
return SYS_openat2 + (int)how.resolve;
|
||||
]])
|
||||
],
|
||||
[rsync_cv_HAVE_OPENAT2=yes], [rsync_cv_HAVE_OPENAT2=no])
|
||||
])
|
||||
if test x"$enable_openat2" != x"no"; then
|
||||
if test x"$rsync_cv_HAVE_OPENAT2" = x"yes"; then
|
||||
AC_DEFINE([HAVE_OPENAT2], 1,
|
||||
[Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.])
|
||||
fi
|
||||
fi
|
||||
|
||||
# arrgh. libc in some old debian version screwed up the largefile
|
||||
# stuff, getting byte range locking wrong
|
||||
AC_CACHE_CHECK([for broken largefile support],rsync_cv_HAVE_BROKEN_LARGEFILE,[
|
||||
@@ -388,21 +432,17 @@ AS_HELP_STRING([--disable-ipv6],[disable to omit ipv6 support]),
|
||||
;;
|
||||
esac ],
|
||||
|
||||
AC_RUN_IFELSE([AC_LANG_SOURCE([[ /* AF_INET6 availability check */
|
||||
#include <stdlib.h>
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
int main()
|
||||
{
|
||||
if (socket(AF_INET6, SOCK_STREAM, 0) < 0)
|
||||
exit(1);
|
||||
else
|
||||
exit(0);
|
||||
}
|
||||
#include <netinet/in.h>
|
||||
]], [[
|
||||
struct sockaddr_in6 sa6;
|
||||
(void)sa6;
|
||||
(void)AF_INET6;
|
||||
]])],
|
||||
[AC_MSG_RESULT(yes)
|
||||
AC_DEFINE(INET6, 1, true if you have IPv6)],
|
||||
[AC_MSG_RESULT(no)],
|
||||
AC_DEFINE(INET6, 1, [true if you have IPv6])],
|
||||
[AC_MSG_RESULT(no)]
|
||||
))
|
||||
|
||||
@@ -869,6 +909,19 @@ AC_HAVE_TYPE([struct stat64], [#include <stdio.h>
|
||||
|
||||
# if we can't find strcasecmp, look in -lresolv (for Unixware at least)
|
||||
#
|
||||
dnl rsync's I/O readiness loops use poll() rather than select() so that a
|
||||
dnl file descriptor at or above FD_SETSIZE cannot overflow an fd_set (which
|
||||
dnl is undefined behaviour and could hang the transfer). poll() is in
|
||||
dnl POSIX.1-2001; fail early and clearly if this target lacks it.
|
||||
dnl
|
||||
dnl io.c and socket.c include <poll.h> unconditionally, so the HEADER has to
|
||||
dnl be required too: a system that exposes poll() through some other header
|
||||
dnl would otherwise pass configure and then fail to compile.
|
||||
AC_CHECK_FUNCS([poll], , [AC_MSG_ERROR([rsync requires poll(); please report the platform to the rsync developers])])
|
||||
if test x"$ac_cv_header_poll_h" != x"yes"; then
|
||||
AC_MSG_ERROR([rsync requires <poll.h>; please report the platform to the rsync developers])
|
||||
fi
|
||||
|
||||
AC_CHECK_FUNCS(strcasecmp)
|
||||
if test x"$ac_cv_func_strcasecmp" = x"no"; then
|
||||
AC_CHECK_LIB(resolv, strcasecmp)
|
||||
@@ -886,17 +939,30 @@ dnl AC_FUNC_MEMCMP
|
||||
|
||||
AC_FUNC_UTIME_NULL
|
||||
AC_FUNC_ALLOCA
|
||||
AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo \
|
||||
AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo fdopendir \
|
||||
getrlimit setrlimit \
|
||||
fchmod fstat ftruncate strchr readlink link utime utimes lutimes strftime \
|
||||
chflags getattrlist mktime innetgr linkat \
|
||||
chflags getattrlist mktime innetgr linkat mknodat mkfifoat \
|
||||
memmove lchown vsnprintf snprintf vasprintf asprintf setsid strpbrk \
|
||||
strlcat strlcpy stpcpy strtol mallinfo mallinfo2 getgroups setgroups geteuid getegid \
|
||||
setlocale setmode open64 lseek64 mkstemp64 mtrace va_copy __va_copy \
|
||||
seteuid strerror putenv iconv_open locale_charset nl_langinfo getxattr \
|
||||
extattr_get_link sigaction sigprocmask setattrlist getgrouplist \
|
||||
initgroups utimensat posix_fallocate attropen setvbuf nanosleep usleep \
|
||||
initgroups utimensat futimens posix_fallocate attropen setvbuf nanosleep usleep \
|
||||
setenv unsetenv)
|
||||
|
||||
dnl dirfd() is a macro or static inline on several systems (the BSDs), so the
|
||||
dnl default AC_CHECK_FUNCS link probe -- which declares `char dirfd(void);` and
|
||||
dnl links against a bare symbol -- gives a false negative there. Probe it with a
|
||||
dnl real compile+link that includes <dirent.h> and actually calls dirfd().
|
||||
AC_CACHE_CHECK([for dirfd], rsync_cv_HAVE_DIRFD,
|
||||
[AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include <dirent.h>]],
|
||||
[[DIR *d = opendir("."); return d ? dirfd(d) < -1 : 0;]])],
|
||||
[rsync_cv_HAVE_DIRFD=yes], [rsync_cv_HAVE_DIRFD=no])])
|
||||
if test x"$rsync_cv_HAVE_DIRFD" = x"yes"; then
|
||||
AC_DEFINE([HAVE_DIRFD], 1, [Define to 1 if you have a working dirfd() (function or macro).])
|
||||
fi
|
||||
|
||||
dnl cygwin iconv.h defines iconv_open as libiconv_open
|
||||
if test x"$ac_cv_func_iconv_open" != x"yes"; then
|
||||
AC_CHECK_FUNC(libiconv_open, [ac_cv_func_iconv_open=yes; AC_DEFINE(HAVE_ICONV_OPEN, 1)])
|
||||
@@ -1218,37 +1284,14 @@ if test x"$rsync_cv_HAVE_SECURE_MKSTEMP" = x"yes"; then
|
||||
fi
|
||||
|
||||
|
||||
AC_CACHE_CHECK([if mknod creates FIFOs],rsync_cv_MKNOD_CREATES_FIFOS,[
|
||||
AC_RUN_IFELSE([AC_LANG_SOURCE([[
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#if HAVE_UNISTD_H
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
int main(void) { int rc, ec; char *fn = "fifo-test";
|
||||
unlink(fn); rc = mknod(fn,S_IFIFO,0600); ec = errno; unlink(fn);
|
||||
if (rc) {printf("(%d %d) ",rc,ec); return ec;}
|
||||
return 0;}]])],[rsync_cv_MKNOD_CREATES_FIFOS=yes],[rsync_cv_MKNOD_CREATES_FIFOS=no],[rsync_cv_MKNOD_CREATES_FIFOS=cross])])
|
||||
if test x"$rsync_cv_MKNOD_CREATES_FIFOS" = x"yes"; then
|
||||
AC_DEFINE(MKNOD_CREATES_FIFOS, 1, [Define to 1 if mknod() can create FIFOs.])
|
||||
fi
|
||||
|
||||
AC_CACHE_CHECK([if mknod creates sockets],rsync_cv_MKNOD_CREATES_SOCKETS,[
|
||||
AC_RUN_IFELSE([AC_LANG_SOURCE([[
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#if HAVE_UNISTD_H
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
int main(void) { int rc, ec; char *fn = "sock-test";
|
||||
unlink(fn); rc = mknod(fn,S_IFSOCK,0600); ec = errno; unlink(fn);
|
||||
if (rc) {printf("(%d %d) ",rc,ec); return ec;}
|
||||
return 0;}]])],[rsync_cv_MKNOD_CREATES_SOCKETS=yes],[rsync_cv_MKNOD_CREATES_SOCKETS=no],[rsync_cv_MKNOD_CREATES_SOCKETS=cross])])
|
||||
if test x"$rsync_cv_MKNOD_CREATES_SOCKETS" = x"yes"; then
|
||||
AC_DEFINE(MKNOD_CREATES_SOCKETS, 1, [Define to 1 if mknod() can create sockets.])
|
||||
fi
|
||||
# Whether mknod()/mknodat() can create a FIFO or a unix-domain socket is a
|
||||
# property of the target filesystem, not a build-time constant -- e.g. mknod
|
||||
# makes sockets on Linux but not the BSDs/macOS/Solaris, and a single transfer
|
||||
# can write to filesystems with different capabilities. So rsync no longer
|
||||
# probes this at configure time (a run-test that also misfired when cross-
|
||||
# compiling); do_mknod*() just try mknod[at]() and, on failure, fall back to
|
||||
# mkfifo[at]()/socket+bind() per call. We only need the libc symbols, checked
|
||||
# above via AC_CHECK_FUNCS (mknod mknodat mkfifo mkfifoat) -- all link tests.
|
||||
|
||||
#
|
||||
# The following test was mostly taken from the tcl/tk plus patches
|
||||
@@ -1422,6 +1465,66 @@ else
|
||||
esac
|
||||
fi
|
||||
|
||||
#################################################
|
||||
# On Linux, POSIX ACLs are stored as the "system.posix_acl_{access,default}"
|
||||
# xattrs, so we can get/set them through a held O_NOFOLLOW fd (fsetxattr) or a
|
||||
# dirfd+leaf (setxattrat, AT_SYMLINK_NOFOLLOW) instead of the path-based libacl
|
||||
# acl_*_file() calls -- making the operation safe against a parent-symlink race.
|
||||
# This needs POSIX ACLs and the f/at xattr syscalls, which on Linux are
|
||||
# available whenever <sys/xattr.h> (or <attr/xattr.h>) is -- independent of the
|
||||
# -X feature (--disable-xattr-support), so we gate on the header, not
|
||||
# enable_xattr_support.
|
||||
AH_TEMPLATE([SUPPORT_ACL_FD],
|
||||
[Define to 1 to do POSIX ACL ops via fd/at xattr syscalls (lib/acl.c)])
|
||||
AH_TEMPLATE([HAVE_XATTRAT_SYSCALLS],
|
||||
[Define to 1 if the setxattrat/getxattrat/removexattrat syscalls are available])
|
||||
if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes" \
|
||||
&& { test x"$ac_cv_header_sys_xattr_h" = x"yes" || test x"$ac_cv_header_attr_xattr_h" = x"yes"; }; then
|
||||
case "$host_os" in
|
||||
*linux*)
|
||||
AC_DEFINE(SUPPORT_ACL_FD, 1)
|
||||
AC_CACHE_CHECK([for SYS_setxattrat],rsync_cv_have_sys_setxattrat,[
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <sys/syscall.h>
|
||||
#include <stdint.h>
|
||||
#ifdef HAVE_UNISTD_H
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
struct xattr_args { uint64_t value; uint32_t size; uint32_t flags; };]],
|
||||
[[struct xattr_args a; a.value = 0; a.size = 0; a.flags = 0;
|
||||
syscall(SYS_setxattrat, 0, ".", 0, "n", &a, sizeof a);
|
||||
syscall(SYS_getxattrat, 0, ".", 0, "n", &a, sizeof a);
|
||||
syscall(SYS_removexattrat, 0, ".", 0, "n");]])],[rsync_cv_have_sys_setxattrat=yes],[rsync_cv_have_sys_setxattrat=no])])
|
||||
if test x"$rsync_cv_have_sys_setxattrat" = x"yes"; then
|
||||
AC_DEFINE(HAVE_XATTRAT_SYSCALLS, 1)
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
#################################################
|
||||
# Detect a patched libacl providing the race-safe
|
||||
# *_at ACL entry points (acl_get_file_at/acl_set_file_at/acl_delete_def_file_at,
|
||||
# ACL_1.3, unreleased upstream). When present we route the race-safe ACL get/
|
||||
# set/delete through them on Linux -- race-safe on every kernel (6.13+ uses
|
||||
# *xattrat; older uses libacl's /proc/self/fd compat). A stock -lacl lacks these
|
||||
# symbols, so this stays undefined and the build falls back to lib/acl.c;
|
||||
# detection must therefore run against the patched lib (CPPFLAGS/LDFLAGS).
|
||||
AH_TEMPLATE([HAVE_LIBACL_AT],
|
||||
[Define to 1 if libacl provides acl_get_file_at/acl_set_file_at/acl_delete_def_file_at])
|
||||
if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes"; then
|
||||
case "$host_os" in
|
||||
*linux*)
|
||||
AC_CHECK_LIB(acl, acl_get_file_at, [rsync_have_libacl_at=yes], [rsync_have_libacl_at=no])
|
||||
if test x"$rsync_have_libacl_at" = x"yes"; then
|
||||
AC_CHECK_FUNCS([acl_set_file_at acl_delete_def_file_at], [], [rsync_have_libacl_at=no])
|
||||
fi
|
||||
if test x"$rsync_have_libacl_at" = x"yes"; then
|
||||
AC_DEFINE(HAVE_LIBACL_AT, 1)
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if test x"$enable_acl_support" = x"no" || test x"$enable_xattr_support" = x"no" || test x"$enable_iconv" = x"no"; then
|
||||
AC_MSG_CHECKING([whether $CC supports -Wno-unused-parameter])
|
||||
OLD_CFLAGS="$CFLAGS"
|
||||
|
||||
+3
-1
@@ -30,7 +30,9 @@ int claim_connection(char *fname, int max_connections)
|
||||
if (max_connections == 0)
|
||||
return 1;
|
||||
|
||||
if ((fd = open(fname, O_RDWR|O_CREAT, 0600)) < 0)
|
||||
/* 'lock file = PATH': refuse symlinks not owned by uid 0 or our euid so
|
||||
* a planted parent can't redirect the root daemon's O_CREAT open. */
|
||||
if ((fd = open_no_attacker_symlinks(fname, O_RDWR|O_CREAT, 0600)) < 0)
|
||||
return 0;
|
||||
|
||||
/* Find a free spot. */
|
||||
|
||||
+8
-1
@@ -84,7 +84,14 @@ BEGIN {
|
||||
defines = defines "\t" vtype " " name ";\n"
|
||||
values = values "\t" $0 ", /* " name " */\n"
|
||||
parms = parms " {\"" pubname "\", P_" ptype psect name ", " enum ", 0},\n"
|
||||
accessors = accessors "FN_" sect "_" atype "(lp_" name ", " name ")\n"
|
||||
# The shell-executed hook params (whose %RSYNC_*% expansion is fed to
|
||||
# /bin/sh) use the _SHELL accessor, which single-quotes peer-controlled
|
||||
# values to prevent injection. Ordinary string params must NOT quote --
|
||||
# it would corrupt a documented `path = /home/%RSYNC_USER_NAME%` etc.
|
||||
if (atype == "STRING" && (name == "early_exec" || name == "prexfer_exec" || name == "postxfer_exec" || name == "name_converter"))
|
||||
accessors = accessors "FN_" sect "_STRING_SHELL(lp_" name ", " name ")\n"
|
||||
else
|
||||
accessors = accessors "FN_" sect "_" atype "(lp_" name ", " name ")\n"
|
||||
|
||||
if (vtype == "char*") {
|
||||
exps = exps "\tBOOL " name "_EXP;\n"
|
||||
|
||||
@@ -6,6 +6,7 @@ STRING daemon_gid NULL
|
||||
STRING daemon_uid NULL
|
||||
STRING motd_file NULL
|
||||
STRING pid_file NULL
|
||||
STRING proxy_protocol_hosts NULL
|
||||
STRING socket_options NULL
|
||||
|
||||
INTEGER listen_backlog 5
|
||||
@@ -15,6 +16,7 @@ BOOL proxy_protocol False
|
||||
|
||||
Locals: =================================================================
|
||||
|
||||
STRING auth_digest NULL
|
||||
STRING auth_users NULL
|
||||
STRING charset NULL
|
||||
STRING comment NULL
|
||||
@@ -55,6 +57,7 @@ BOOL fake_super False
|
||||
BOOL forward_lookup True
|
||||
BOOL ignore_errors False
|
||||
BOOL ignore_nonreadable False
|
||||
BOOL insecure_links False
|
||||
BOOL list True
|
||||
BOOL read_only True
|
||||
BOOL reverse_lookup True
|
||||
|
||||
@@ -34,6 +34,49 @@ int ignore_perishable = 0;
|
||||
int non_perishable_cnt = 0;
|
||||
int skipped_deletes = 0;
|
||||
|
||||
/* Held fd of the directory whose contents delete_dir_contents() is currently
|
||||
* removing, so delete_item()'s per-entry rmdir/unlink/chmod go through it
|
||||
* instead of re-resolving the full path for every entry. Set (with save/
|
||||
* restore across the recursion) around the delete loop; -1 outside a recursive
|
||||
* delete or when the secure resolver is gated off (chroot / non-receiver) or
|
||||
* the path doesn't live directly in that dir. */
|
||||
static int del_dirfd = -1;
|
||||
static const char *del_dir_prefix;
|
||||
static int del_dir_prefix_len;
|
||||
|
||||
/* If `path` is a single component directly inside the dir being deleted,
|
||||
* point *leaf at its basename and return the held dir fd; else return -1. */
|
||||
static int del_held_dfd(const char *path, const char **leaf)
|
||||
{
|
||||
if (del_dirfd >= 0
|
||||
&& strncmp(path, del_dir_prefix, del_dir_prefix_len) == 0
|
||||
&& path[del_dir_prefix_len] == '/'
|
||||
&& strchr(path + del_dir_prefix_len + 1, '/') == NULL) {
|
||||
*leaf = path + del_dir_prefix_len + 1;
|
||||
return del_dirfd;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void del_chmod(const char *fbuf, mode_t mode)
|
||||
{
|
||||
const char *leaf;
|
||||
int dfd = del_held_dfd(fbuf, &leaf);
|
||||
if (dfd >= 0)
|
||||
do_chmod_atfd(dfd, leaf, mode);
|
||||
else
|
||||
do_chmod_at(fbuf, mode);
|
||||
}
|
||||
|
||||
static int del_unlink(const char *fbuf)
|
||||
{
|
||||
const char *leaf;
|
||||
int dfd = del_held_dfd(fbuf, &leaf);
|
||||
if (dfd >= 0 && do_unlink_atfd(dfd, leaf, 0) == 0)
|
||||
return 0;
|
||||
return robust_unlink(fbuf); /* fall back (ETXTBSY retry, or not held) */
|
||||
}
|
||||
|
||||
static inline int is_backup_file(char *fn)
|
||||
{
|
||||
int k = strlen(fn) - backup_suffix_len;
|
||||
@@ -83,6 +126,18 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)
|
||||
flags = (flags & ~(DEL_RECURSE|DEL_MAKE_ROOM|DEL_NO_UID_WRITE))
|
||||
| DEL_DIR_IS_EMPTY;
|
||||
|
||||
/* Hold this dir open so the per-entry chmod/rmdir/unlink below (and in
|
||||
* delete_item) become *at() calls against it rather than re-resolving the
|
||||
* full path for every entry. Save/restore around the recursion. */
|
||||
int save_del_dirfd = del_dirfd;
|
||||
const char *save_del_prefix = del_dir_prefix;
|
||||
int save_del_prefix_len = del_dir_prefix_len;
|
||||
fname[dlen] = '\0';
|
||||
del_dirfd = open_dir_secure(fname);
|
||||
fname[dlen] = '/';
|
||||
del_dir_prefix = fname;
|
||||
del_dir_prefix_len = dlen;
|
||||
|
||||
for (j = dirlist->used; j--; ) {
|
||||
struct file_struct *fp = dirlist->files[j];
|
||||
|
||||
@@ -98,7 +153,7 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)
|
||||
|
||||
strlcpy(p, fp->basename, remainder);
|
||||
if (!(fp->mode & S_IWUSR) && !am_root && fp->flags & FLAG_OWNED_BY_US)
|
||||
do_chmod_at(fname, fp->mode | S_IWUSR);
|
||||
del_chmod(fname, fp->mode | S_IWUSR);
|
||||
/* Save stack by recursing to ourself directly. */
|
||||
if (S_ISDIR(fp->mode)) {
|
||||
if (delete_dir_contents(fname, flags | DEL_RECURSE) != DR_SUCCESS)
|
||||
@@ -108,6 +163,12 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)
|
||||
ret = DR_NOT_EMPTY;
|
||||
}
|
||||
|
||||
if (del_dirfd >= 0)
|
||||
close(del_dirfd);
|
||||
del_dirfd = save_del_dirfd;
|
||||
del_dir_prefix = save_del_prefix;
|
||||
del_dir_prefix_len = save_del_prefix_len;
|
||||
|
||||
fname[dlen] = '\0';
|
||||
|
||||
done:
|
||||
@@ -139,7 +200,7 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags)
|
||||
}
|
||||
|
||||
if (flags & DEL_NO_UID_WRITE)
|
||||
do_chmod_at(fbuf, mode | S_IWUSR);
|
||||
del_chmod(fbuf, mode | S_IWUSR);
|
||||
|
||||
if (S_ISDIR(mode) && !(flags & DEL_DIR_IS_EMPTY)) {
|
||||
/* This only happens on the first call to delete_item() since
|
||||
@@ -159,19 +220,21 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags)
|
||||
}
|
||||
|
||||
if (S_ISDIR(mode)) {
|
||||
const char *leaf;
|
||||
int dfd = del_held_dfd(fbuf, &leaf);
|
||||
what = "rmdir";
|
||||
ok = do_rmdir_at(fbuf) == 0;
|
||||
ok = (dfd >= 0 ? do_unlink_atfd(dfd, leaf, AT_REMOVEDIR) : do_rmdir_at(fbuf)) == 0;
|
||||
} else {
|
||||
if (make_backups > 0 && !(flags & DEL_FOR_BACKUP) && (backup_dir || !is_backup_file(fbuf))) {
|
||||
what = "make_backup";
|
||||
ok = make_backup(fbuf, True);
|
||||
if (ok == 2) {
|
||||
what = "unlink";
|
||||
ok = robust_unlink(fbuf) == 0;
|
||||
ok = del_unlink(fbuf) == 0;
|
||||
}
|
||||
} else {
|
||||
what = "unlink";
|
||||
ok = robust_unlink(fbuf) == 0;
|
||||
ok = del_unlink(fbuf) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
Handling the rsync SGML documentation
|
||||
|
||||
rsync documentation is now primarily in Docbook format. Docbook is an
|
||||
SGML/XML documentation format that is becoming standard on free
|
||||
operating systems. It's also used for Samba documentation.
|
||||
|
||||
The SGML files are source code that can be translated into various
|
||||
useful output formats, primarily PDF, HTML, Postscript and plain text.
|
||||
|
||||
To do this transformation on Debian, you should install the
|
||||
docbook-utils package. Having done that, you can say
|
||||
|
||||
docbook2pdf rsync.sgml
|
||||
|
||||
and so on.
|
||||
|
||||
On other systems you probably need James Clark's "sp" and "JadeTeX"
|
||||
packages. Work it out for yourself and send a note to the mailing
|
||||
list.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
Notes on rsync profiling
|
||||
|
||||
strlcpy is hot:
|
||||
|
||||
0.00 0.00 1/7735635 push_dir [68]
|
||||
0.00 0.00 1/7735635 pop_dir [71]
|
||||
0.00 0.00 1/7735635 send_file_list [15]
|
||||
0.01 0.00 18857/7735635 send_files [4]
|
||||
0.04 0.00 129260/7735635 send_file_entry [18]
|
||||
0.04 0.00 129260/7735635 make_file [20]
|
||||
0.04 0.00 141666/7735635 send_directory <cycle 1> [36]
|
||||
2.29 0.00 7316589/7735635 f_name [13]
|
||||
[14] 11.7 2.42 0.00 7735635 strlcpy [14]
|
||||
|
||||
|
||||
Here's the top few functions:
|
||||
|
||||
46.23 9.57 9.57 13160929 0.00 0.00 mdfour64
|
||||
14.78 12.63 3.06 13160929 0.00 0.00 copy64
|
||||
11.69 15.05 2.42 7735635 0.00 0.00 strlcpy
|
||||
10.05 17.13 2.08 41438 0.05 0.38 sum_update
|
||||
4.11 17.98 0.85 13159996 0.00 0.00 mdfour_update
|
||||
1.50 18.29 0.31 file_compare
|
||||
1.45 18.59 0.30 129261 0.00 0.01 send_file_entry
|
||||
1.23 18.84 0.26 2557585 0.00 0.00 f_name
|
||||
1.11 19.07 0.23 1483750 0.00 0.00 u_strcmp
|
||||
1.11 19.30 0.23 118129 0.00 0.00 writefd_unbuffered
|
||||
0.92 19.50 0.19 1085011 0.00 0.00 writefd
|
||||
0.43 19.59 0.09 156987 0.00 0.00 read_timeout
|
||||
0.43 19.68 0.09 129261 0.00 0.00 clean_fname
|
||||
0.39 19.75 0.08 32887 0.00 0.38 matched
|
||||
0.34 19.82 0.07 1 70.00 16293.92 send_files
|
||||
0.29 19.89 0.06 129260 0.00 0.00 make_file
|
||||
0.29 19.95 0.06 75430 0.00 0.00 read_unbuffered
|
||||
|
||||
|
||||
|
||||
mdfour could perhaps be made faster:
|
||||
|
||||
/* NOTE: This code makes no attempt to be fast! */
|
||||
|
||||
There might be an optimized version somewhere that we can borrow.
|
||||
-351
@@ -1,351 +0,0 @@
|
||||
<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook V4.1//EN">
|
||||
<book id="rsync">
|
||||
<bookinfo>
|
||||
<title>rsync</title>
|
||||
<copyright>
|
||||
<year>1996 -- 2002</year>
|
||||
<holder>Martin Pool</holder>
|
||||
<holder>Andrew Tridgell</holder>
|
||||
</copyright>
|
||||
<author>
|
||||
<firstname>Martin</firstname>
|
||||
<surname>Pool</surname>
|
||||
</author>
|
||||
</bookinfo>
|
||||
|
||||
<chapter>
|
||||
<title>Introduction</title>
|
||||
|
||||
<para>rsync is a flexible program for efficiently copying files or
|
||||
directory trees.
|
||||
|
||||
<para>rsync has many options to select which files will be copied
|
||||
and how they are to be transferred. It may be used as an
|
||||
alternative to ftp, http, scp or rcp.
|
||||
|
||||
<para>The rsync remote-update protocol allows rsync to transfer just
|
||||
the differences between two sets of files across the network link,
|
||||
using an efficient checksum-search algorithm described in the
|
||||
technical report that accompanies this package.</para>
|
||||
|
||||
<para>Some of the additional features of rsync are:</para>
|
||||
|
||||
<itemizedlist>
|
||||
|
||||
<listitem>
|
||||
<para>support for copying links, devices, owners, groups and
|
||||
permissions
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
exclude and exclude-from options similar to GNU tar
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
a CVS exclude mode for ignoring the same files that CVS would ignore
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
can use any transparent remote shell, including rsh or ssh
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
does not require root privileges
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
pipelining of file transfers to minimize latency costs
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
support for anonymous or authenticated rsync servers (ideal for
|
||||
mirroring)
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</chapter>
|
||||
|
||||
|
||||
|
||||
<chapter>
|
||||
<title>Using rsync</title>
|
||||
<section>
|
||||
<title>
|
||||
Introductory example
|
||||
</title>
|
||||
|
||||
<para>
|
||||
Probably the most common case of rsync usage is to copy files
|
||||
to or from a remote machine using
|
||||
<application>ssh</application> as a network transport. In
|
||||
this situation rsync is a good alternative to
|
||||
<application>scp</application>.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
The most commonly used arguments for rsync are
|
||||
</para>
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><option>-v</option></term>
|
||||
<listitem>
|
||||
<para>Be verbose. Primarily, display the name of each file as it is copied.</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
|
||||
<varlistentry>
|
||||
<term><option>-a</option></term>
|
||||
<listitem>
|
||||
<para>
|
||||
Reproduce the structure and attributes of the origin files as exactly
|
||||
as possible: this includes copying subdirectories, symlinks, special
|
||||
files, ownership and permissions. (@xref{Attributes to
|
||||
copy}.)
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
|
||||
|
||||
<para><option>-v </option>
|
||||
|
||||
<para><option>-z</option>
|
||||
Compress network traffic, using a modified version of the
|
||||
@command{zlib} library.</para>
|
||||
|
||||
<para><option>-P</option>
|
||||
Display a progress indicator while files are transferred. This should
|
||||
normally be omitted if rsync is not run on a terminal.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<section>
|
||||
<title>Local and remote</title>
|
||||
|
||||
<para>There are six different ways of using rsync. They
|
||||
are:</para>
|
||||
|
||||
|
||||
|
||||
<!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE MSGSET PROCEDURE SIDEBAR QANDASET ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS ABSTRACT AUTHORBLURB EPIGRAPH INDEXTERM REFENTRY SECTION) -->
|
||||
<orderedlist>
|
||||
<listitem>
|
||||
<para>
|
||||
for copying local files. This is invoked when neither
|
||||
source nor destination path contains a @code{:} separator
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
for copying from the local machine to a remote machine using
|
||||
a remote shell program as the transport (such as rsh or
|
||||
ssh). This is invoked when the destination path contains a
|
||||
single @code{:} separator.
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
for copying from a remote machine to the local machine
|
||||
using a remote shell program. This is invoked when the source
|
||||
contains a @code{:} separator.
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
for copying from a remote rsync server to the local
|
||||
machine. This is invoked when the source path contains a @code{::}
|
||||
separator or a @code{rsync://} URL.
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
for copying from the local machine to a remote rsync
|
||||
server. This is invoked when the destination path contains a @code{::}
|
||||
separator.
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
for listing files on a remote machine. This is done the
|
||||
same way as rsync transfers except that you leave off the
|
||||
local destination.
|
||||
|
||||
</listitem>
|
||||
</orderedlist>
|
||||
<para>
|
||||
Note that in all cases (other than listing) at least one of the source
|
||||
and destination paths must be local.
|
||||
|
||||
<para>
|
||||
Any one invocation of rsync makes a copy in a single direction. rsync
|
||||
currently has no equivalent of @command{ftp}'s interactive mode.
|
||||
|
||||
@cindex @sc{nfs}
|
||||
@cindex network filesystems
|
||||
@cindex remote filesystems
|
||||
|
||||
<para>
|
||||
rsync's network protocol is generally faster at copying files than
|
||||
network filesystems such as @sc{nfs} or @sc{cifs}. It is better to
|
||||
run rsync on the file server either as a daemon or over ssh than
|
||||
running rsync giving the network directory.
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
|
||||
|
||||
<chapter>
|
||||
<title>Frequently asked questions</title>
|
||||
|
||||
|
||||
<!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE MSGSET PROCEDURE SIDEBAR QANDASET ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS ABSTRACT AUTHORBLURB EPIGRAPH INDEXTERM SECTION SIMPLESECT REFENTRY SECT1) -->
|
||||
<qandaset>
|
||||
<!-- one of (QANDADIV QANDAENTRY) -->
|
||||
|
||||
<qandaentry>
|
||||
<question>
|
||||
<!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST
|
||||
SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE
|
||||
TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO
|
||||
SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS
|
||||
CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS
|
||||
DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA
|
||||
ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT
|
||||
MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE
|
||||
INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE
|
||||
PROCEDURE ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS INDEXTERM) -->
|
||||
<para>Are there mailing lists for rsync?
|
||||
</question>
|
||||
|
||||
<answer>
|
||||
<para>Yes, and you can subscribe and unsubscribe through a
|
||||
web interface at
|
||||
<ulink
|
||||
url="http://lists.samba.org/">http://lists.samba.org/</ulink>
|
||||
</para>
|
||||
|
||||
<para>
|
||||
If you are having trouble with the mailing list, please
|
||||
send mail to the administrator
|
||||
|
||||
<email>rsync-admin@lists.samba.org</email>
|
||||
|
||||
not to the list itself.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
The mailing list archives are searchable. Use
|
||||
<ulink url="http://google.com/">Google</ulink> and prepend
|
||||
the search with <userinput>site:lists.samba.org
|
||||
rsync</userinput>, plus relevant keywords.
|
||||
</para>
|
||||
</answer>
|
||||
</qandaentry>
|
||||
|
||||
|
||||
<qandaentry>
|
||||
<question>
|
||||
<para>
|
||||
Why is rsync so much bigger when I build it with
|
||||
<command>gcc</command>?
|
||||
</para>
|
||||
</question>
|
||||
<answer>
|
||||
<para>
|
||||
On gcc, rsync builds by default with debug symbols
|
||||
included. If you strip both executables, they should end
|
||||
up about the same size. (Use <command>make
|
||||
install-strip</command>.)
|
||||
</para>
|
||||
</answer>
|
||||
</qandaentry>
|
||||
|
||||
|
||||
<qandaentry>
|
||||
<question>
|
||||
<para>Is rsync useful for a single large file like an ISO image?</para>
|
||||
</question>
|
||||
<answer>
|
||||
<para>
|
||||
Yes, but note the following:
|
||||
|
||||
<para>
|
||||
Background: A common use of rsync is to update a file (or set of files) in one location from a more
|
||||
correct or up-to-date copy in another location, taking advantage of portions of the files that are
|
||||
identical to speed up the process. (Note that rsync will transfer a file in its entirety if no copy
|
||||
exists at the destination.)
|
||||
|
||||
<para>
|
||||
(This discussion is written in terms of updating a local copy of a file from a correct file in a
|
||||
remote location, although rsync can work in either direction.)
|
||||
|
||||
<para>
|
||||
The file to be updated (the local file) must be in a destination directory that has enough space for
|
||||
two copies of the file. (In addition, keep an extra copy of the file to be updated in a different
|
||||
location for safety -- see the discussion (below) about rsync's behavior when the rsync process is
|
||||
interrupted before completion.)
|
||||
|
||||
<para>
|
||||
The local file must have the same name as the remote file being sync'd to (I think?). If you are
|
||||
trying to upgrade an iso from, for example, beta1 to beta2, rename the local file to the same name
|
||||
as the beta2 file. *(This is a useful thing to do -- only the changed portions will be
|
||||
transmitted.)*
|
||||
|
||||
<para>
|
||||
The extra copy of the local file kept in a different location is because of rsync's behavior if
|
||||
interrupted before completion:
|
||||
|
||||
<para>
|
||||
* If you specify the --partial option and rsync is interrupted, rsync will save the partially
|
||||
rsync'd file and throw away the original local copy. (The partially rsync'd file is correct but
|
||||
truncated.) If rsync is restarted, it will not have a local copy of the file to check for duplicate
|
||||
blocks beyond the section of the file that has already been rsync'd, thus the remainder of the rsync
|
||||
process will be a "pure transfer" of the file rather than taking advantage of the rsync algorithm.
|
||||
|
||||
<para>
|
||||
* If you don't specify the --partial option and rsync is interrupted, rsync will throw away the
|
||||
partially rsync'd file, and, when rsync is restarted starts the rsync process over from the
|
||||
beginning.
|
||||
|
||||
<para>
|
||||
Which of these is most desirable depends on the degree of commonality between the local and remote
|
||||
copies of the file *and how much progress was made before the interruption*.
|
||||
|
||||
<para>
|
||||
The ideal approach after an interruption would be to create a new file by taking the original file
|
||||
and deleting a portion equal in size to the portion already rsync'd and then appending *the
|
||||
remaining* portion to the portion of the file that has already been rsync'd. (There has been some
|
||||
discussion about creating an option to do this automatically.)
|
||||
|
||||
The --compare-dest option is useful when transferring multiple files, but is of no benefit in
|
||||
transferring a single file. (AFAIK)
|
||||
|
||||
*Other potentially useful information can be found at:
|
||||
-[3]http://twiki.org/cgi-bin/view/Wikilearn/RsyncingALargeFile
|
||||
|
||||
This answer, formatted with "real" bullets, can be found at:
|
||||
-[4]http://twiki.org/cgi-bin/view/Wikilearn/RsyncingALargeFileFAQ*
|
||||
|
||||
</para>
|
||||
</answer>
|
||||
</qandaentry>
|
||||
</qandaset>
|
||||
</chapter>
|
||||
|
||||
|
||||
<appendix>
|
||||
<title>Other Resources</title>
|
||||
|
||||
<para><ulink url="http://www.ccp14.ac.uk/ccp14admin/rsync/"></ulink></para>
|
||||
</appendix>
|
||||
</book>
|
||||
@@ -41,6 +41,100 @@ extern int sanitize_paths;
|
||||
extern int protocol_version;
|
||||
extern int trust_sender_args;
|
||||
extern int module_id;
|
||||
extern int operator_path_resolve;
|
||||
|
||||
/* Set while the daemon loads its own filter parameters; see parse_filter_file(). */
|
||||
int daemon_config_filter_file = 0;
|
||||
|
||||
/* Where the rule text now being parsed came from, when that is a file's
|
||||
* CONTENTS rather than an argument. A rule that fails to parse used to be
|
||||
* echoed back verbatim, and the peer chooses which file gets merged (a
|
||||
* per-directory merge rule travels over the protocol, so no argument of ours
|
||||
* ever names it), which made the filter parser a read-any-line oracle: any
|
||||
* line that is not valid filter syntax came straight back in the error.
|
||||
* Report where the bad rule is, not what it says. */
|
||||
static int rule_src_in_file = 0; /* parsing a file's contents right now */
|
||||
static const char *rule_src_file = NULL; /* ...and its name is safe to show */
|
||||
static int rule_src_line = 0;
|
||||
/* Where a file whose own name we must NOT print was named, which is a location
|
||||
* we CAN print: it keeps the diagnostic useful without echoing the pathname a
|
||||
* merge rule supplied. */
|
||||
static const char *rule_src_named_at = NULL;
|
||||
|
||||
/* True while the text we are handling came out of a file's contents: either we
|
||||
* are parsing that file right now, or this is a deferred per-dir merge whose
|
||||
* NAME came from one and which carries the provenance on the rule. */
|
||||
#define TEXT_FROM_FILE(template) \
|
||||
(rule_src_in_file \
|
||||
|| ((template) && (template)->rflags & FILTRULE_FROM_FILE))
|
||||
|
||||
/* "FILE line N", or just "FILE" when the count is not a line count. */
|
||||
static const char *rule_src_where(void)
|
||||
{
|
||||
static char buf[MAXPATHLEN + 32];
|
||||
|
||||
if (!rule_src_file) {
|
||||
if (!rule_src_named_at)
|
||||
return "a file read earlier"; /* origin not retained */
|
||||
snprintf(buf, sizeof buf, "a file named at %s", rule_src_named_at);
|
||||
return buf;
|
||||
}
|
||||
if (rule_src_line < 0)
|
||||
return rule_src_file;
|
||||
snprintf(buf, sizeof buf, "%s line %d", rule_src_file, rule_src_line);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* THE chokepoint. Every diagnostic string that is, or is built from, a filter
|
||||
* rule's own text -- a pattern, a merge-file name, a path composed from one --
|
||||
* must be passed through rule_text() on its way to rprintf(). When the rule
|
||||
* came from an argument the text is returned unchanged, because it is the
|
||||
* user's own and hiding it only makes typos harder to fix. When it came from
|
||||
* a FILE's contents it is replaced by a description of where it came from,
|
||||
* because the peer chooses which file gets merged and any line of it that
|
||||
* reaches a message is a line the peer can read back.
|
||||
*
|
||||
* Doing it here rather than at each site is the point: a message added later
|
||||
* cannot reintroduce the leak by forgetting to check, and there is one place
|
||||
* to audit. `template' is the rule the text belongs to, or NULL when the only
|
||||
* thing that matters is whether we are parsing a file right now.
|
||||
*
|
||||
* The returned buffer is rotated, so two calls in one rprintf() are safe. */
|
||||
static const char *rule_text_len(const filter_rule *template,
|
||||
const char *text, int len)
|
||||
{
|
||||
static char buf[2][BIGPATHBUFLEN];
|
||||
static int which = 0;
|
||||
char *b = buf[which];
|
||||
|
||||
which ^= 1;
|
||||
if (!TEXT_FROM_FILE(template)) {
|
||||
if (len < 0)
|
||||
return text;
|
||||
snprintf(b, sizeof buf[0], "%.*s", len, text);
|
||||
return b;
|
||||
}
|
||||
snprintf(b, sizeof buf[0], "<rule from %s>", rule_src_where());
|
||||
return b;
|
||||
}
|
||||
|
||||
static const char *rule_text(const filter_rule *template, const char *text)
|
||||
{
|
||||
return rule_text_len(template, text, -1);
|
||||
}
|
||||
|
||||
/* For the extra detail some messages add ABOUT the text -- a character of it,
|
||||
* an offset into it. Dropped along with the text it describes. */
|
||||
static const char *rule_detail(const filter_rule *template, const char *detail)
|
||||
{
|
||||
return TEXT_FROM_FILE(template) ? "" : detail;
|
||||
}
|
||||
|
||||
static void filter_rule_err(const char *msg, const char *rulestr)
|
||||
{
|
||||
rprintf(FERROR, "%s: %s\n", msg, rule_text(NULL, rulestr));
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
}
|
||||
|
||||
extern char curr_dir[MAXPATHLEN];
|
||||
extern unsigned int curr_dir_len;
|
||||
@@ -71,6 +165,9 @@ static int dirbuf_depth;
|
||||
/* This is True when we're scanning parent dirs for per-dir merge-files. */
|
||||
static BOOL parent_dirscan = False;
|
||||
|
||||
#define MAX_MERGE_DEPTH 32
|
||||
static int merge_depth = 0;
|
||||
|
||||
/* This array contains a list of all the currently active per-dir merge
|
||||
* files. This makes it easier to save the appropriate values when we
|
||||
* "push" down into each subdirectory. */
|
||||
@@ -171,10 +268,10 @@ static void add_rule(filter_rule_list *listp, const char *pat, unsigned int pat_
|
||||
else
|
||||
mention_rule_suffix = DEBUG_GTE(FILTER, 2) ? "" : NULL;
|
||||
if (mention_rule_suffix) {
|
||||
rprintf(FINFO, "[%s] add_rule(%s%.*s%s)%s%s\n",
|
||||
who_am_i(), get_rule_prefix(rule, pat, 0, NULL),
|
||||
(int)pat_len, pat, (rule->rflags & FILTRULE_DIRECTORY) ? "/" : "",
|
||||
listp->debug_type, mention_rule_suffix);
|
||||
rprintf(FINFO, "[%s] add_rule(%s%s)%s%s\n",
|
||||
who_am_i(), rule_detail(rule, get_rule_prefix(rule, pat, 0, NULL)),
|
||||
rule_text_len(rule, pat, (int)pat_len),
|
||||
listp->debug_type, rule_detail(rule, mention_rule_suffix));
|
||||
}
|
||||
|
||||
/* These flags also indicate that we're reading a list that
|
||||
@@ -279,7 +376,7 @@ static void add_rule(filter_rule_list *listp, const char *pat, unsigned int pat_
|
||||
}
|
||||
|
||||
lp = new_array0(filter_rule_list, 1);
|
||||
if (asprintf(&lp->debug_type, " [per-dir %s]", cp) < 0)
|
||||
if (asprintf(&lp->debug_type, " [per-dir %s]", rule_text(rule, cp)) < 0)
|
||||
out_of_memory("add_rule");
|
||||
rule->u.mergelist = lp;
|
||||
|
||||
@@ -427,7 +524,7 @@ void add_implied_include(const char *arg, int skip_daemon_module)
|
||||
if (cp[1] == ']') {
|
||||
if (!saw_wild)
|
||||
cp++; /* A \] in a non-wild filter causes a problem, so drop the \ . */
|
||||
} else if (!strchr("*[?", cp[1])) {
|
||||
} else if (!cp[1] || !strchr("*[?", cp[1])) {
|
||||
backslash_cnt++;
|
||||
if (saw_wild)
|
||||
*p++ = '\\';
|
||||
@@ -596,7 +693,8 @@ static void pop_filter_list(filter_rule_list *listp)
|
||||
* value and will be updated with the length of the resulting name. We
|
||||
* always return a name that is null terminated, even if the merge_file
|
||||
* name was not. */
|
||||
static char *parse_merge_name(const char *merge_file, unsigned int *len_ptr,
|
||||
static char *parse_merge_name(const filter_rule *template,
|
||||
const char *merge_file, unsigned int *len_ptr,
|
||||
unsigned int prefix_skip)
|
||||
{
|
||||
static char buf[MAXPATHLEN];
|
||||
@@ -627,7 +725,7 @@ static char *parse_merge_name(const char *merge_file, unsigned int *len_ptr,
|
||||
}
|
||||
if (!sanitize_path(fn, merge_file, r, dirbuf_depth, SP_DEFAULT)) {
|
||||
rprintf(FERROR, "merge-file name overflows: %s\n",
|
||||
merge_file);
|
||||
rule_text(template, merge_file));
|
||||
return NULL;
|
||||
}
|
||||
fn_len = strlen(fn);
|
||||
@@ -640,7 +738,8 @@ static char *parse_merge_name(const char *merge_file, unsigned int *len_ptr,
|
||||
if (fn != buf) {
|
||||
int d_len = dirbuf_len - prefix_skip;
|
||||
if (d_len + fn_len >= MAXPATHLEN) {
|
||||
rprintf(FERROR, "merge-file name overflows: %s\n", fn);
|
||||
rprintf(FERROR, "merge-file name overflows: %s\n",
|
||||
rule_text(template, fn));
|
||||
return NULL;
|
||||
}
|
||||
memcpy(buf, dirbuf + prefix_skip, d_len);
|
||||
@@ -690,7 +789,7 @@ static BOOL setup_merge_file(int mergelist_num, filter_rule *ex,
|
||||
char *x, *y, *pat = ex->pattern;
|
||||
unsigned int len;
|
||||
|
||||
if (!(x = parse_merge_name(pat, NULL, 0)) || *x != '/')
|
||||
if (!(x = parse_merge_name(ex, pat, NULL, 0)) || *x != '/')
|
||||
return 0;
|
||||
|
||||
if (DEBUG_GTE(FILTER, 2)) {
|
||||
@@ -816,7 +915,7 @@ void *push_local_filters(const char *dir, unsigned int dirlen)
|
||||
io_error |= IOERR_GENERAL;
|
||||
rprintf(FERROR,
|
||||
"cannot add local filter rules in long-named directory: %s\n",
|
||||
full_fname(dirbuf));
|
||||
rule_text(ex, full_fname(dirbuf)));
|
||||
}
|
||||
dirbuf[dirbuf_len] = '\0';
|
||||
}
|
||||
@@ -999,8 +1098,8 @@ static void report_filter_result(enum logcode code, char const *name,
|
||||
: "file";
|
||||
rprintf(code, "[%s] %sing %s %s because of pattern %s%s%s\n",
|
||||
w, actions[*w=='g'][!(ent->rflags & FILTRULE_INCLUDE)],
|
||||
t, name, ent->pattern,
|
||||
ent->rflags & FILTRULE_DIRECTORY ? "/" : "", type);
|
||||
t, name, rule_text(ent, ent->pattern),
|
||||
rule_detail(ent, ent->rflags & FILTRULE_DIRECTORY ? "/" : ""), type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,6 +1132,56 @@ int check_server_filter(filter_rule_list *listp, enum logcode code, const char *
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Returns 1 if `name` matches an implied-parent rule (a directory component
|
||||
* seeded by add_implied_include() with FILTRULE_DIRECTORY) but not a leaf
|
||||
* rule -- i.e. the client asked for something under the dir, never the dir
|
||||
* itself as content.
|
||||
*
|
||||
* The receiver uses this to refuse a malicious sender that sets XMIT_TOP_DIR
|
||||
* without XMIT_NO_CONTENT_DIR on such a dir: the honest encoding is both flags
|
||||
* (flist.c send path), so otherwise the receiver would set FLAG_CONTENT_DIR
|
||||
* and delete_in_dir() could sweep pre-existing siblings under --delete. */
|
||||
int is_implied_parent_dir(const char *name)
|
||||
{
|
||||
filter_rule *ent;
|
||||
int parent_match = 0;
|
||||
|
||||
if (!implied_filter_list.head)
|
||||
return 0;
|
||||
|
||||
/* The receiver exempts its synthetic transfer-root entry from the
|
||||
* requested-name filter. Treat it as parent-only unless an empty/root
|
||||
* source argument added the root-content rule. */
|
||||
if ((name[0] == '.' && name[1] == '\0')
|
||||
|| (name[0] == '/' && name[1] == '.' && name[2] == '\0')) {
|
||||
for (ent = implied_filter_list.head; ent; ent = ent->next) {
|
||||
if (!(ent->rflags & FILTRULE_INCLUDE))
|
||||
continue;
|
||||
if (strcmp(ent->pattern, "/**") == 0
|
||||
|| strcmp(ent->pattern, "/*") == 0)
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (ent = implied_filter_list.head; ent; ent = ent->next) {
|
||||
if (ent->rflags & (FILTRULE_PERDIR_MERGE | FILTRULE_CVS_IGNORE))
|
||||
continue;
|
||||
if (!rule_matches(name, ent, NAME_IS_DIR))
|
||||
continue;
|
||||
if (!(ent->rflags & FILTRULE_INCLUDE))
|
||||
continue;
|
||||
if (ent->rflags & FILTRULE_DIRECTORY) {
|
||||
parent_match = 1;
|
||||
continue;
|
||||
}
|
||||
/* A non-DIRECTORY include rule = a leaf the client asked for, so
|
||||
* the dir is legitimately in the list, not parent-only. */
|
||||
return 0;
|
||||
}
|
||||
return parent_match;
|
||||
}
|
||||
|
||||
/* Return -1 if file "name" is defined to be excluded by the specified
|
||||
* exclude list, 1 if it is included, and 0 if it was not matched. */
|
||||
int check_filter(filter_rule_list *listp, enum logcode code,
|
||||
@@ -1112,6 +1261,8 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
|
||||
/* Inherit from the template. Don't inherit FILTRULES_SIDES; we check
|
||||
* that later. */
|
||||
rule->rflags = template->rflags & FILTRULES_FROM_CONTAINER;
|
||||
if (rule_src_in_file)
|
||||
rule->rflags |= FILTRULE_FROM_FILE; /* before parse_merge_name() */
|
||||
|
||||
/* Figure out what kind of a filter rule "s" is pointing at. Note
|
||||
* that if FILTRULE_NO_PREFIXES is set, the rule is either an include
|
||||
@@ -1209,8 +1360,7 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
|
||||
rule->rflags |= FILTRULE_CLEAR_LIST;
|
||||
break;
|
||||
default:
|
||||
rprintf(FERROR, "Unknown filter rule: `%s'\n", *rulestr_ptr);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
filter_rule_err("Unknown filter rule", *rulestr_ptr);
|
||||
}
|
||||
while (ch != '!' && *++s && *s != ' ' && *s != '_') {
|
||||
if (template->rflags & FILTRULE_WORD_SPLIT && isspace(*s)) {
|
||||
@@ -1219,11 +1369,15 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
|
||||
}
|
||||
switch (*s) {
|
||||
default:
|
||||
invalid:
|
||||
rprintf(FERROR,
|
||||
"invalid modifier '%c' at position %d in filter rule: %s\n",
|
||||
*s, (int)(s - (const uchar *)*rulestr_ptr), *rulestr_ptr);
|
||||
invalid: {
|
||||
char where[32];
|
||||
snprintf(where, sizeof where, " '%c' at position %d",
|
||||
*s, (int)(s - (const uchar *)*rulestr_ptr));
|
||||
rprintf(FERROR, "invalid modifier%s in filter rule: %s\n",
|
||||
rule_detail(NULL, where),
|
||||
rule_text(NULL, *rulestr_ptr));
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
}
|
||||
case '-':
|
||||
if (!BITS_SETnUNSET(rule->rflags, FILTRULE_MERGE_FILE, FILTRULE_NO_PREFIXES))
|
||||
goto invalid;
|
||||
@@ -1295,10 +1449,8 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
|
||||
/* The filter and template both specify side(s). This
|
||||
* is dodgy (and won't work correctly if the template is
|
||||
* a one-sided per-dir merge rule), so reject it. */
|
||||
rprintf(FERROR,
|
||||
"specified-side merge file contains specified-side filter: %s\n",
|
||||
*rulestr_ptr);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
filter_rule_err("specified-side merge file contains specified-side filter",
|
||||
*rulestr_ptr);
|
||||
}
|
||||
rule->rflags |= template->rflags & FILTRULES_SIDES;
|
||||
}
|
||||
@@ -1313,17 +1465,14 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
|
||||
len = strlen((char*)s);
|
||||
|
||||
if (rule->rflags & FILTRULE_CLEAR_LIST) {
|
||||
if (!(rule->rflags & FILTRULE_NO_PREFIXES)
|
||||
if (!(template->rflags & FILTRULE_NO_PREFIXES)
|
||||
&& !(xflags & XFLG_OLD_PREFIXES) && len) {
|
||||
rprintf(FERROR,
|
||||
"'!' rule has trailing characters: %s\n", *rulestr_ptr);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
filter_rule_err("'!' rule has trailing characters", *rulestr_ptr);
|
||||
}
|
||||
if (len > 1)
|
||||
rule->rflags &= ~FILTRULE_CLEAR_LIST;
|
||||
} else if (!len && !(rule->rflags & FILTRULE_CVS_IGNORE)) {
|
||||
rprintf(FERROR, "unexpected end of filter rule: %s\n", *rulestr_ptr);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
filter_rule_err("unexpected end of filter rule", *rulestr_ptr);
|
||||
}
|
||||
|
||||
/* --delete-excluded turns an un-modified include/exclude into a sender-side rule. */
|
||||
@@ -1382,8 +1531,8 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
|
||||
break;
|
||||
|
||||
if (pat_len >= MAXPATHLEN) {
|
||||
rprintf(FERROR, "discarding over-long filter: %.*s\n",
|
||||
(int)pat_len, pat);
|
||||
rprintf(FERROR, "discarding over-long filter: %s\n",
|
||||
rule_text_len(NULL, pat, (int)pat_len));
|
||||
free_continue:
|
||||
free_filter(rule);
|
||||
continue;
|
||||
@@ -1411,6 +1560,11 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
|
||||
filter_rule *excl_self;
|
||||
|
||||
excl_self = new0(filter_rule);
|
||||
/* The pattern below is the merge rule's own text, so it
|
||||
* inherits that rule's provenance. Built by hand, this
|
||||
* rule looked argument-origin once parsing finished and
|
||||
* the match trace echoed a merge file's contents at -vv. */
|
||||
excl_self->rflags = rule->rflags & FILTRULE_FROM_FILE;
|
||||
/* Find the beginning of the basename and add an exclude for it. */
|
||||
for (name = pat + pat_len; name > pat && name[-1] != '/'; name--) {}
|
||||
add_rule(listp, name, (pat + pat_len) - name, excl_self, 0);
|
||||
@@ -1420,7 +1574,7 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
|
||||
if (parent_dirscan) {
|
||||
const char *p;
|
||||
unsigned int len = pat_len;
|
||||
if ((p = parse_merge_name(pat, &len, module_dirlen)))
|
||||
if ((p = parse_merge_name(rule, pat, &len, module_dirlen)))
|
||||
add_rule(listp, p, len, rule, 0);
|
||||
else
|
||||
free_filter(rule);
|
||||
@@ -1429,7 +1583,7 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
|
||||
} else {
|
||||
const char *p;
|
||||
unsigned int len = pat_len;
|
||||
if ((p = parse_merge_name(pat, &len, 0)))
|
||||
if ((p = parse_merge_name(rule, pat, &len, 0)))
|
||||
parse_filter_file(listp, p, rule, XFLG_FATAL_ERRORS);
|
||||
free_filter(rule);
|
||||
continue;
|
||||
@@ -1450,46 +1604,165 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_
|
||||
char line[BIGPATHBUFLEN];
|
||||
char *eob = line + sizeof line - 1;
|
||||
BOOL word_split = (template->rflags & FILTRULE_WORD_SPLIT) != 0;
|
||||
const char *save_src_file, *save_src_named_at;
|
||||
int save_src_line, save_src_in_file;
|
||||
int named_by_file;
|
||||
int pending = EOF;
|
||||
char named_at[MAXPATHLEN + 32];
|
||||
/* Our own copy: fname may point into parse_merge_name()'s static buffer,
|
||||
* which a merge rule inside THIS file overwrites while we still need it. */
|
||||
char src_name[MAXPATHLEN];
|
||||
|
||||
if (!fname || !*fname)
|
||||
return;
|
||||
|
||||
if (merge_depth >= MAX_MERGE_DEPTH) {
|
||||
rprintf(FERROR,
|
||||
"[%s] merge-file include depth limit (%d) exceeded at %s\n",
|
||||
who_am_i(), MAX_MERGE_DEPTH, rule_text(template, fname));
|
||||
/* Match the failed-open path below: abort under a fatal
|
||||
* (operator-supplied) merge, otherwise drop the rule. */
|
||||
if (xflags & XFLG_FATAL_ERRORS)
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
return;
|
||||
}
|
||||
merge_depth++;
|
||||
|
||||
if (*fname != '-' || fname[1] || am_server) {
|
||||
/* This path is operator- and (via per-directory merge files like
|
||||
* .cvsignore) sender-controlled: a planted symlink could leak a
|
||||
* root-readable file through the filter parser, or redirect an
|
||||
* --exclude-from open via a planted parent. Refuse symlinks not
|
||||
* owned by uid 0 or our euid. */
|
||||
const char *open_path;
|
||||
int fd;
|
||||
if (daemon_filter_list.head) {
|
||||
char *dir;
|
||||
strlcpy(line, fname, sizeof line);
|
||||
clean_fname(line, CFN_COLLAPSE_DOT_DOT_DIRS);
|
||||
if (check_filter(&daemon_filter_list, FLOG, line, 0) < 0)
|
||||
fp = NULL;
|
||||
else
|
||||
fp = fopen(line, "rb");
|
||||
/* parse_merge_name() prepends module_dir for absolute paths,
|
||||
* so strip module_dirlen back off before the check or the
|
||||
* anchored module-relative daemon rule won't match (as
|
||||
* options.c does for --exclude-from/--include-from). The
|
||||
* original absolute path is still used for the open below. */
|
||||
dir = line + (*line == '/' ? module_dirlen : 0);
|
||||
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
|
||||
if (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0) {
|
||||
/* Hidden by the daemon filter: treat the merge file as
|
||||
* non-existent rather than tripping XFLG_FATAL_ERRORS
|
||||
* below, so it neither errors out nor leaks a
|
||||
* fatal-vs-silent oracle. */
|
||||
if (DEBUG_GTE(FILTER, 2)) {
|
||||
/* Same rule as everywhere else: the name is
|
||||
* file content when a rule we read named it,
|
||||
* and so is "the daemon filter hides it". */
|
||||
rprintf(FINFO, "[%s] parse_filter_file(%s)%s\n",
|
||||
who_am_i(), rule_text(template, fname),
|
||||
rule_detail(template, " hidden by daemon filter"));
|
||||
}
|
||||
merge_depth--;
|
||||
return;
|
||||
}
|
||||
open_path = line;
|
||||
} else
|
||||
fp = fopen(fname, "rb");
|
||||
open_path = fname;
|
||||
|
||||
/* Confine the open to the module root. The ownership walk on its own
|
||||
* is not enough for a peer-driven merge file: a non-chrooted daemon
|
||||
* writes --backup-dir entries as root, so a raced backup symlink is
|
||||
* ROOT-owned -- exactly what open_no_attacker_symlinks() treats as
|
||||
* trusted -- and naming it in a dir-merge rule would read an
|
||||
* out-of-module file in as filter rules (their text comes back to the
|
||||
* peer in "Unknown filter rule" errors).
|
||||
*
|
||||
* The daemon's own "filter"/"include from"/"exclude from" parameters
|
||||
* are exempt: those are operator-configured and legitimately live
|
||||
* outside the module (/etc/rsync/excludes and the like). */
|
||||
int save_opr = operator_path_resolve;
|
||||
if (!daemon_config_filter_file)
|
||||
operator_path_resolve = 1;
|
||||
fd = open_no_attacker_symlinks(open_path, O_RDONLY, 0);
|
||||
operator_path_resolve = save_opr;
|
||||
|
||||
if (fd < 0)
|
||||
fp = NULL;
|
||||
else if (!(fp = fdopen(fd, "rb")))
|
||||
close(fd);
|
||||
} else
|
||||
fp = stdin;
|
||||
|
||||
if (DEBUG_GTE(FILTER, 2)) {
|
||||
/* The name is file CONTENT when a rule we read named it, and a
|
||||
* word-split per-dir merge turns every word of a file into one
|
||||
* of these -- so the trace would echo what the syntax errors no
|
||||
* longer do. Say where it came from instead. */
|
||||
rprintf(FINFO, "[%s] parse_filter_file(%s,%x,%x)%s\n",
|
||||
who_am_i(), fname, template->rflags, xflags,
|
||||
fp ? "" : " [not found]");
|
||||
who_am_i(), rule_text(template, fname), template->rflags, xflags,
|
||||
rule_detail(template, fp ? "" : " [not found]"));
|
||||
}
|
||||
|
||||
if (!fp) {
|
||||
if (xflags & XFLG_FATAL_ERRORS) {
|
||||
rsyserr(FERROR, errno,
|
||||
"failed to open %sclude file %s",
|
||||
template->rflags & FILTRULE_INCLUDE ? "in" : "ex",
|
||||
fname);
|
||||
/* rule_src_file is still the PARENT's context here: when it
|
||||
* is set, this name came out of a file we read, so neither
|
||||
* the name nor errno (an existence oracle) may be shown. */
|
||||
if (TEXT_FROM_FILE(template)) {
|
||||
/* errno too: it answers "does this path exist". */
|
||||
rprintf(FERROR, "failed to open %sclude file %s\n",
|
||||
template->rflags & FILTRULE_INCLUDE ? "in" : "ex",
|
||||
rule_text(template, fname));
|
||||
} else {
|
||||
rsyserr(FERROR, errno,
|
||||
"failed to open %sclude file %s",
|
||||
template->rflags & FILTRULE_INCLUDE ? "in" : "ex",
|
||||
fname);
|
||||
}
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
merge_depth--;
|
||||
return;
|
||||
}
|
||||
/* Before dirbuf is cut back: a per-directory fname points INTO dirbuf,
|
||||
* so truncating first leaves only the directory and the location we
|
||||
* report loses the filename. */
|
||||
strlcpy(src_name, fname, sizeof src_name);
|
||||
dirbuf[dirbuf_len] = '\0';
|
||||
|
||||
/* Rule text from here on is this file's contents, not an argument, so
|
||||
* a syntax error must not echo it. Saved and restored because a merge
|
||||
* rule inside this file can bring us back in for another file. */
|
||||
save_src_in_file = rule_src_in_file;
|
||||
save_src_file = rule_src_file;
|
||||
save_src_line = rule_src_line;
|
||||
/* If a rule we read named THIS file, our own path is file content too:
|
||||
* track the location for provenance but do not put it in a message. */
|
||||
named_by_file = TEXT_FROM_FILE(template);
|
||||
save_src_named_at = rule_src_named_at;
|
||||
if (named_by_file) {
|
||||
/* Snapshot where we were told to merge this, before that state
|
||||
* is replaced below (rule_src_where returns a static buffer).
|
||||
* A DEFERRED merge has no live location to point at -- the file
|
||||
* that named it was read and finished long ago -- so leave the
|
||||
* generic description rather than nesting two vague ones. */
|
||||
if (rule_src_in_file) {
|
||||
strlcpy(named_at, rule_src_where(), sizeof named_at);
|
||||
rule_src_named_at = named_at;
|
||||
} else
|
||||
rule_src_named_at = NULL;
|
||||
}
|
||||
rule_src_in_file = 1;
|
||||
rule_src_file = named_by_file ? NULL : src_name;
|
||||
rule_src_line = word_split ? -1 : 0; /* -1: tokens, not lines */
|
||||
|
||||
while (1) {
|
||||
char *s = line;
|
||||
int ch, overflow = 0;
|
||||
if (rule_src_line >= 0)
|
||||
rule_src_line++;
|
||||
while (1) {
|
||||
if ((ch = getc(fp)) == EOF) {
|
||||
if (pending != EOF) { /* a CR lookahead we could not push back */
|
||||
ch = pending;
|
||||
pending = EOF;
|
||||
} else if ((ch = getc(fp)) == EOF) {
|
||||
if (ferror(fp) && errno == EINTR) {
|
||||
clearerr(fp);
|
||||
continue;
|
||||
@@ -1498,25 +1771,51 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_
|
||||
}
|
||||
if (word_split && isspace(ch))
|
||||
break;
|
||||
if (eol_nulls? !ch : (ch == '\n' || ch == '\r'))
|
||||
if (eol_nulls? !ch : (ch == '\n' || ch == '\r')) {
|
||||
if (ch == '\r') { /* CRLF is one line, not two */
|
||||
int nxt;
|
||||
while ((nxt = getc(fp)) == EOF
|
||||
&& ferror(fp) && errno == EINTR)
|
||||
clearerr(fp);
|
||||
if (nxt == EOF) {
|
||||
if (!ferror(fp))
|
||||
ch = EOF; /* real end of file */
|
||||
} else if (nxt != '\n' && ungetc(nxt, fp) == EOF) {
|
||||
/* Pushback failed: hand it to the
|
||||
* NEXT rule, where it belongs --
|
||||
* appending it here would both
|
||||
* corrupt this rule and skip the
|
||||
* s < eob bound below. */
|
||||
pending = nxt;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (s < eob)
|
||||
*s++ = ch;
|
||||
else
|
||||
overflow = 1;
|
||||
}
|
||||
if (overflow) {
|
||||
rprintf(FERROR, "discarding over-long filter: %s...\n", line);
|
||||
rprintf(FERROR, "discarding over-long filter: %s\n",
|
||||
rule_text_len(NULL, line, 0));
|
||||
s = line;
|
||||
}
|
||||
*s = '\0';
|
||||
/* Skip an empty token and (when line parsing) comments. */
|
||||
if (*line && (word_split || (*line != ';' && *line != '#')))
|
||||
if (*line && (word_split || (*line != ';' && *line != '#'))) {
|
||||
rule_src_file = named_by_file ? NULL : src_name;
|
||||
parse_filter_str(listp, line, template, xflags);
|
||||
}
|
||||
if (ch == EOF)
|
||||
break;
|
||||
}
|
||||
rule_src_in_file = save_src_in_file;
|
||||
rule_src_file = save_src_file;
|
||||
rule_src_line = save_src_line;
|
||||
rule_src_named_at = save_src_named_at;
|
||||
fclose(fp);
|
||||
merge_depth--;
|
||||
}
|
||||
|
||||
/* If the "for_xfer" flag is set, the prefix is made compatible with the
|
||||
|
||||
@@ -75,11 +75,60 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)
|
||||
/* Note that the offset is just the caller letting us know where
|
||||
* the current file position is in the file. The use_seek arg tells
|
||||
* us that we should seek over matching data instead of writing it. */
|
||||
/* Flush any deferred run of zero bytes as a hole, advancing the file
|
||||
* position past it (both do_lseek() and do_punch_hole() move the offset). */
|
||||
static int flush_sparse_hole(int f)
|
||||
{
|
||||
if (!sparse_seek)
|
||||
return 0;
|
||||
if (sparse_past_write >= preallocated_len) {
|
||||
if (do_lseek(f, sparse_seek, SEEK_CUR) < 0) {
|
||||
sparse_seek = 0;
|
||||
return -1;
|
||||
}
|
||||
} else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) {
|
||||
sparse_seek = 0;
|
||||
return -1;
|
||||
}
|
||||
sparse_seek = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int full_sparse_write(int f, const char *buf, int len)
|
||||
{
|
||||
while (len > 0) {
|
||||
int ret = write(f, buf, len);
|
||||
if (ret <= 0) {
|
||||
if (ret < 0 && errno == EINTR)
|
||||
continue;
|
||||
sparse_seek = 0;
|
||||
return -1;
|
||||
}
|
||||
buf += ret;
|
||||
len -= ret;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Emit one span of data that is not being turned into a hole. For an in-place
|
||||
* update (use_seek) the bytes on disk already match, so we only need to move
|
||||
* past them; otherwise we write them out. Either way a deferred hole is
|
||||
* flushed first so that the span lands at the right offset. */
|
||||
static int emit_sparse_span(int f, int use_seek, const char *buf, int len)
|
||||
{
|
||||
if (flush_sparse_hole(f) < 0)
|
||||
return -1;
|
||||
if (use_seek)
|
||||
return do_lseek(f, len, SEEK_CUR) < 0 ? -1 : 0;
|
||||
return full_sparse_write(f, buf, len);
|
||||
}
|
||||
|
||||
static int write_sparse(int f, int use_seek, OFF_T offset, const char *buf, int len)
|
||||
{
|
||||
int l1 = 0, l2 = 0;
|
||||
int ret;
|
||||
int l1, l2, i, start, end;
|
||||
|
||||
/* Always treat a leading and trailing run of zeros as a (deferred)
|
||||
* hole, since they may merge with holes in the adjacent write calls. */
|
||||
for (l1 = 0; l1 < len && buf[l1] == 0; l1++) {}
|
||||
for (l2 = 0; l2 < len-l1 && buf[len-(l2+1)] == 0; l2++) {}
|
||||
|
||||
@@ -88,37 +137,46 @@ static int write_sparse(int f, int use_seek, OFF_T offset, const char *buf, int
|
||||
if (l1 == len)
|
||||
return len;
|
||||
|
||||
if (sparse_seek) {
|
||||
if (sparse_past_write >= preallocated_len) {
|
||||
if (do_lseek(f, sparse_seek, SEEK_CUR) < 0)
|
||||
return -1;
|
||||
} else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) {
|
||||
sparse_seek = 0;
|
||||
return -1;
|
||||
/* Scan the middle [l1, len-l2) for interior runs of zeros that are at
|
||||
* least SPARSE_WRITE_SIZE long (the hole granularity rsync has always
|
||||
* used) and defer those as holes. Everything in between -- which may
|
||||
* include shorter zero runs not worth a hole -- is emitted in one go,
|
||||
* rather than being chopped into SPARSE_WRITE_SIZE-byte pieces, which
|
||||
* made copying a large non-sparse file cost ~one write() per KiB.
|
||||
*
|
||||
* The matched (use_seek) case runs through the same scan: its interior
|
||||
* zero runs still have to be punched out, which is what --inplace
|
||||
* --sparse relies on to keep a hole-y basis file sparse. */
|
||||
start = l1;
|
||||
end = len - l2;
|
||||
for (i = l1; i < end; ) {
|
||||
int z;
|
||||
if (buf[i] != 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
for (z = 1; i + z < end && buf[i+z] == 0; z++) {}
|
||||
if (z < SPARSE_WRITE_SIZE) {
|
||||
i += z;
|
||||
continue;
|
||||
}
|
||||
if (i > start) {
|
||||
if (emit_sparse_span(f, use_seek, buf + start, i - start) < 0)
|
||||
return -1;
|
||||
sparse_past_write = offset + i;
|
||||
}
|
||||
sparse_seek += z;
|
||||
i += z;
|
||||
start = i;
|
||||
}
|
||||
if (end > start) {
|
||||
if (emit_sparse_span(f, use_seek, buf + start, end - start) < 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
sparse_seek = l2;
|
||||
sparse_past_write = offset + len - l2;
|
||||
|
||||
if (use_seek) {
|
||||
/* The in-place data already matches. */
|
||||
if (do_lseek(f, len - (l1+l2), SEEK_CUR) < 0)
|
||||
return -1;
|
||||
return len;
|
||||
}
|
||||
|
||||
while ((ret = write(f, buf + l1, len - (l1+l2))) <= 0) {
|
||||
if (ret < 0 && errno == EINTR)
|
||||
continue;
|
||||
sparse_seek = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (ret != (int)(len - (l1+l2))) {
|
||||
sparse_seek = 0;
|
||||
return l1+ret;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
@@ -153,8 +211,10 @@ int write_file(int f, int use_seek, OFF_T offset, const char *buf, int len)
|
||||
while (len > 0) {
|
||||
int r1;
|
||||
if (sparse_files > 0) {
|
||||
int len1 = MIN(len, SPARSE_WRITE_SIZE);
|
||||
r1 = write_sparse(f, use_seek, offset, buf, len1);
|
||||
/* write_sparse() handles the whole span itself, scanning
|
||||
* for holes and coalescing the non-zero data into large
|
||||
* write()s instead of SPARSE_WRITE_SIZE-byte dribbles. */
|
||||
r1 = write_sparse(f, use_seek, offset, buf, len);
|
||||
offset += r1;
|
||||
} else {
|
||||
if (!wf_writeBuf) {
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
extern int am_root;
|
||||
extern int am_server;
|
||||
extern int am_daemon;
|
||||
extern int am_chrooted;
|
||||
extern char *module_dir;
|
||||
extern unsigned int module_dirlen;
|
||||
extern int module_dirfd;
|
||||
extern unsigned int curr_dir_len;
|
||||
extern int am_sender;
|
||||
extern int am_generator;
|
||||
extern int inc_recurse;
|
||||
@@ -64,6 +69,7 @@ extern int non_perishable_cnt;
|
||||
extern int prune_empty_dirs;
|
||||
extern int copy_links;
|
||||
extern int copy_unsafe_links;
|
||||
extern int insecure_links;
|
||||
extern int protocol_version;
|
||||
extern int sanitize_paths;
|
||||
extern int munge_symlinks;
|
||||
@@ -132,6 +138,18 @@ static int64 tmp_dev = -1, tmp_ino;
|
||||
#endif
|
||||
static char tmp_sum[MAX_DIGEST_LEN];
|
||||
|
||||
#ifdef ST_MTIME_NSEC
|
||||
/* Return st_mtim nsec if it is in the wire-valid range, else 0. */
|
||||
static inline uint32 wire_mtime_nsec_from_stat(const STRUCT_STAT *stp)
|
||||
{
|
||||
unsigned long nsec = (unsigned long)stp->ST_MTIME_NSEC;
|
||||
|
||||
if (nsec > MAX_WIRE_NSEC)
|
||||
return 0;
|
||||
return (uint32)nsec;
|
||||
}
|
||||
#endif
|
||||
|
||||
static char empty_sum[MAX_DIGEST_LEN];
|
||||
static int flist_count_offset; /* for --delete --progress */
|
||||
static int show_filelist_progress;
|
||||
@@ -202,13 +220,47 @@ void show_flist_stats(void)
|
||||
*
|
||||
* The stat structure pointed to by stp will contain information about the
|
||||
* link or the referent as appropriate, if they exist. */
|
||||
/* Set by send_directory() to the fd of the directory it is currently scanning
|
||||
* (and that dir's path prefix), so the per-entry stat can go through the
|
||||
* already-open dir fd instead of re-resolving the full path for every entry.
|
||||
* Pure performance and sender-side only -- the scanned dir is already open, so
|
||||
* fstatat(scan_dirfd, basename) is identical to lstat(scandir/basename); no
|
||||
* confinement is implied or needed. */
|
||||
static int scan_dirfd = -1;
|
||||
static const char *scan_dir_prefix;
|
||||
static int scan_dir_prefix_len;
|
||||
|
||||
static int scan_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks)
|
||||
{
|
||||
/* Use the held scan fd only for a single component directly inside the
|
||||
* scanned dir, and only when am_root >= 0 (link_stat_at folds in no
|
||||
* fake-super %stat xattr; link_stat does so via get_stat_xattr, a no-op
|
||||
* once am_root >= 0). */
|
||||
if (scan_dirfd >= 0 && am_root >= 0
|
||||
&& strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0
|
||||
&& path[scan_dir_prefix_len] == '/'
|
||||
&& strchr(path + scan_dir_prefix_len + 1, '/') == NULL)
|
||||
return link_stat_at(scan_dirfd, path + scan_dir_prefix_len + 1, stp, follow_dirlinks);
|
||||
return link_stat(path, stp, follow_dirlinks);
|
||||
}
|
||||
|
||||
static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz)
|
||||
{
|
||||
if (scan_dirfd >= 0 && am_root >= 0
|
||||
&& strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0
|
||||
&& path[scan_dir_prefix_len] == '/'
|
||||
&& strchr(path + scan_dir_prefix_len + 1, '/') == NULL)
|
||||
return do_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz);
|
||||
return do_readlink(path, linkbuf, bufsiz);
|
||||
}
|
||||
|
||||
static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)
|
||||
{
|
||||
#ifdef SUPPORT_LINKS
|
||||
if (link_stat(path, stp, copy_dirlinks) < 0)
|
||||
if (scan_link_stat(path, stp, copy_dirlinks) < 0)
|
||||
return -1;
|
||||
if (S_ISLNK(stp->st_mode)) {
|
||||
int llen = do_readlink(path, linkbuf, MAXPATHLEN - 1);
|
||||
int llen = scan_readlink(path, linkbuf, MAXPATHLEN - 1);
|
||||
if (llen < 0)
|
||||
return -1;
|
||||
linkbuf[llen] = '\0';
|
||||
@@ -249,6 +301,30 @@ int link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks)
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Held-dirfd variant of link_stat(): stat single-component `name` relative to
|
||||
* directory fd `dfd`, instead of re-resolving a full path. Equivalent to
|
||||
* link_stat() only when NOT in --fake-super mode -- x_stat/x_lstat fold the
|
||||
* fake-super %stat xattr into the result via get_stat_xattr(), which is a
|
||||
* path-based no-op once am_root >= 0. Callers therefore use this only when
|
||||
* am_root >= 0 (and a valid dfd), falling back to link_stat() otherwise. */
|
||||
int link_stat_at(int dfd, const char *name, STRUCT_STAT *stp, int follow_dirlinks)
|
||||
{
|
||||
#ifdef SUPPORT_LINKS
|
||||
if (copy_links)
|
||||
return do_stat_atfd(dfd, name, stp);
|
||||
if (do_lstat_atfd(dfd, name, stp) < 0)
|
||||
return -1;
|
||||
if (follow_dirlinks && S_ISLNK(stp->st_mode)) {
|
||||
STRUCT_STAT st;
|
||||
if (do_stat_atfd(dfd, name, &st) == 0 && S_ISDIR(st.st_mode))
|
||||
*stp = st;
|
||||
}
|
||||
return 0;
|
||||
#else
|
||||
return do_stat_atfd(dfd, name, stp);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline int path_is_daemon_excluded(char *path, int ignore_filename)
|
||||
{
|
||||
if (daemon_filter_list.head) {
|
||||
@@ -291,17 +367,31 @@ static void flist_expand(struct file_list *flist, int extra)
|
||||
{
|
||||
struct file_struct **new_ptr;
|
||||
|
||||
/* Refuse BEFORE any int arithmetic below can overflow: used+extra (computed
|
||||
* in the early-return and the cap below) and the malloced growth math. Only
|
||||
* reachable past INT_MAX entries (my_alloc's --max-alloc cap normally stops
|
||||
* the list growing anywhere near there). */
|
||||
if (extra < 0 || flist->used < 0 || flist->used > INT_MAX - extra)
|
||||
goto too_large;
|
||||
|
||||
if (flist->used + extra <= flist->malloced)
|
||||
return;
|
||||
|
||||
if (flist->malloced < FLIST_START)
|
||||
flist->malloced = FLIST_START;
|
||||
else if (flist->malloced >= FLIST_LINEAR)
|
||||
else if (flist->malloced >= FLIST_LINEAR) {
|
||||
if (flist->malloced > INT_MAX - FLIST_LINEAR)
|
||||
goto too_large;
|
||||
flist->malloced += FLIST_LINEAR;
|
||||
else if (flist->malloced < FLIST_START_LARGE/16)
|
||||
} else if (flist->malloced < FLIST_START_LARGE/16) {
|
||||
if (flist->malloced > INT_MAX/4)
|
||||
goto too_large;
|
||||
flist->malloced *= 4;
|
||||
else
|
||||
} else {
|
||||
if (flist->malloced > INT_MAX/2)
|
||||
goto too_large;
|
||||
flist->malloced *= 2;
|
||||
}
|
||||
|
||||
/* In case count jumped or we are starting the list
|
||||
* with a known size just set it. */
|
||||
@@ -318,6 +408,11 @@ static void flist_expand(struct file_list *flist, int extra)
|
||||
}
|
||||
|
||||
flist->files = new_ptr;
|
||||
return;
|
||||
|
||||
too_large:
|
||||
rprintf(FERROR, "[%s] file list has grown too large to expand\n", who_am_i());
|
||||
exit_cleanup(RERR_MALLOC);
|
||||
}
|
||||
|
||||
static void flist_done_allocating(struct file_list *flist)
|
||||
@@ -764,7 +859,7 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
|
||||
if ((basename = strrchr(thisname, '/')) != NULL) {
|
||||
int len = basename++ - thisname;
|
||||
if (len != lastdir_len || memcmp(thisname, lastdir, len) != 0) {
|
||||
if (len != lastdir_len || !lastdir || memcmp(thisname, lastdir, len) != 0) {
|
||||
lastdir = new_array(char, len + 1);
|
||||
memcpy(lastdir, thisname, len);
|
||||
lastdir[len] = '\0';
|
||||
@@ -813,9 +908,17 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
rdev_major = DEV_MAJOR(devp);
|
||||
rdev = MAKEDEV(rdev_major, DEV_MINOR(devp));
|
||||
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
|
||||
} else if (IS_DEVICE(mode)) {
|
||||
/* Abbrev-branch counterpart to the !preserve_devices
|
||||
* stub-alloc below: zeroed F_RDEV_P slots. */
|
||||
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
|
||||
}
|
||||
if (preserve_links && S_ISLNK(mode))
|
||||
linkname_len = strlen(F_SYMLINK(first)) + 1;
|
||||
else if (S_ISLNK(mode))
|
||||
/* Abbrev-branch counterpart to the !preserve_links
|
||||
* stub-alloc below: empty linkname. */
|
||||
linkname_len = 1;
|
||||
else
|
||||
linkname_len = 0;
|
||||
real_ISREG_entry = S_ISREG(mode) ? 1 : 0;
|
||||
@@ -865,13 +968,18 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
mode = from_wire_mode(read_int(f));
|
||||
/* Reject modes whose type bits are not one of the standard
|
||||
* file types; otherwise garbage mode values propagate through
|
||||
* the file-type checks below unpredictably. */
|
||||
if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode)
|
||||
&& !S_ISCHR(mode) && !S_ISBLK(mode)
|
||||
&& !S_ISFIFO(mode) && !S_ISSOCK(mode)) {
|
||||
* the file-type checks below unpredictably. mode 0 is the one
|
||||
* legitimate exception: --delete-missing-args (missing_args==2)
|
||||
* sends a missing arg as a mode-0 entry (IS_MISSING_FILE), the
|
||||
* generator's delete signal (#910). */
|
||||
if (mode != 0 || missing_args != 2) {
|
||||
if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode)
|
||||
&& !S_ISCHR(mode) && !S_ISBLK(mode)
|
||||
&& !S_ISFIFO(mode) && !S_ISSOCK(mode)) {
|
||||
rprintf(FERROR, "invalid file mode 0%o for %s [%s]\n",
|
||||
(unsigned)mode, lastname, who_am_i());
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (atimes_ndx && !S_ISDIR(mode) && !(xflags & XMIT_SAME_ATIME)) {
|
||||
@@ -932,6 +1040,15 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
if (IS_DEVICE(mode))
|
||||
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
|
||||
file_length = 0;
|
||||
} else if (IS_DEVICE(mode)) {
|
||||
/* Peer/batch sent an S_IFCHR/S_IFBLK entry but we are not
|
||||
* preserving devices. A cooperating sender wouldn't do this;
|
||||
* a crafted batch can. Allocate (and zero, via the memset
|
||||
* below) the DEV_EXTRA_CNT slots so F_RDEV_P() callers
|
||||
* (set_stat_xattr under --fake-super, generator IS_DEVICE
|
||||
* paths) read {0,0} instead of the previous pool slot. */
|
||||
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
|
||||
file_length = 0;
|
||||
} else if (protocol_version < 28)
|
||||
rdev = MAKEDEV(0, 0);
|
||||
|
||||
@@ -952,6 +1069,14 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
#endif
|
||||
if (munge_symlinks)
|
||||
linkname_len += SYMLINK_PREFIX_LEN;
|
||||
} else if (S_ISLNK(mode)) {
|
||||
/* Peer/batch sent an S_IFLNK entry but we are not preserving
|
||||
* links (no -l, and the batch stream-flags didn't set it). A
|
||||
* cooperating sender wouldn't do this; a crafted batch can.
|
||||
* Allocate one byte for an empty linkname so F_SYMLINK()
|
||||
* callers (log.c %L, generator.c) read a valid "" instead of
|
||||
* the next pool slot's redzone. */
|
||||
linkname_len = 1;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
@@ -999,6 +1124,15 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
exit_cleanup(RERR_UNSUPPORTED);
|
||||
}
|
||||
|
||||
/* "." is the synthetic transfer root. Reinterpreting it as a file lets
|
||||
* --force recursively remove the real destination directory before the
|
||||
* receiver creates that file. */
|
||||
if ((!strcmp(thisname, ".") || !strcmp(thisname, "/.")) && !S_ISDIR(mode)) {
|
||||
rprintf(FERROR, "ERROR: rejecting non-directory transfer-root entry: %s\n",
|
||||
thisname);
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
|
||||
if (*thisname == '/' ? thisname[1] != '.' || thisname[2] != '\0' : *thisname != '.' || thisname[1] != '\0') {
|
||||
int filt_flags = S_ISDIR(mode) ? NAME_IS_DIR : NAME_IS_FILE;
|
||||
if (!trust_sender_filter /* a per-dir filter rule means we must trust the sender's filtering */
|
||||
@@ -1038,7 +1172,8 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
memcpy(bp, basename, basename_len);
|
||||
|
||||
#ifdef SUPPORT_HARD_LINKS
|
||||
if (xflags & XMIT_HLINKED
|
||||
if (preserve_hard_links && xflags & XMIT_HLINKED
|
||||
&& !S_ISDIR(mode)
|
||||
#ifndef CAN_HARDLINK_SYMLINK
|
||||
&& !S_ISLNK(mode)
|
||||
#endif
|
||||
@@ -1093,6 +1228,26 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
if (basename_len == 1+1 && *basename == '.') /* +1 for '\0' */
|
||||
F_DEPTH(file)--;
|
||||
if (protocol_version >= 30) {
|
||||
/* Stop a malicious sender expanding --delete scope by flagging
|
||||
* an implied parent as a content dir: if we only allowed this
|
||||
* entry as a parent of the requested leaf, force the flags back
|
||||
* to the honest implied-parent encoding (XMIT_TOP_DIR |
|
||||
* XMIT_NO_CONTENT_DIR) so it lands in FLAG_IMPLIED_DIR, not
|
||||
* FLAG_CONTENT_DIR, and delete_in_dir() can't sweep siblings.
|
||||
* Not gated on trust_sender_filter: implied_filter_list is
|
||||
* receiver-owned state, so a per-dir filter must not be able to
|
||||
* downgrade this defense. */
|
||||
if (implied_filter_list.head
|
||||
&& is_implied_parent_dir(thisname)
|
||||
&& (!(xflags & XMIT_NO_CONTENT_DIR) || !(xflags & XMIT_TOP_DIR))) {
|
||||
if (DEBUG_GTE(FILTER, 1)) {
|
||||
rprintf(FINFO,
|
||||
"[%s] receiver downgraded implied-parent dir %s "
|
||||
"to non-content (sender xflags=0x%x)\n",
|
||||
who_am_i(), thisname, xflags);
|
||||
}
|
||||
xflags |= XMIT_NO_CONTENT_DIR | XMIT_TOP_DIR;
|
||||
}
|
||||
if (!(xflags & XMIT_NO_CONTENT_DIR)) {
|
||||
if (xflags & XMIT_TOP_DIR)
|
||||
file->flags |= FLAG_TOP_DIR;
|
||||
@@ -1100,13 +1255,17 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
} else if (xflags & XMIT_TOP_DIR)
|
||||
file->flags |= FLAG_IMPLIED_DIR;
|
||||
} else if (xflags & XMIT_TOP_DIR) {
|
||||
in_del_hier = recurse;
|
||||
del_hier_name_len = F_DEPTH(file) == 0 ? 0 : l1 + l2;
|
||||
if (relative_paths && del_hier_name_len > 2
|
||||
&& lastname[del_hier_name_len-1] == '.'
|
||||
&& lastname[del_hier_name_len-2] == '/')
|
||||
del_hier_name_len -= 2;
|
||||
file->flags |= FLAG_TOP_DIR | FLAG_CONTENT_DIR;
|
||||
if (implied_filter_list.head && is_implied_parent_dir(thisname))
|
||||
file->flags |= FLAG_IMPLIED_DIR;
|
||||
else {
|
||||
in_del_hier = recurse;
|
||||
del_hier_name_len = F_DEPTH(file) == 0 ? 0 : l1 + l2;
|
||||
if (relative_paths && del_hier_name_len > 2
|
||||
&& lastname[del_hier_name_len-1] == '.'
|
||||
&& lastname[del_hier_name_len-2] == '/')
|
||||
del_hier_name_len -= 2;
|
||||
file->flags |= FLAG_TOP_DIR | FLAG_CONTENT_DIR;
|
||||
}
|
||||
} else if (in_del_hier) {
|
||||
if (!relative_paths || !del_hier_name_len
|
||||
|| (l1 >= del_hier_name_len
|
||||
@@ -1126,7 +1285,11 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
|
||||
#ifdef SUPPORT_LINKS
|
||||
if (linkname_len) {
|
||||
bp += basename_len;
|
||||
if (first_hlink_ndx >= flist->ndx_start) {
|
||||
if (!preserve_links) {
|
||||
/* The empty-linkname case allocated above; nothing on
|
||||
* the wire to read. Just terminate it. */
|
||||
*bp = '\0';
|
||||
} else if (first_hlink_ndx >= flist->ndx_start) {
|
||||
struct file_struct *first = flist->files[first_hlink_ndx - flist->ndx_start];
|
||||
memcpy(bp, F_SYMLINK(first), linkname_len);
|
||||
} else {
|
||||
@@ -1250,7 +1413,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
|
||||
int extra_len = file_extra_cnt * EXTRA_LEN;
|
||||
const char *basename;
|
||||
alloc_pool_t *pool;
|
||||
STRUCT_STAT st;
|
||||
STRUCT_STAT st = {0};
|
||||
char *bp;
|
||||
|
||||
if (strlcpy(thisname, fname, sizeof thisname) >= sizeof thisname) {
|
||||
@@ -1383,7 +1546,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
|
||||
|
||||
if ((basename = strrchr(thisname, '/')) != NULL) {
|
||||
int len = basename++ - thisname;
|
||||
if (len != lastdir_len || memcmp(thisname, lastdir, len) != 0) {
|
||||
if (len != lastdir_len || !lastdir || memcmp(thisname, lastdir, len) != 0) {
|
||||
lastdir = new_array(char, len + 1);
|
||||
memcpy(lastdir, thisname, len);
|
||||
lastdir[len] = '\0';
|
||||
@@ -1412,8 +1575,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
|
||||
}
|
||||
|
||||
#ifdef ST_MTIME_NSEC
|
||||
if (st.ST_MTIME_NSEC && protocol_version >= 31)
|
||||
extra_len += EXTRA_LEN;
|
||||
{
|
||||
uint32 nsec = wire_mtime_nsec_from_stat(&st);
|
||||
|
||||
if (nsec && protocol_version >= 31)
|
||||
extra_len += EXTRA_LEN;
|
||||
}
|
||||
#endif
|
||||
#if SIZEOF_CAPITAL_OFF_T >= 8
|
||||
if (st.st_size > 0xFFFFFFFFu && S_ISREG(st.st_mode))
|
||||
@@ -1426,6 +1593,18 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
|
||||
extra_len += SUM_EXTRA_CNT * EXTRA_LEN;
|
||||
}
|
||||
|
||||
#ifdef HAVE_STRUCT_STAT_ST_RDEV
|
||||
/* The sender path historically passes rdev via the tmp_rdev static
|
||||
* (read by send_file_entry()), so make_file() never reserved
|
||||
* DEV_EXTRA_CNT in the file_struct itself. But receiver-side callers
|
||||
* (recv_generator's --inplace --backup back_file, backup.c make_backup)
|
||||
* hand this struct to set_file_attrs() -> set_stat_xattr(), which reads
|
||||
* F_RDEV_P(file) under --fake-super. Reserve and populate the slots
|
||||
* so the struct is self-contained, matching recv_file_entry(). */
|
||||
if (IS_DEVICE(st.st_mode))
|
||||
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
|
||||
#endif
|
||||
|
||||
#if EXTRA_ROUNDING > 0
|
||||
if (extra_len & (EXTRA_ROUNDING * EXTRA_LEN))
|
||||
extra_len = (extra_len | (EXTRA_ROUNDING * EXTRA_LEN)) + EXTRA_LEN;
|
||||
@@ -1459,7 +1638,10 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
|
||||
|
||||
#ifdef HAVE_STRUCT_STAT_ST_RDEV
|
||||
if (IS_DEVICE(st.st_mode)) {
|
||||
uint32 *devp = F_RDEV_P(file);
|
||||
tmp_rdev = st.st_rdev;
|
||||
DEV_MAJOR(devp) = major(st.st_rdev);
|
||||
DEV_MINOR(devp) = minor(st.st_rdev);
|
||||
st.st_size = 0;
|
||||
} else if (IS_SPECIAL(st.st_mode))
|
||||
st.st_size = 0;
|
||||
@@ -1468,9 +1650,13 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
|
||||
file->flags = flags;
|
||||
file->modtime = st.st_mtime;
|
||||
#ifdef ST_MTIME_NSEC
|
||||
if (st.ST_MTIME_NSEC && protocol_version >= 31) {
|
||||
file->flags |= FLAG_MOD_NSEC;
|
||||
F_MOD_NSEC(file) = st.ST_MTIME_NSEC;
|
||||
{
|
||||
uint32 nsec = wire_mtime_nsec_from_stat(&st);
|
||||
|
||||
if (nsec && protocol_version >= 31) {
|
||||
file->flags |= FLAG_MOD_NSEC;
|
||||
F_MOD_NSEC(file) = nsec;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
file->len32 = (uint32)st.st_size;
|
||||
@@ -1640,6 +1826,7 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
|
||||
sx.st.st_mode = file->mode;
|
||||
if (get_acl(fname, &sx) < 0) {
|
||||
io_error |= IOERR_GENERAL;
|
||||
free_acl(&sx);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -1647,8 +1834,11 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs) {
|
||||
sx.st.st_mode = file->mode;
|
||||
if (get_xattr(fname, &sx) < 0) {
|
||||
if (get_xattr(fname, -1, &sx) < 0) {
|
||||
io_error |= IOERR_GENERAL;
|
||||
#ifdef SUPPORT_ACLS
|
||||
free_acl(&sx); /* get_acl() above may have loaded one */
|
||||
#endif
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -1823,6 +2013,77 @@ static void interpret_stat_error(const char *fname, int is_dir)
|
||||
}
|
||||
}
|
||||
|
||||
#if defined HAVE_FDOPENDIR && defined HAVE_DIRFD
|
||||
/* Open a source directory for scanning confined beneath the transfer root.
|
||||
* secure_relative_open() does a per-component O_NOFOLLOW walk that refuses a
|
||||
* parent component raced into a symlink pointing out of the tree; fdopendir()
|
||||
* then turns the held fd into the DIR* the scan reads. This mirrors the
|
||||
* sender's confined content open (sender.c): the directory enumeration must be
|
||||
* confined the same way, or a parent-symlink race (or, for a daemon following
|
||||
* mode, an in-module symlink to outside) lets the scan enumerate an out-of-tree
|
||||
* directory and leak its names/metadata/symlink targets. O_DIRECTORY without
|
||||
* O_NOFOLLOW makes secure_relative_open() follow in-tree directory symlinks
|
||||
* beneath the anchor and refuse escapes, so this serves both the default
|
||||
* no-follow scan and a daemon's symlink-following scan (see the caller).
|
||||
* Returns NULL with errno set on failure, like opendir(). */
|
||||
static DIR *secure_opendir(const char *fbuf)
|
||||
{
|
||||
int dfd, fl;
|
||||
DIR *d;
|
||||
|
||||
if (am_daemon && (!am_chrooted || module_dirlen)
|
||||
&& module_dir && module_dir[0] == '/' && *fbuf != '/' && module_dirfd >= 0
|
||||
&& curr_dir_len >= module_dirlen
|
||||
&& strncmp(curr_dir, module_dir, module_dirlen) == 0
|
||||
&& (curr_dir[module_dirlen] == '\0' || curr_dir[module_dirlen] == '/')) {
|
||||
/* Daemon: anchor the confined scan at the module root pinned by identity
|
||||
* at module setup (module_dirfd, opened while the daemon was positioned
|
||||
* there and still privileged), and walk the module-relative path of the
|
||||
* scan target beneath it. This re-follows the same in-module path -- so a
|
||||
* legitimate in-module ".." climb (sub/climb -> ../sibling) or an in-module
|
||||
* directory symlink is followed, and an escape refused -- without
|
||||
* re-walking the absolute module path as the dropped uid (the privilege-
|
||||
* drop EACCES), and without assuming the lexical curr_dir depth matches the
|
||||
* real cwd (a followed in-module symlink can desync them; anchoring at the
|
||||
* pinned module root and walking down the logical path is correct either
|
||||
* way). */
|
||||
const char *p = curr_dir + module_dirlen;
|
||||
char modrel[MAXPATHLEN];
|
||||
while (*p == '/')
|
||||
p++;
|
||||
if ((size_t)snprintf(modrel, sizeof modrel, "%s%s%s",
|
||||
p, *p ? "/" : "", fbuf) >= sizeof modrel) {
|
||||
errno = ENAMETOOLONG;
|
||||
return NULL;
|
||||
}
|
||||
dfd = secure_relative_open_at(module_dirfd, *modrel ? modrel : ".",
|
||||
O_RDONLY | O_DIRECTORY, 0);
|
||||
} else if (*fbuf == '/') {
|
||||
/* An absolute scan path (an absolute --relative / --files-from name, or a
|
||||
* "/" transfer root): anchor at "/" -- operator-named, trusted. */
|
||||
const char *relp = fbuf;
|
||||
while (*relp == '/')
|
||||
relp++;
|
||||
dfd = secure_relative_open("/", relp, O_RDONLY | O_DIRECTORY, 0);
|
||||
} else {
|
||||
/* Non-daemon (or chrooted) sender: confine beneath the cwd the sender
|
||||
* chdir'd into (the transfer root). */
|
||||
dfd = secure_relative_open(NULL, fbuf, O_RDONLY | O_DIRECTORY, 0);
|
||||
}
|
||||
|
||||
if (dfd < 0)
|
||||
return NULL;
|
||||
if ((fl = fcntl(dfd, F_GETFD)) >= 0)
|
||||
fcntl(dfd, F_SETFD, fl | FD_CLOEXEC);
|
||||
if (!(d = fdopendir(dfd))) {
|
||||
int save = errno;
|
||||
close(dfd);
|
||||
errno = save;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* This function is normally called by the sender, but the receiving side also
|
||||
* calls it from get_dirlist() with f set to -1 so that we just construct the
|
||||
* file list in memory without sending it over the wire. Also, get_dirlist()
|
||||
@@ -1841,7 +2102,31 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
|
||||
|
||||
assert(flist != NULL);
|
||||
|
||||
if (!(d = opendir(fbuf))) {
|
||||
#if defined HAVE_FDOPENDIR && defined HAVE_DIRFD
|
||||
/* Confine the enumeration beneath the transfer root. secure_opendir()
|
||||
* follows in-tree directory symlinks (RESOLVE_BENEATH) and refuses one that
|
||||
* escapes, so it serves both modes:
|
||||
* - a daemon/hardened sender (secure_relpath_active()) is confined to the
|
||||
* module in EVERY mode -- including -L/--copy-dirlinks/--copy-unsafe-
|
||||
* links, matching the content open (sender_open_copylinks_confined) --
|
||||
* so a following mode cannot be lured to enumerate outside the module;
|
||||
* - a non-daemon sender is confined in the default no-follow mode; its
|
||||
* symlink-following modes intentionally dereference out of the
|
||||
* operator's own tree, so they keep the legacy opendir().
|
||||
* f >= 0 is the sender's outgoing scan; get_dirlist() passes f < 0 and keeps
|
||||
* the legacy opendir(). A module opted out of confinement ("insecure links =
|
||||
* yes", admin-only) -- or a non-daemon --insecure-links -- uses the legacy
|
||||
* opendir() too, restoring the pre-hardening enumeration (re-opening the
|
||||
* escape; documented). */
|
||||
if (f >= 0 && !symlink_optout_allowed() && (secure_relpath_active()
|
||||
|| !(copy_links || copy_unsafe_links || copy_dirlinks || insecure_links)))
|
||||
d = secure_opendir(fbuf);
|
||||
else
|
||||
d = opendir(fbuf);
|
||||
#else
|
||||
d = opendir(fbuf);
|
||||
#endif
|
||||
if (!d) {
|
||||
if (errno == ENOENT) {
|
||||
if (am_sender) /* Can abuse this for vanished error w/ENOENT: */
|
||||
interpret_stat_error(fbuf, True);
|
||||
@@ -1864,6 +2149,14 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
|
||||
} else
|
||||
remainder = 0;
|
||||
|
||||
#ifdef HAVE_DIRFD
|
||||
/* Let the per-entry stat (readlink_stat -> scan_link_stat) go through the
|
||||
* already-open directory fd instead of re-resolving fbuf for each name. */
|
||||
scan_dirfd = dirfd(d);
|
||||
scan_dir_prefix = fbuf;
|
||||
scan_dir_prefix_len = len;
|
||||
#endif
|
||||
|
||||
for (errno = 0, di = readdir(d); di; errno = 0, di = readdir(d)) {
|
||||
unsigned name_len;
|
||||
char *dname = d_name(di);
|
||||
@@ -1892,6 +2185,9 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
|
||||
send_file_name(f, flist, fbuf, NULL, flags, filter_level);
|
||||
}
|
||||
|
||||
scan_dirfd = -1; /* fbuf is about to be reused / d closed */
|
||||
scan_dir_prefix = NULL; /* and don't leave the global pointing into fbuf */
|
||||
scan_dir_prefix_len = 0;
|
||||
fbuf[len] = '\0';
|
||||
|
||||
if (errno) {
|
||||
@@ -2025,7 +2321,8 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist)
|
||||
int len, dlen, flags = FLAG_DIVERT_DIRS | FLAG_CONTENT_DIR;
|
||||
size_t j;
|
||||
|
||||
f_name(file, fbuf);
|
||||
if (!f_name(file, fbuf))
|
||||
return;
|
||||
dlen = strlen(fbuf);
|
||||
|
||||
if (!change_pathname(file, NULL, 0))
|
||||
@@ -2070,10 +2367,9 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist)
|
||||
}
|
||||
|
||||
if (name_type != NORMAL_NAME) {
|
||||
STRUCT_STAT st;
|
||||
if (name_type == MISSING_NAME)
|
||||
memset(&st, 0, sizeof st);
|
||||
else if (link_stat(fbuf, &st, 1) != 0) {
|
||||
STRUCT_STAT st = {0};
|
||||
|
||||
if (name_type != MISSING_NAME && link_stat(fbuf, &st, 1) != 0) {
|
||||
interpret_stat_error(fbuf, True);
|
||||
continue;
|
||||
}
|
||||
@@ -2205,7 +2501,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
|
||||
static const char *lastdir;
|
||||
static int lastdir_len = -1;
|
||||
int len, dirlen;
|
||||
STRUCT_STAT st;
|
||||
STRUCT_STAT st = {0};
|
||||
char *p, *dir;
|
||||
struct file_list *flist;
|
||||
struct timeval start_tv, end_tv;
|
||||
@@ -2596,11 +2892,33 @@ struct file_list *recv_file_list(int f, int dir_ndx)
|
||||
#endif
|
||||
|
||||
if (inc_recurse && dir_ndx >= 0) {
|
||||
if (!first_flist) {
|
||||
/* All flists have already been freed via the NDX_DONE
|
||||
* chain, so dir_flist is stale: its files[] entries
|
||||
* point into a destroyed pool. A sub-flist marker now
|
||||
* is a protocol violation (and would otherwise UAF the
|
||||
* stale dir entry below, then deref an uninitialised
|
||||
* slot in the freshly reset dir_flist further down). */
|
||||
rprintf(FERROR_XFER,
|
||||
"rsync: refusing sub-flist after final flist was freed\n");
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
if (dir_ndx >= dir_flist->used) {
|
||||
rprintf(FERROR_XFER, "rsync: refusing invalid dir_ndx %u >= %u\n", dir_ndx, dir_flist->used);
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
struct file_struct *file = dir_flist->files[dir_ndx];
|
||||
if (!F_IS_ACTIVE(file)) {
|
||||
/* flist_sort_and_clean() can clear_file() a directory
|
||||
* entry that was a duplicate or otherwise pruned, but
|
||||
* the cleared file_struct stays in dir_flist. A peer
|
||||
* that then sends a sub-flist for that slot would make
|
||||
* f_name() return NULL into the dirname strcmp() below. */
|
||||
rprintf(FERROR_XFER,
|
||||
"rsync: refusing flist for cleared dir_ndx %d\n",
|
||||
dir_ndx);
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
if (file->flags & FLAG_GOT_DIR_FLIST) {
|
||||
rprintf(FERROR_XFER, "rsync: refusing malicious duplicate flist for dir %d\n", dir_ndx);
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
@@ -2629,7 +2947,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)
|
||||
if ((flags = read_varint(f)) == 0) {
|
||||
int err = read_varint(f);
|
||||
if (!ignore_errors)
|
||||
io_error |= err;
|
||||
io_error |= err & IOERR_VALID_MASK;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -2647,7 +2965,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)
|
||||
}
|
||||
err = read_varint(f);
|
||||
if (!ignore_errors)
|
||||
io_error |= err;
|
||||
io_error |= err & IOERR_VALID_MASK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2662,7 +2980,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)
|
||||
cur_dir++;
|
||||
if (cur_dir != good_dirname) {
|
||||
const char *d = dir_ndx >= 0 ? f_name(dir_flist->files[dir_ndx], NULL) : empty_dir;
|
||||
if (strcmp(cur_dir, d) != 0) {
|
||||
if (!d || strcmp(cur_dir, d) != 0) {
|
||||
rprintf(FERROR,
|
||||
"ABORTING due to invalid path from sender: %s/%s\n",
|
||||
cur_dir, file->basename);
|
||||
@@ -2750,9 +3068,17 @@ struct file_list *recv_file_list(int f, int dir_ndx)
|
||||
/* Recv the io_error flag */
|
||||
int err = read_int(f);
|
||||
if (!ignore_errors)
|
||||
io_error |= err;
|
||||
io_error |= err & IOERR_VALID_MASK;
|
||||
} else if (inc_recurse && flist->ndx_start == 1) {
|
||||
if (!file_total || strcmp(flist->sorted[flist->low]->basename, ".") != 0)
|
||||
/* The first inc_recurse flist has no parent in dir_flist; a
|
||||
* malicious peer can send a "." entry whose mode is not a
|
||||
* directory, so it never lands in dir_flist (used stays 0) yet
|
||||
* the basename test below still passes. That left parent_ndx at
|
||||
* its default 0 and the consumers dereferenced dir_flist->files[0]
|
||||
* = uninitialised heap. Require dir_flist to actually hold an
|
||||
* entry before trusting index 0. */
|
||||
if (!file_total || !dir_flist->used
|
||||
|| strcmp(flist->sorted[flist->low]->basename, ".") != 0)
|
||||
flist->parent_ndx = -1;
|
||||
}
|
||||
|
||||
|
||||
+535
-54
@@ -29,6 +29,7 @@ extern int do_xfers;
|
||||
extern int stdout_format_has_i;
|
||||
extern int logfile_format_has_i;
|
||||
extern int am_root;
|
||||
extern int operator_path_resolve;
|
||||
extern int am_server;
|
||||
extern int am_daemon;
|
||||
extern int inc_recurse;
|
||||
@@ -41,6 +42,7 @@ extern int preserve_xattrs;
|
||||
extern int preserve_links;
|
||||
extern int preserve_devices;
|
||||
extern int preserve_specials;
|
||||
extern int drop_devices;
|
||||
extern int preserve_hard_links;
|
||||
extern int preserve_executability;
|
||||
extern int preserve_perms;
|
||||
@@ -66,6 +68,7 @@ extern int inplace;
|
||||
extern int append_mode;
|
||||
extern int make_backups;
|
||||
extern int csum_length;
|
||||
extern int xfer_sum_len;
|
||||
extern int ignore_times;
|
||||
extern int size_only;
|
||||
extern OFF_T max_size;
|
||||
@@ -236,7 +239,11 @@ static int read_delay_line(char *buf, int *flags_p)
|
||||
goto invalid_data;
|
||||
}
|
||||
past_space++;
|
||||
len = j - read_pos - (past_space - bp) + 1; /* count the '\0' */
|
||||
/* Name length + NUL. Computed from past_space directly: the old
|
||||
* `j - read_pos - (past_space - bp)` form was off by +1 when a '!'
|
||||
* prefix had advanced bp past read_pos, over-reading deldelay_buf
|
||||
* by one byte on a buffer-filling final entry. */
|
||||
len = (deldelay_buf + j) - past_space + 1;
|
||||
read_pos = j + 1;
|
||||
|
||||
if (len > MAXPATHLEN) {
|
||||
@@ -455,7 +462,7 @@ static inline int xattrs_differ(const char *fname, struct file_struct *file, sta
|
||||
{
|
||||
if (preserve_xattrs) {
|
||||
if (!XATTR_READY(*sxp))
|
||||
get_xattr(fname, sxp);
|
||||
get_xattr(fname, -1, sxp);
|
||||
if (xattr_diff(file, sxp, 0))
|
||||
return 1;
|
||||
}
|
||||
@@ -564,7 +571,7 @@ void itemize(const char *fnamecmp, struct file_struct *file, int ndx, int statre
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs) {
|
||||
if (!XATTR_READY(*sxp))
|
||||
get_xattr(fnamecmp, sxp);
|
||||
get_xattr(fnamecmp, -1, sxp);
|
||||
if (xattr_diff(file, sxp, 1))
|
||||
iflags |= ITEM_REPORT_XATTR;
|
||||
}
|
||||
@@ -697,6 +704,11 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)
|
||||
{
|
||||
int32 blength;
|
||||
int s2length;
|
||||
/* The strong sum can be no longer than the negotiated checksum digest:
|
||||
* a short checksum (e.g. xxh64 = 8 bytes, when xxh128/xxh3 are absent)
|
||||
* makes xfer_sum_len < SUM_LENGTH, and the sender rejects an s2length
|
||||
* larger than xfer_sum_len (io.c). */
|
||||
int max_s2length = MIN(SUM_LENGTH, xfer_sum_len);
|
||||
int64 l;
|
||||
|
||||
if (len < 0) {
|
||||
@@ -712,8 +724,7 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)
|
||||
else {
|
||||
int32 max_blength = protocol_version < 30 ? OLD_MAX_BLOCK_SIZE : MAX_BLOCK_SIZE;
|
||||
int32 c;
|
||||
int cnt;
|
||||
for (c = 1, l = len, cnt = 0; l >>= 2; c <<= 1, cnt++) {}
|
||||
for (c = 1, l = len; l >>= 2; c <<= 1) {}
|
||||
if (c < 0 || c >= max_blength)
|
||||
blength = max_blength;
|
||||
else {
|
||||
@@ -731,7 +742,7 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)
|
||||
if (protocol_version < 27) {
|
||||
s2length = csum_length;
|
||||
} else if (csum_length == SUM_LENGTH) {
|
||||
s2length = SUM_LENGTH;
|
||||
s2length = max_s2length;
|
||||
} else {
|
||||
int32 c;
|
||||
int b = BLOCKSUM_BIAS;
|
||||
@@ -740,7 +751,7 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)
|
||||
/* add a bit, subtract rollsum, round up. */
|
||||
s2length = (b + 1 - 32 + 7) / 8; /* --optimize in compiler-- */
|
||||
s2length = MAX(s2length, csum_length);
|
||||
s2length = MIN(s2length, SUM_LENGTH);
|
||||
s2length = MIN(s2length, max_s2length);
|
||||
}
|
||||
|
||||
sum->flength = len;
|
||||
@@ -926,8 +937,10 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru
|
||||
rsyserr(FINFO, errno, "copy_file %s => %s",
|
||||
full_fname(src), copy_to);
|
||||
}
|
||||
/* Try to clean up. */
|
||||
unlink(copy_to);
|
||||
/* Try to clean up. copy_to's parent components are peer-named
|
||||
* and can be raced to a symlink, so resolve each with O_NOFOLLOW
|
||||
* via do_unlink_at() like the other generator-side unlinks. */
|
||||
do_unlink_at(copy_to);
|
||||
cleanup_disable();
|
||||
return -1;
|
||||
}
|
||||
@@ -939,6 +952,118 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Stat an alternate-basis candidate (basis_dir[j]/fname) for a daemon /./
|
||||
* inner-module chroot through the secure resolver, so a --compare/copy/link-dest
|
||||
* basis can't reach outside the inner module via a symlinked parent (the kernel
|
||||
* chroot confines only the outer path). secure_relative_open() refuses a parent
|
||||
* that escapes beneath the module root. Plain link_stat() everywhere else --
|
||||
* the non-chroot daemon sanitizes basis paths already, and a local receiver must
|
||||
* still follow an operator's --link-dest=../backup. */
|
||||
static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
{
|
||||
extern int am_chrooted;
|
||||
extern int operator_path_resolve;
|
||||
extern unsigned int module_dirlen;
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
/* The basis dir (--link-dest/--compare-dest/--copy-dest) is an operator-
|
||||
* supplied path. For a non-daemon receiver, resolve it with the ownership
|
||||
* walk: a symlink component owned by uid 0 or the euid (the operator's own
|
||||
* basis dir, e.g. --link-dest=/var/backups) is followed, a foreign-owned one
|
||||
* is refused -- absolute and relative alike. Refusing it makes the basis
|
||||
* look absent, so the file transfers normally instead of being read/linked/
|
||||
* skipped through an attacker's symlink. --insecure-links restores legacy
|
||||
* following; a daemon keeps its stronger confinement (chroot / secure
|
||||
* resolver) below. Only when am_root >= 0: link_stat_at() omits the
|
||||
* fake-super %stat xattr that link_stat() folds in, so --fake-super keeps
|
||||
* the plain path (a lower-severity, non-root basis lookup). */
|
||||
if (!am_daemon && am_root >= 0 && !symlink_optout_allowed()) {
|
||||
const char *leaf;
|
||||
int dfd = owner_walk_parent(path, &leaf);
|
||||
int r, e;
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
r = link_stat_at(dfd, leaf, stp, 0);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return r;
|
||||
}
|
||||
/* A non-chroot daemon serving an operator/peer alt-dest basis: resolve through
|
||||
* the ownership walk with module-ROOT confinement (operator_path_resolve) so an
|
||||
* in-module symlink whose target lands OUTSIDE the module is refused -- the
|
||||
* basis then looks absent and the file transfers normally instead of being
|
||||
* stat'd/read/linked through the link (closes the --compare-dest=/E read
|
||||
* oracle). "insecure links = yes" falls through to the legacy link_stat()
|
||||
* below, restoring 3.2.7 following. Only an ABSOLUTE basis (rooted under the
|
||||
* module by check_alt_basis_dirs, so it can reach an in-module symlink) is
|
||||
* confined here; a RELATIVE basis (--link-dest=../01) is a dest-relative
|
||||
* sibling whose "../" is already clamped to the module root by sanitize_path,
|
||||
* and must keep the plain link_stat below (#915/#930). The leaf is taken
|
||||
* under the confined parent with O_NOFOLLOW/AT_SYMLINK_NOFOLLOW, so
|
||||
* --copy-links can't follow a leaf symlink out of the module. */
|
||||
if (am_daemon && !am_chrooted && path[0] == '/' && !symlink_optout_allowed()) {
|
||||
const char *leaf;
|
||||
int dfd, e, save = operator_path_resolve;
|
||||
operator_path_resolve = 1;
|
||||
dfd = owner_walk_parent(path, &leaf);
|
||||
operator_path_resolve = save;
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
if (am_root >= 0) {
|
||||
int r = do_lstat_atfd(dfd, leaf, stp);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return r;
|
||||
}
|
||||
#ifdef SUPPORT_XATTRS
|
||||
{
|
||||
/* --fake-super: O_NOFOLLOW-open the held leaf (the daemon owns its
|
||||
* fake-super files) so the %stat xattr link_stat() would fold is
|
||||
* preserved while a leaf symlink is still refused. */
|
||||
int lfd = do_open_atfd(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0);
|
||||
STRUCT_STAT xst;
|
||||
e = errno;
|
||||
close(dfd);
|
||||
if (lfd < 0) { errno = e; return -1; }
|
||||
if (do_fstat(lfd, stp) < 0) { e = errno; close(lfd); errno = e; return -1; }
|
||||
if (get_stat_xattr(NULL, lfd, stp, &xst) == 0)
|
||||
*stp = xst;
|
||||
close(lfd);
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
{
|
||||
int r = do_lstat_atfd(dfd, leaf, stp);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
if (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !symlink_optout_allowed()) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
if (slash) {
|
||||
char dir[MAXPATHLEN];
|
||||
size_t dlen = (size_t)(slash - path);
|
||||
int dfd, r, e;
|
||||
if (dlen >= sizeof dir) { errno = ENAMETOOLONG; return -1; }
|
||||
memcpy(dir, path, dlen);
|
||||
dir[dlen] = '\0';
|
||||
if ((dfd = secure_relative_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0)) < 0)
|
||||
return -1;
|
||||
r = link_stat_at(dfd, slash + 1, stp, 0);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return link_stat(path, stp, 0);
|
||||
}
|
||||
|
||||
/* This is only called for regular files. We return -2 if we've finished
|
||||
* handling the file, -1 if no dest-linking occurred, or a non-negative
|
||||
* value if we found an alternate basis file. If we're called with the
|
||||
@@ -956,7 +1081,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
|
||||
|
||||
do {
|
||||
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
|
||||
if (link_stat(cmpbuf, &sxp->st, 0) < 0 || !S_ISREG(sxp->st.st_mode))
|
||||
if (basis_link_stat(cmpbuf, &sxp->st) < 0 || !S_ISREG(sxp->st.st_mode))
|
||||
continue;
|
||||
if (match_level == 0) {
|
||||
best_match = j;
|
||||
@@ -982,7 +1107,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
|
||||
if (j != best_match) {
|
||||
j = best_match;
|
||||
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
|
||||
if (link_stat(cmpbuf, &sxp->st, 0) < 0)
|
||||
if (basis_link_stat(cmpbuf, &sxp->st) < 0)
|
||||
goto got_nothing_for_ya;
|
||||
}
|
||||
|
||||
@@ -995,7 +1120,20 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
|
||||
}
|
||||
#ifdef SUPPORT_HARD_LINKS
|
||||
if (alt_dest_type == LINK_DEST) {
|
||||
if (!hard_link_one(file, fname, cmpbuf, 1))
|
||||
/* For a NON-daemon receiver the basis dir is an operator path:
|
||||
* resolve the link source via the ownership walk so a foreign-owned
|
||||
* symlink raced in after the basis_link_stat() check is still
|
||||
* refused (matching basis_link_stat's !am_daemon gate). A daemon
|
||||
* keeps its stronger module-anchored confinement (do_link_at's
|
||||
* secure_relpath_active path) -- the ownership walk would follow an
|
||||
* operator-owned symlink out of the module. */
|
||||
int hlok, op = !am_daemon;
|
||||
if (op)
|
||||
operator_path_resolve = 1;
|
||||
hlok = hard_link_one(file, fname, cmpbuf, 1);
|
||||
if (op)
|
||||
operator_path_resolve = 0;
|
||||
if (!hlok)
|
||||
goto try_a_copy;
|
||||
if (atimes_ndx)
|
||||
set_file_attrs(fname, file, sxp, NULL, 0);
|
||||
@@ -1024,6 +1162,13 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
|
||||
#ifdef SUPPORT_HARD_LINKS
|
||||
try_a_copy: /* Copy the file locally. */
|
||||
#endif
|
||||
/* NB: the copy-dest basis read is deliberately NOT routed through the
|
||||
* ownership walk: copy_altdest_file()->copy_file() also opens the dest
|
||||
* and copies xattrs through a held O_NOFOLLOW fd, and forcing
|
||||
* operator_path_resolve across that re-opens the copy_xattrs parent-
|
||||
* symlink race (copy-xattrs-symlink-race). basis_link_stat() already
|
||||
* refuses a foreign-owned basis symlink, closing the static escape; the
|
||||
* post-stat race on an absolute copy-dest basis is a documented residual. */
|
||||
if (!dry_run && copy_altdest_file(cmpbuf, fname, file) < 0) {
|
||||
if (find_exact_for_existing) /* Can get here via hard-link failure */
|
||||
goto got_nothing_for_ya;
|
||||
@@ -1053,8 +1198,10 @@ got_nothing_for_ya:
|
||||
}
|
||||
|
||||
/* This is only called for non-regular files. We return -2 if we've finished
|
||||
* handling the file, or -1 if no dest-linking occurred, or a non-negative
|
||||
* value if we found an alternate basis file. */
|
||||
* handling the file, -3 if we matched one but the destination refused to
|
||||
* hard-link it (the caller creates it instead, and must not report it again),
|
||||
* or -1 if no dest-linking occurred, or a non-negative value if we found an
|
||||
* alternate basis file. */
|
||||
static int try_dests_non(struct file_struct *file, char *fname, int ndx,
|
||||
char *cmpbuf, stat_x *sxp, int itemizing,
|
||||
enum logcode code)
|
||||
@@ -1077,7 +1224,7 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,
|
||||
|
||||
do {
|
||||
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
|
||||
if (link_stat(cmpbuf, &sxp->st, 0) < 0)
|
||||
if (basis_link_stat(cmpbuf, &sxp->st) < 0)
|
||||
continue;
|
||||
if (ftype != get_file_type(sxp->st.st_mode))
|
||||
continue;
|
||||
@@ -1104,11 +1251,12 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,
|
||||
if (j != best_match) {
|
||||
j = best_match;
|
||||
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
|
||||
if (link_stat(cmpbuf, &sxp->st, 0) < 0)
|
||||
if (basis_link_stat(cmpbuf, &sxp->st) < 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (match_level == 3) {
|
||||
int cannot_hardlink = 0;
|
||||
#ifdef SUPPORT_HARD_LINKS
|
||||
if (alt_dest_type == LINK_DEST
|
||||
#ifndef CAN_HARDLINK_SYMLINK
|
||||
@@ -1119,12 +1267,29 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,
|
||||
#endif
|
||||
&& !S_ISDIR(file->mode)) {
|
||||
if (do_link_at(cmpbuf, fname) < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"failed to hard-link %s with %s",
|
||||
cmpbuf, fname);
|
||||
return j;
|
||||
}
|
||||
if (preserve_hard_links && F_IS_HLINKED(file))
|
||||
/* CAN_HARDLINK_SYMLINK/_SPECIAL answer for whatever
|
||||
* filesystem the build tree sat on; the destination is
|
||||
* free to disagree, and one host can hold both (macOS
|
||||
* builds on APFS, backs up to HFS+). A refusal here is
|
||||
* that same answer arriving late, so fall back to a copy
|
||||
* as a build without the macro does -- the caller creates
|
||||
* the entry either way, so failing the transfer only cost
|
||||
* the exit status.
|
||||
*
|
||||
* Every errno, as the regular-file path next door already
|
||||
* does (try_dests_reg -> hard_link_one -> try_a_copy).
|
||||
* Picking out the "cannot" errnos is not possible anyway:
|
||||
* link(2) documents EPERM both for a filesystem with no
|
||||
* hard-link support and for an ordinary permission
|
||||
* refusal, and FUSE reports ENOSYS for the same thing.
|
||||
*
|
||||
* The rest report themselves: ENOSPC/EDQUOT/EROFS fail the
|
||||
* copy too, EMLINK and EXDEV mean it was never linkable.
|
||||
* EIO alone goes unremarked, deliberately -- a diagnostic
|
||||
* here lands in --link-dest's itemised output. */
|
||||
cannot_hardlink = 1;
|
||||
match_level = 2;
|
||||
} else if (preserve_hard_links && F_IS_HLINKED(file))
|
||||
finish_hard_link(file, fname, ndx, NULL, itemizing, code, -1);
|
||||
} else
|
||||
#endif
|
||||
@@ -1140,7 +1305,11 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,
|
||||
rprintf(FCLIENT, "%s%s is uptodate\n",
|
||||
fname, ftype == FT_DIR ? "/" : "");
|
||||
}
|
||||
return -2;
|
||||
/* -2 tells the caller the entry is already up to date, which for
|
||||
* --link-dest means "skip it". We could not link it, so say -3
|
||||
* instead: no caller claims that, and the fall-through creates the
|
||||
* entry -- the same place a build without the macro ends up. */
|
||||
return cannot_hardlink ? -3 : -2;
|
||||
}
|
||||
|
||||
return j;
|
||||
@@ -1210,6 +1379,232 @@ static BOOL is_below(struct file_struct *file, struct file_struct *subtree)
|
||||
*
|
||||
* Note that f_out is set to -1 when doing final directory-permission and
|
||||
* modification-time repair. */
|
||||
|
||||
/* Held-dirfd helpers for the per-entry ops below: when the secure resolver is
|
||||
* active they act on the entry's basename relative to its cached directory fd
|
||||
* (held_dfd_for, keyed on file->dirname), else fall back to the full-path
|
||||
* do_*_at wrappers (behaviour-identical). held_dfd_for() declines when fname
|
||||
* isn't in file->dirname (e.g. the single-file local_name dest), and the leaf
|
||||
* is derived from fname, not file->basename. */
|
||||
static int gen_entry_stat(const char *fname, struct file_struct *file,
|
||||
STRUCT_STAT *stp, int follow_dirlinks)
|
||||
{
|
||||
int dfd;
|
||||
/* link_stat_at folds in no fake-super xattr, so only use it when
|
||||
* am_root >= 0 (where link_stat's get_stat_xattr is a no-op anyway). */
|
||||
if (am_root >= 0 && (dfd = held_dfd_for(fname, file)) >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
return link_stat_at(dfd, slash ? slash + 1 : fname, stp, follow_dirlinks);
|
||||
}
|
||||
return link_stat(fname, stp, follow_dirlinks);
|
||||
}
|
||||
|
||||
static int gen_entry_mkdir(char *fname, struct file_struct *file, mode_t mode)
|
||||
{
|
||||
int dfd = held_dfd_for(fname, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
return do_mkdir_atfd(dfd, slash ? slash + 1 : fname, mode);
|
||||
}
|
||||
return do_mkdir_at(fname, mode);
|
||||
}
|
||||
|
||||
static int gen_entry_chmod(const char *fname, struct file_struct *file, mode_t mode)
|
||||
{
|
||||
int dfd = held_dfd_for(fname, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
return do_chmod_atfd(dfd, slash ? slash + 1 : fname, mode);
|
||||
}
|
||||
return do_chmod_at(fname, mode);
|
||||
}
|
||||
|
||||
static void gen_entry_set_times(const char *fname, struct file_struct *file, STRUCT_STAT *stp)
|
||||
{
|
||||
int dfd = held_dfd_for(fname, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
if (set_times_at(dfd, slash ? slash + 1 : fname, stp) != -2)
|
||||
return; /* handled (success or error) by the at-on-dfd tier */
|
||||
}
|
||||
set_times(fname, stp);
|
||||
}
|
||||
|
||||
static int gen_entry_symlink(const char *slnk, const char *path, struct file_struct *file)
|
||||
{
|
||||
int dfd = held_dfd_for(path, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
return do_symlink_atfd(slnk, dfd, slash ? slash + 1 : path);
|
||||
}
|
||||
return do_symlink_at(slnk, path);
|
||||
}
|
||||
|
||||
/* True when this build compiled no fd-relative primitive able to create this
|
||||
* kind of node. That is a property of the build, not of the call, so it is
|
||||
* decided here rather than inferred from an errno. */
|
||||
static int no_atfd_mknod_primitive(mode_t mode)
|
||||
{
|
||||
#ifndef AT_FDCWD
|
||||
(void)mode;
|
||||
return 1;
|
||||
#else
|
||||
/* --fake-super creates the placeholder with openat(), which exists
|
||||
* wherever AT_FDCWD does, so a failure there is a real failure and is
|
||||
* deliberately NOT retried unconfined -- even though a runtime denial
|
||||
* (a seccomp policy permitting open() but not openat()) would then fail
|
||||
* a create that the errno-based test used to let through. */
|
||||
if (am_root < 0)
|
||||
return 0;
|
||||
# ifdef HAVE_MKNODAT
|
||||
(void)mode;
|
||||
return 0;
|
||||
# elif defined(HAVE_MKFIFOAT)
|
||||
return !S_ISFIFO(mode); /* FIFOs are covered; device nodes are not */
|
||||
# else
|
||||
(void)mode;
|
||||
return 1;
|
||||
# endif
|
||||
#endif
|
||||
}
|
||||
|
||||
static int gen_entry_mknod(const char *path, struct file_struct *file, mode_t mode, dev_t rdev)
|
||||
{
|
||||
int dfd;
|
||||
/* do_mknod_atfd can't create a socket (no portable bindat); fall back. */
|
||||
if (!S_ISSOCK(mode) && (dfd = held_dfd_for(path, file)) >= 0) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
int ret = do_mknod_atfd(dfd, slash ? slash + 1 : path, mode, rdev);
|
||||
/* Fall through to the unconfined path-based create only where this
|
||||
* build compiled no fd-relative primitive for this kind of node --
|
||||
* SECURITY.md's rule for a platform that cannot be secure at all.
|
||||
* Testing errno == ENOSYS is not that test: a live mknodat() or
|
||||
* mkfifoat() can return ENOSYS too (an unimplemented FUSE mknod,
|
||||
* or seccomp), which would drop confinement on a platform that
|
||||
* does have the secure primitive. */
|
||||
if (ret == 0 || !no_atfd_mknod_primitive(mode))
|
||||
return ret;
|
||||
}
|
||||
return do_mknod_at(path, mode, rdev);
|
||||
}
|
||||
|
||||
static int gen_entry_unlink(const char *path, struct file_struct *file)
|
||||
{
|
||||
int dfd = held_dfd_for(path, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
return do_unlink_atfd(dfd, slash ? slash + 1 : path, 0);
|
||||
}
|
||||
return do_unlink_at(path);
|
||||
}
|
||||
|
||||
/* opath and npath are both expected to live in the entry's directory (the
|
||||
* tmp -> final rename); when both resolve to the held dir fd the rename is a
|
||||
* single renameat() within it, else fall back to the full-path wrapper. */
|
||||
static int gen_entry_rename(const char *opath, const char *npath, struct file_struct *file)
|
||||
{
|
||||
int odfd = held_dfd_for(opath, file);
|
||||
int ndfd = held_dfd_for(npath, file);
|
||||
if (odfd >= 0 && ndfd >= 0) {
|
||||
const char *os = strrchr(opath, '/');
|
||||
const char *ns = strrchr(npath, '/');
|
||||
return do_rename_atfd(odfd, os ? os + 1 : opath, ndfd, ns ? ns + 1 : npath);
|
||||
}
|
||||
return do_rename_at(opath, npath);
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_XATTRS
|
||||
/* Copy xattrs from src onto fname through a held, O_NOFOLLOW-opened fd so a
|
||||
* parent-symlink race can't redirect the setxattr. A hardened receiver always
|
||||
* uses a confined fd -- via the cached dir fd, or a secure re-pin when that
|
||||
* misses -- and refuses rather than path-write if it can't pin; only a
|
||||
* non-hardened receiver falls back to the path-based copy (matches
|
||||
* set_file_attrs' held-fd handling). */
|
||||
static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file_struct *file)
|
||||
{
|
||||
int dfd = held_dfd_for(fname, file);
|
||||
int xfd = -1, sfd = -1, ret;
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
xfd = openat(dfd, slash ? slash + 1 : fname,
|
||||
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
|
||||
if (xfd < 0) {
|
||||
/* We hold a confined parent dirfd but couldn't pin the
|
||||
* leaf (e.g. it was raced to a symlink) -- refuse rather
|
||||
* than fall back to a path-based set that would follow the
|
||||
* parent components. */
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"gen_entry_copy_xattrs: openat(%s) failed",
|
||||
full_fname(fname));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW
|
||||
else if (secure_relpath_active()) {
|
||||
/* No cached parent dirfd (a path deeper than the dirfd cache, or a raced
|
||||
* parent) but we must confine: re-pin the dest leaf through the secure
|
||||
* resolver so copy_xattrs uses fsetxattr, not a path-based lsetxattr a
|
||||
* flipped parent could redirect out of tree. A raced parent/leaf makes
|
||||
* this fail -> refuse rather than path-write. */
|
||||
int odir = 0;
|
||||
# ifdef O_DIRECTORY
|
||||
if (S_ISDIR(file->mode))
|
||||
odir = O_DIRECTORY;
|
||||
# endif
|
||||
xfd = secure_relative_open(NULL, fname,
|
||||
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0);
|
||||
if (xfd < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"gen_entry_copy_xattrs: secure open of %s failed",
|
||||
full_fname(fname));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
/* Pin the SOURCE (alt-dest basis) leaf too so the xattr READ can't be raced
|
||||
* out of tree (copy_file does the same for its content+xattr source). A
|
||||
* relative basis goes through the RESOLVE_BENEATH resolver; an absolute one
|
||||
* through the operator ownership walk. Refuse (don't path-read) when we are
|
||||
* meant to confine but can't pin; a non-hardened receiver path-reads (sfd<0). */
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW
|
||||
if (secure_relpath_active() && src && *src && !symlink_optout_allowed()) {
|
||||
int odir = 0;
|
||||
#ifdef O_DIRECTORY
|
||||
if (S_ISDIR(file->mode)) /* secure_relative_open rejects a dir leaf without this */
|
||||
odir = O_DIRECTORY;
|
||||
#endif
|
||||
if (src[0] != '/')
|
||||
sfd = secure_relative_open(NULL, src, O_RDONLY | O_NOFOLLOW | odir, 0);
|
||||
else {
|
||||
int save = operator_path_resolve, sdfd, e;
|
||||
const char *leaf;
|
||||
operator_path_resolve = 1;
|
||||
sdfd = owner_walk_parent(src, &leaf);
|
||||
operator_path_resolve = save;
|
||||
if (sdfd >= 0) {
|
||||
sfd = openat(sdfd, leaf, O_RDONLY | O_NOFOLLOW | odir | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
|
||||
e = errno; close(sdfd); errno = e;
|
||||
}
|
||||
}
|
||||
if (sfd < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"gen_entry_copy_xattrs: secure open of basis %s failed",
|
||||
full_fname(src));
|
||||
if (xfd >= 0)
|
||||
close(xfd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
ret = copy_xattrs(src, sfd, fname, xfd);
|
||||
if (sfd >= 0)
|
||||
close(sfd);
|
||||
if (xfd >= 0)
|
||||
close(xfd);
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
int itemizing, enum logcode code, int f_out)
|
||||
{
|
||||
@@ -1224,7 +1619,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
static int need_fuzzy_dirlist = 0;
|
||||
struct file_struct *fuzzy_file = NULL;
|
||||
int fd = -1, f_copy = -1;
|
||||
stat_x sx, real_sx;
|
||||
stat_x sx = {0}, real_sx = {0};
|
||||
STRUCT_STAT partial_st;
|
||||
struct file_struct *back_file = NULL;
|
||||
int statret, real_ret, stat_errno;
|
||||
@@ -1347,7 +1742,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
}
|
||||
parent_dirname = dn;
|
||||
|
||||
statret = link_stat(fname, &sx.st, keep_dirlinks && is_dir);
|
||||
statret = gen_entry_stat(fname, file, &sx.st, keep_dirlinks && is_dir);
|
||||
stat_errno = errno;
|
||||
}
|
||||
|
||||
@@ -1433,7 +1828,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
&& (stype == FT_DIR
|
||||
|| delete_item(fname, sx.st.st_mode, del_opts | DEL_FOR_DIR) != 0))
|
||||
goto cleanup; /* Any errors get reported later. */
|
||||
if (do_mkdir_at(fname, (file->mode|added_perms) & 0700) == 0)
|
||||
if (gen_entry_mkdir(fname, file, (file->mode|added_perms) & 0700) == 0)
|
||||
file->flags |= FLAG_DIR_CREATED;
|
||||
goto cleanup;
|
||||
}
|
||||
@@ -1475,10 +1870,13 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
itemize(fnamecmp, file, ndx, statret, &sx,
|
||||
statret ? ITEM_LOCAL_CHANGE : 0, 0, NULL);
|
||||
}
|
||||
if (real_ret != 0 && do_mkdir_at(fname,file->mode|added_perms) < 0 && errno != EEXIST) {
|
||||
if (real_ret != 0 && gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST) {
|
||||
/* The parent may have just been created by make_path(), so
|
||||
* drop any cached (failed) dir fd before the retry. */
|
||||
reset_dir_fd_cache();
|
||||
if (!relative_paths || errno != ENOENT
|
||||
|| make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0
|
||||
|| (do_mkdir_at(fname, file->mode|added_perms) < 0 && errno != EEXIST)) {
|
||||
|| (gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST)) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"recv_generator: mkdir %s failed",
|
||||
full_fname(fname));
|
||||
@@ -1492,7 +1890,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs && statret == 1)
|
||||
copy_xattrs(fnamecmpbuf, fname);
|
||||
gen_entry_copy_xattrs(fnamecmpbuf, fname, file);
|
||||
#endif
|
||||
if (set_file_attrs(fname, file, real_ret ? NULL : &real_sx, NULL, 0)
|
||||
&& INFO_GTE(NAME, 1) && code != FNONE && f_out != -1)
|
||||
@@ -1505,7 +1903,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
#ifdef HAVE_CHMOD
|
||||
if (!am_root && (file->mode & S_IRWXU) != S_IRWXU && dir_tweaking) {
|
||||
mode_t mode = file->mode | S_IRWXU;
|
||||
if (do_chmod_at(fname, mode) < 0) {
|
||||
if (gen_entry_chmod(fname, file, mode) < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"failed to modify permissions on %s",
|
||||
full_fname(fname));
|
||||
@@ -1579,7 +1977,14 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
}
|
||||
} else if (basis_dir[0] != NULL) {
|
||||
int j = try_dests_non(file, fname, ndx, fnamecmpbuf, &sx, itemizing, code);
|
||||
if (j == -2) {
|
||||
if (j == -3) {
|
||||
/* The destination cannot hard-link this type. Land exactly
|
||||
* where a build without CAN_HARDLINK_SYMLINK lands: create
|
||||
* the entry, but leave the reporting to the itemisation
|
||||
* try_dests_non() already emitted. */
|
||||
itemizing = 0;
|
||||
code = FNONE;
|
||||
} else if (j == -2) {
|
||||
#ifndef CAN_HARDLINK_SYMLINK
|
||||
if (alt_dest_type == LINK_DEST) {
|
||||
/* Resort to --copy-dest behavior. */
|
||||
@@ -1618,10 +2023,20 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if ((am_root && preserve_devices && ftype == FT_DEVICE)
|
||||
|| (preserve_specials && ftype == FT_SPECIAL)) {
|
||||
/* --drop-D refuses to CREATE devices/specials without touching
|
||||
* preserve_devices/preserve_specials, which also frame the file list's
|
||||
* rdev fields -- clearing those on one end of a connection alone
|
||||
* desynchronises it. Falls through to the "skipping non-regular file"
|
||||
* path below, exactly as --no-D reaches it. */
|
||||
if (!drop_devices
|
||||
&& ((am_root && preserve_devices && ftype == FT_DEVICE)
|
||||
|| (preserve_specials && ftype == FT_SPECIAL))) {
|
||||
dev_t rdev;
|
||||
int del_for_flag;
|
||||
/* Whether the dest existed, captured before the type-mismatch
|
||||
* flip below clears statret -- so atomic_create() gets a delete
|
||||
* flag (and reads sx.st) only when sx.st was actually stat'd. */
|
||||
int dest_existed = (statret == 0);
|
||||
if (ftype == FT_DEVICE) {
|
||||
uint32 *devp = F_RDEV_P(file);
|
||||
rdev = MAKEDEV(DEV_MAJOR(devp), DEV_MINOR(devp));
|
||||
@@ -1648,7 +2063,12 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
}
|
||||
} else if (basis_dir[0] != NULL) {
|
||||
int j = try_dests_non(file, fname, ndx, fnamecmpbuf, &sx, itemizing, code);
|
||||
if (j == -2) {
|
||||
if (j == -3) {
|
||||
/* As above: a destination that cannot hard-link this type
|
||||
* behaves like a build without CAN_HARDLINK_SPECIAL. */
|
||||
itemizing = 0;
|
||||
code = FNONE;
|
||||
} else if (j == -2) {
|
||||
#ifndef CAN_HARDLINK_SPECIAL
|
||||
if (alt_dest_type == LINK_DEST) {
|
||||
/* Resort to --copy-dest behavior. */
|
||||
@@ -1668,7 +2088,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
fname, (int)file->mode,
|
||||
(long)major(rdev), (long)minor(rdev));
|
||||
}
|
||||
if (atomic_create(file, fname, NULL, NULL, rdev, &sx, del_for_flag)) {
|
||||
if (atomic_create(file, fname, NULL, NULL, rdev, &sx, dest_existed ? del_for_flag : 0)) {
|
||||
set_file_attrs(fname, file, NULL, NULL, 0);
|
||||
if (itemizing) {
|
||||
itemize(fnamecmp, file, ndx, statret, &sx,
|
||||
@@ -1712,7 +2132,8 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (update_only > 0 && statret == 0 && file->modtime - sx.st.st_mtime < modify_window) {
|
||||
if (update_only > 0 && statret == 0 && stype == ftype
|
||||
&& file->modtime - sx.st.st_mtime < modify_window) {
|
||||
if (INFO_GTE(SKIP, 1))
|
||||
rprintf(FINFO, "%s is newer\n", fname);
|
||||
#ifdef SUPPORT_HARD_LINKS
|
||||
@@ -1814,7 +2235,12 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
;
|
||||
else if (quick_check_ok(FT_REG, fnamecmp, file, &sx.st)) {
|
||||
if (partialptr) {
|
||||
/* The --partial-dir basis is an operator/peer path: unlink it
|
||||
* through the exclude-aware ownership walk so a symlinked
|
||||
* partial-dir can't delete a file in an excluded subtree. */
|
||||
operator_path_resolve = 1;
|
||||
do_unlink_at(partialptr);
|
||||
operator_path_resolve = 0;
|
||||
handle_partial_dir(partialptr, PDIR_DELETE);
|
||||
}
|
||||
set_file_attrs(fname, file, &sx, NULL, maybe_ATTRS_REPORT | maybe_ATTRS_ACCURATE_TIME);
|
||||
@@ -1853,15 +2279,26 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
|
||||
if (read_batch || whole_file) {
|
||||
if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) {
|
||||
if (!(backupptr = get_backup_name(fname)))
|
||||
/* The --backup-dir (backupptr) is an operator path; this in-place
|
||||
* backup bypasses make_backup(), so set operator_path_resolve here
|
||||
* too -- get_backup_name() (make_path) and copy_file() then resolve
|
||||
* it with the ownership walk instead of following any symlink. */
|
||||
operator_path_resolve = 1;
|
||||
if (!(backupptr = get_backup_name(fname))) {
|
||||
operator_path_resolve = 0;
|
||||
goto cleanup;
|
||||
if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS)))
|
||||
}
|
||||
if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) {
|
||||
operator_path_resolve = 0;
|
||||
goto pretend_missing;
|
||||
}
|
||||
if (copy_file(fname, backupptr, -1, back_file->mode) < 0) {
|
||||
operator_path_resolve = 0;
|
||||
unmake_file(back_file);
|
||||
back_file = NULL;
|
||||
goto cleanup;
|
||||
}
|
||||
operator_path_resolve = 0;
|
||||
}
|
||||
goto notify_others;
|
||||
}
|
||||
@@ -1889,13 +2326,19 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
}
|
||||
|
||||
if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) {
|
||||
/* Operator --backup-dir, bypassing make_backup(): resolve get_backup_name()
|
||||
* (make_path), the unlink and the create with the ownership walk. */
|
||||
operator_path_resolve = 1;
|
||||
if (!(backupptr = get_backup_name(fname))) {
|
||||
operator_path_resolve = 0;
|
||||
goto cleanup;
|
||||
}
|
||||
if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) {
|
||||
operator_path_resolve = 0;
|
||||
goto pretend_missing;
|
||||
}
|
||||
if (robust_unlink(backupptr) && errno != ENOENT) {
|
||||
operator_path_resolve = 0;
|
||||
rsyserr(FERROR_XFER, errno, "unlink %s",
|
||||
full_fname(backupptr));
|
||||
unmake_file(back_file);
|
||||
@@ -1903,11 +2346,13 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
goto cleanup;
|
||||
}
|
||||
if ((f_copy = do_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) {
|
||||
operator_path_resolve = 0;
|
||||
rsyserr(FERROR_XFER, errno, "open %s", full_fname(backupptr));
|
||||
unmake_file(back_file);
|
||||
back_file = NULL;
|
||||
goto cleanup;
|
||||
}
|
||||
operator_path_resolve = 0;
|
||||
fnamecmp_type = FNAMECMP_BACKUP;
|
||||
}
|
||||
|
||||
@@ -1970,14 +2415,34 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
close(fd);
|
||||
if (back_file) {
|
||||
int save_preserve_xattrs = preserve_xattrs;
|
||||
if (f_copy >= 0)
|
||||
close(f_copy);
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs) {
|
||||
copy_xattrs(fname, backupptr);
|
||||
/* The delta-backup path wrote backupptr via the held f_copy, so
|
||||
* copy its xattrs through that fd here. The whole-file/inplace
|
||||
* path (f_copy < 0) backed it up via copy_file(), which already
|
||||
* copied the xattrs through its own held fd -- don't repeat it
|
||||
* with a path-based set a parent-symlink race could redirect. */
|
||||
if (preserve_xattrs && f_copy >= 0) {
|
||||
/* Read fname's xattrs through a confined fd so the copy onto the
|
||||
* held backup fd can't be fed an out-of-module source by a raced
|
||||
* parent symlink; a hardened race skips rather than path-reads. */
|
||||
int bfd = backup_source_fd(fname);
|
||||
if (!backup_metadata_hardened() || bfd >= 0)
|
||||
copy_xattrs(fname, bfd, backupptr, f_copy);
|
||||
if (bfd >= 0)
|
||||
close(bfd);
|
||||
preserve_xattrs = 0;
|
||||
}
|
||||
#endif
|
||||
if (f_copy >= 0)
|
||||
close(f_copy);
|
||||
/* backupptr's data/xattrs were written safely (confined create under
|
||||
* operator_path_resolve, held-fd xattr copy above). This metadata set
|
||||
* re-resolves backupptr by path and is NOT wrapped in operator mode:
|
||||
* set_file_attrs() also drives the path-based xattr set whose held-fd
|
||||
* race-fix operator mode would defeat (cf. the copy-dest note in
|
||||
* try_dests_reg). The static --backup-dir escape is already closed; a
|
||||
* parent-symlink flipped in after the confined create races only these
|
||||
* chmod/chown/times -- a documented residual. */
|
||||
set_file_attrs(backupptr, back_file, NULL, NULL, 0);
|
||||
preserve_xattrs = save_preserve_xattrs;
|
||||
if (INFO_GTE(BACKUP, 1)) {
|
||||
@@ -1988,6 +2453,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
}
|
||||
|
||||
free_stat_x(&sx);
|
||||
free_stat_x(&real_sx);
|
||||
}
|
||||
|
||||
/* If we are replacing an existing hard link, symlink, device, or special file,
|
||||
@@ -2022,7 +2488,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const
|
||||
|
||||
if (slnk) {
|
||||
#ifdef SUPPORT_LINKS
|
||||
if (do_symlink_at(slnk, create_name) < 0) {
|
||||
if (gen_entry_symlink(slnk, create_name, file) < 0) {
|
||||
rsyserr(FERROR_XFER, errno, "symlink %s -> \"%s\" failed",
|
||||
full_fname(create_name), slnk);
|
||||
return 0;
|
||||
@@ -2038,22 +2504,35 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const
|
||||
return 0;
|
||||
#endif
|
||||
} else {
|
||||
if (do_mknod_at(create_name, file->mode, rdev) < 0) {
|
||||
rsyserr(FERROR_XFER, errno, "mknod %s failed",
|
||||
if (gen_entry_mknod(create_name, file, file->mode, rdev) < 0) {
|
||||
int e = errno;
|
||||
/* A nested socket can't be created race-safely where there is no
|
||||
* bindat() (the BSDs, macOS, Solaris): syscall.c returns EOPNOTSUPP
|
||||
* rather than re-resolving an unsafe parent. A socket inode is only
|
||||
* a placeholder -- a live socket isn't usefully transferred -- so
|
||||
* skip it with a warning instead of failing the whole transfer
|
||||
* (got_xfer_error -> exit 23). Top-level sockets still create via
|
||||
* the path-based bind() fallback. */
|
||||
if (S_ISSOCK(file->mode) && (e == EOPNOTSUPP || e == ENOSYS)) {
|
||||
rprintf(FWARNING, "skipping socket (creation unsupported here): %s\n",
|
||||
full_fname(create_name));
|
||||
return 0;
|
||||
}
|
||||
rsyserr(FERROR_XFER, e, "mknod %s failed",
|
||||
full_fname(create_name));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip_atomic) {
|
||||
if (do_rename_at(tmpname, fname) < 0) {
|
||||
if (gen_entry_rename(tmpname, fname, file) < 0) {
|
||||
char *full_tmpname = strdup(full_fname(tmpname));
|
||||
if (full_tmpname == NULL)
|
||||
out_of_memory("atomic_create");
|
||||
rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\" failed",
|
||||
full_tmpname, full_fname(fname));
|
||||
free(full_tmpname);
|
||||
do_unlink_at(tmpname);
|
||||
gen_entry_unlink(tmpname, file);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -2117,15 +2596,15 @@ static void touch_up_dirs(struct file_list *flist, int ndx)
|
||||
continue;
|
||||
fname = f_name(file, NULL);
|
||||
if (fix_dir_perms)
|
||||
do_chmod_at(fname, file->mode);
|
||||
gen_entry_chmod(fname, file, file->mode);
|
||||
if (need_retouch_dir_times) {
|
||||
STRUCT_STAT st;
|
||||
if (link_stat(fname, &st, 0) == 0 && mtime_differs(&st, file)) {
|
||||
if (gen_entry_stat(fname, file, &st, 0) == 0 && mtime_differs(&st, file)) {
|
||||
st.st_mtime = file->modtime;
|
||||
#ifdef ST_MTIME_NSEC
|
||||
st.ST_MTIME_NSEC = F_MOD_NSEC_or_0(file);
|
||||
#endif
|
||||
set_times(fname, &st);
|
||||
gen_entry_set_times(fname, file, &st);
|
||||
}
|
||||
}
|
||||
if (counter >= loopchk_limit) {
|
||||
@@ -2226,7 +2705,8 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)
|
||||
|
||||
if (delete_during == 2 || !dir_tweaking) {
|
||||
/* Skip directory touch-up. */
|
||||
} else if (first_flist->parent_ndx >= 0)
|
||||
} else if (first_flist->parent_ndx >= 0
|
||||
&& first_flist->parent_ndx < dir_flist->used)
|
||||
touch_up_dirs(dir_flist, first_flist->parent_ndx);
|
||||
|
||||
flist_free(first_flist); /* updates first_flist */
|
||||
@@ -2297,7 +2777,8 @@ void generate_files(int f_out, const char *local_name)
|
||||
}
|
||||
#endif
|
||||
|
||||
if (inc_recurse && cur_flist->parent_ndx >= 0) {
|
||||
if (inc_recurse && cur_flist->parent_ndx >= 0
|
||||
&& cur_flist->parent_ndx < dir_flist->used) {
|
||||
struct file_struct *fp = dir_flist->files[cur_flist->parent_ndx];
|
||||
if (solo_file)
|
||||
strlcpy(fbuf, solo_file, sizeof fbuf);
|
||||
@@ -2384,7 +2865,7 @@ void generate_files(int f_out, const char *local_name)
|
||||
write_ndx(f_out, NDX_DONE);
|
||||
|
||||
if (protocol_version >= 31 && EARLY_DELETE_DONE_MSG()) {
|
||||
if ((INFO_GTE(STATS, 2) && (delete_mode || force_delete)) || read_batch)
|
||||
if (delete_mode || force_delete || read_batch)
|
||||
write_del_stats(f_out);
|
||||
if (EARLY_DELAY_DONE_MSG()) /* Can't send this before delay */
|
||||
write_ndx(f_out, NDX_DONE);
|
||||
@@ -2429,7 +2910,7 @@ void generate_files(int f_out, const char *local_name)
|
||||
|
||||
if (protocol_version >= 31) {
|
||||
if (!EARLY_DELETE_DONE_MSG()) {
|
||||
if (INFO_GTE(STATS, 2) || read_batch)
|
||||
if (delete_mode || force_delete || read_batch)
|
||||
write_del_stats(f_out);
|
||||
write_ndx(f_out, NDX_DONE);
|
||||
}
|
||||
|
||||
@@ -57,5 +57,6 @@
|
||||
printf("%lu", (unsigned long)gid);
|
||||
printf("\n");
|
||||
|
||||
free(list);
|
||||
return 0;
|
||||
}
|
||||
+25
-10
@@ -19,7 +19,7 @@
|
||||
|
||||
#include "rsync.h"
|
||||
|
||||
#define HASH_LOAD_LIMIT(size) ((size)*3/4)
|
||||
#define HASH_LOAD_LIMIT(size) ((size)/4*3) /* /4 first: never overflows int */
|
||||
|
||||
struct hashtable *hashtable_create(int size, int key64)
|
||||
{
|
||||
@@ -28,15 +28,25 @@ struct hashtable *hashtable_create(int size, int key64)
|
||||
int node_size = key64 ? sizeof (struct ht_int64_node)
|
||||
: sizeof (struct ht_int32_node);
|
||||
|
||||
/* Pick a power of 2 that can hold the requested size. */
|
||||
if (size & (size-1) || size < 16) {
|
||||
/* Pick a power of 2 that can hold the requested size. Test size < 16 first
|
||||
* so a negative/zero req short-circuits before the size-1 (INT_MIN is UB). */
|
||||
if (size < 16 || (size & (size-1))) {
|
||||
size = 16;
|
||||
while (size < req)
|
||||
while (size < req) {
|
||||
if (size > INT_MAX/2) { /* the next doubling would overflow int */
|
||||
rprintf(FERROR, "[%s] hashtable_create: requested size %d is too large\n",
|
||||
who_am_i(), req);
|
||||
exit_cleanup(RERR_MALLOC);
|
||||
}
|
||||
size *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
tbl = new(struct hashtable);
|
||||
tbl->nodes = new_array0(char, size * node_size);
|
||||
/* Pass size and node_size as SEPARATE factors so my_alloc's overflow /
|
||||
* --max-alloc guard sees both; computing size*node_size as int would wrap to
|
||||
* a tiny count and under-allocate (heap overflow on later node access). */
|
||||
tbl->nodes = my_alloc(do_calloc, size, node_size, __FILE__, __LINE__);
|
||||
tbl->size = size;
|
||||
tbl->entries = 0;
|
||||
tbl->node_size = node_size;
|
||||
@@ -90,10 +100,15 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)
|
||||
|
||||
if (data_when_new && tbl->entries > HASH_LOAD_LIMIT(tbl->size)) {
|
||||
void *old_nodes = tbl->nodes;
|
||||
int size = tbl->size * 2;
|
||||
int i;
|
||||
int size, i;
|
||||
|
||||
tbl->nodes = new_array0(char, size * tbl->node_size);
|
||||
if (tbl->size > INT_MAX/2) { /* doubling would overflow int */
|
||||
rprintf(FERROR, "[%s] hashtable grow: size overflow\n", who_am_i());
|
||||
exit_cleanup(RERR_MALLOC);
|
||||
}
|
||||
size = tbl->size * 2;
|
||||
/* Separate factors so my_alloc's guard sees both (see hashtable_create). */
|
||||
tbl->nodes = my_alloc(do_calloc, size, tbl->node_size, __FILE__, __LINE__);
|
||||
tbl->size = size;
|
||||
tbl->entries = 0;
|
||||
|
||||
@@ -120,7 +135,7 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)
|
||||
|
||||
if (!key64) {
|
||||
/* Based on Jenkins One-at-a-time hash. */
|
||||
uchar buf[4], *keyp = buf;
|
||||
uchar buf[4] = {0}, *keyp = buf; /* {0} only to satisfy the analyzer (SIVALu fills buf) */
|
||||
int i;
|
||||
|
||||
SIVALu(buf, 0, key);
|
||||
@@ -351,7 +366,7 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)
|
||||
*/
|
||||
|
||||
#define NON_ZERO_32(x) ((x) ? (x) : (uint32_t)1)
|
||||
#define NON_ZERO_64(x, y) ((x) || (y) ? (y) | (int64)(x) << 32 | (y) : (int64)1)
|
||||
#define NON_ZERO_64(x, y) ((x) || (y) ? (y) | (uint64_t)(x) << 32 | (y) : (int64)1)
|
||||
|
||||
uint32_t hashlittle(const void *key, size_t length)
|
||||
{
|
||||
|
||||
@@ -125,8 +125,22 @@ static void match_gnums(int32 *ndx_list, int ndx_count)
|
||||
if (inc_recurse) {
|
||||
node = hashtable_find(prior_hlinks, gnum, data_when_new);
|
||||
if (node->data == data_when_new) {
|
||||
if (gnum < hlink_flist->ndx_start) {
|
||||
/* A non-first hard-link entry whose
|
||||
* gnum points before this flist's
|
||||
* ndx_start should already have been
|
||||
* recorded in prior_hlinks by an
|
||||
* earlier flist. A peer that sends
|
||||
* such a back-reference on the first
|
||||
* flist (or to a gnum that was never
|
||||
* declared XMIT_HLINK_FIRST) is
|
||||
* misbehaving. */
|
||||
rprintf(FERROR,
|
||||
"hard-link gnum %d precedes flist start %d\n",
|
||||
(int)gnum, (int)hlink_flist->ndx_start);
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
node->data = new_array0(char, 5);
|
||||
assert(gnum >= hlink_flist->ndx_start);
|
||||
file->flags |= FLAG_HLINK_FIRST;
|
||||
prev = -1;
|
||||
} else if (CVAL(node->data, 0) == 0) {
|
||||
@@ -406,7 +420,14 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st))
|
||||
/* Content-based basis match only applies to regular
|
||||
* files: for a hard-linked symlink/device/special the
|
||||
* exact-inode check above is the only meaningful test,
|
||||
* and quick_check_ok(FT_REG, ...) would read F_SUM()
|
||||
* on a file_struct that has no SUM_EXTRA_CNT space
|
||||
* (recv_file_entry only allocates it for S_ISREG). */
|
||||
if (!S_ISREG(file->mode)
|
||||
|| !quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st))
|
||||
continue;
|
||||
statret = 1;
|
||||
if (unchanged_attrs(cmpbuf, file, &alt_sx))
|
||||
@@ -430,7 +451,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
|
||||
if (preserve_xattrs) {
|
||||
free_xattr(sxp);
|
||||
if (!XATTR_READY(alt_sx))
|
||||
get_xattr(cmpbuf, sxp);
|
||||
get_xattr(cmpbuf, -1, sxp);
|
||||
else {
|
||||
sxp->xattr = alt_sx.xattr;
|
||||
alt_sx.xattr = NULL;
|
||||
|
||||
@@ -31,7 +31,15 @@
|
||||
#include "ifuncs.h"
|
||||
#include "inums.h"
|
||||
|
||||
/** If no timeout is specified then use a 60 second select timeout */
|
||||
#include <poll.h>
|
||||
|
||||
/* Readiness bits we act on. poll() can report POLLERR/POLLHUP/POLLNVAL even
|
||||
* when they were not requested, and POLLPRI stands in for select()'s old
|
||||
* exception set. */
|
||||
#define POLL_RD_BITS (POLLIN | POLLPRI | POLLERR | POLLHUP)
|
||||
#define POLL_WR_BITS (POLLOUT | POLLERR | POLLHUP)
|
||||
|
||||
/** If no timeout is specified then use a 60 second I/O timeout */
|
||||
#define SELECT_TIMEOUT 60
|
||||
|
||||
extern int bwlimit;
|
||||
@@ -59,6 +67,7 @@ extern int xfer_sum_len;
|
||||
extern int daemon_connection;
|
||||
extern int protocol_version;
|
||||
extern int remove_source_files;
|
||||
extern int write_batch;
|
||||
extern int preserve_hard_links;
|
||||
extern BOOL extra_flist_sending_enabled;
|
||||
extern BOOL flush_ok_after_signal;
|
||||
@@ -79,6 +88,7 @@ BOOL flist_receiving_enabled = False;
|
||||
/* Ignore an EOF error if non-zero. See whine_about_eof(). */
|
||||
int kluge_around_eof = 0;
|
||||
int got_kill_signal = -1; /* is set to 0 only after multiplexed I/O starts */
|
||||
volatile sig_atomic_t got_sigusr2 = 0; /* set by the async-signal-safe SIGUSR2 handler */
|
||||
|
||||
int sock_f_in = -1;
|
||||
int sock_f_out = -1;
|
||||
@@ -102,6 +112,11 @@ static struct {
|
||||
static time_t last_io_in;
|
||||
static time_t last_io_out;
|
||||
|
||||
/* Absolute wall-clock bound for peer-controlled daemon handshake reads.
|
||||
* This is deliberately separate from io_timeout: the latter is an idle
|
||||
* transfer timeout and may be supplied by the module or client. */
|
||||
static time_t daemon_handshake_deadline;
|
||||
|
||||
static int write_batch_monitor_in = -1;
|
||||
static int write_batch_monitor_out = -1;
|
||||
|
||||
@@ -113,6 +128,38 @@ static xbuf ff_xb = EMPTY_XBUF;
|
||||
static xbuf iconv_buf = EMPTY_XBUF;
|
||||
#endif
|
||||
static int select_timeout = SELECT_TIMEOUT;
|
||||
|
||||
/* Turn select_timeout (in seconds) into a poll() millisecond count, keeping it
|
||||
* positive and bounded. A negative count means "wait forever" to poll(), which
|
||||
* would bypass our keepalives and timeout enforcement entirely. */
|
||||
static int poll_timeout_ms(void)
|
||||
{
|
||||
int secs = select_timeout;
|
||||
|
||||
if (secs <= 0 || secs > SELECT_TIMEOUT)
|
||||
secs = SELECT_TIMEOUT;
|
||||
return secs * 1000;
|
||||
}
|
||||
|
||||
static int handshake_poll_timeout_ms(void)
|
||||
{
|
||||
time_t now, left;
|
||||
int timeout = poll_timeout_ms();
|
||||
|
||||
if (!daemon_handshake_deadline)
|
||||
return timeout;
|
||||
|
||||
now = time(NULL);
|
||||
left = daemon_handshake_deadline - now;
|
||||
if (left <= 0) {
|
||||
rprintf(FERROR, "[%s] daemon handshake timeout -- exiting\n", who_am_i());
|
||||
exit_cleanup(RERR_TIMEOUT);
|
||||
}
|
||||
if (left <= INT_MAX / 1000 && left * 1000 < timeout)
|
||||
timeout = (int)left * 1000;
|
||||
return timeout;
|
||||
}
|
||||
|
||||
static int active_filecnt = 0;
|
||||
static OFF_T active_bytecnt = 0;
|
||||
static int first_message = 1;
|
||||
@@ -220,9 +267,15 @@ static NORETURN void whine_about_eof(BOOL allow_kluge)
|
||||
int i;
|
||||
if (kluge_around_eof > 0)
|
||||
exit_cleanup(0);
|
||||
/* If we're still here after 10 seconds, exit with an error. */
|
||||
for (i = 10*1000/20; i--; )
|
||||
/* The receiver is waiting here for the generator's SIGUSR2; act on it
|
||||
* (exit cleanly) the moment it arrives rather than sleeping the full
|
||||
* 10s and then erroring. The async-signal-safe handler only sets the
|
||||
* flag, so this loop must poll it. */
|
||||
for (i = 10*1000/20; i--; ) {
|
||||
if (got_sigusr2)
|
||||
receive_sigusr2();
|
||||
msleep(20);
|
||||
}
|
||||
}
|
||||
|
||||
rprintf(FERROR, RSYNC_NAME ": connection unexpectedly closed "
|
||||
@@ -243,31 +296,35 @@ static size_t safe_read(int fd, char *buf, size_t len)
|
||||
assert(fd != iobuf.in_fd);
|
||||
|
||||
while (1) {
|
||||
struct timeval tv;
|
||||
fd_set r_fds, e_fds;
|
||||
struct pollfd pfd;
|
||||
int cnt;
|
||||
|
||||
FD_ZERO(&r_fds);
|
||||
FD_SET(fd, &r_fds);
|
||||
FD_ZERO(&e_fds);
|
||||
FD_SET(fd, &e_fds);
|
||||
tv.tv_sec = select_timeout;
|
||||
tv.tv_usec = 0;
|
||||
if (got_sigusr2) /* receiver told to wrap up (e.g. a --read-batch fd) */
|
||||
receive_sigusr2();
|
||||
|
||||
cnt = select(fd+1, &r_fds, NULL, &e_fds, &tv);
|
||||
/* We use poll() rather than select() so that a high-numbered fd
|
||||
* (>= FD_SETSIZE) cannot overflow an fd_set bitmap. */
|
||||
pfd.fd = fd;
|
||||
pfd.events = POLLIN | POLLPRI;
|
||||
pfd.revents = 0;
|
||||
|
||||
cnt = poll(&pfd, 1, handshake_poll_timeout_ms());
|
||||
if (cnt <= 0) {
|
||||
if (cnt < 0 && errno == EBADF) {
|
||||
rsyserr(FERROR, errno, "safe_read select failed");
|
||||
if (cnt < 0 && errno != EINTR && errno != EAGAIN) {
|
||||
rsyserr(FERROR, errno, "safe_read poll failed");
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
check_timeout(1, MSK_ALLOW_FLUSH);
|
||||
continue;
|
||||
}
|
||||
|
||||
/*if (FD_ISSET(fd, &e_fds))
|
||||
rprintf(FINFO, "select exception on fd %d\n", fd); */
|
||||
/* An invalid fd is reported here rather than via poll()'s return. */
|
||||
if (pfd.revents & POLLNVAL) {
|
||||
rsyserr(FERROR, EBADF, "safe_read poll failed");
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
|
||||
if (FD_ISSET(fd, &r_fds)) {
|
||||
if (pfd.revents & POLL_RD_BITS) {
|
||||
ssize_t n = read(fd, buf + got, len - got);
|
||||
if (DEBUG_GTE(IO, 2)) {
|
||||
rprintf(FINFO, "[%s] safe_read(%d)=%" SIZE_T_FMT_MOD "d\n",
|
||||
@@ -315,6 +372,9 @@ static void safe_write(int fd, const char *buf, size_t len)
|
||||
|
||||
assert(fd != iobuf.out_fd);
|
||||
|
||||
if (got_sigusr2) /* receiver told to wrap up before this (batch) write */
|
||||
receive_sigusr2();
|
||||
|
||||
n = write(fd, buf, len);
|
||||
if ((size_t)n == len)
|
||||
return;
|
||||
@@ -332,19 +392,21 @@ static void safe_write(int fd, const char *buf, size_t len)
|
||||
}
|
||||
|
||||
while (len) {
|
||||
struct timeval tv;
|
||||
fd_set w_fds;
|
||||
struct pollfd pfd;
|
||||
int cnt;
|
||||
|
||||
FD_ZERO(&w_fds);
|
||||
FD_SET(fd, &w_fds);
|
||||
tv.tv_sec = select_timeout;
|
||||
tv.tv_usec = 0;
|
||||
if (got_sigusr2) /* receiver told to wrap up (e.g. a --write-batch fd) */
|
||||
receive_sigusr2();
|
||||
|
||||
cnt = select(fd + 1, NULL, &w_fds, NULL, &tv);
|
||||
/* poll() avoids the FD_SETSIZE limit that select() imposes. */
|
||||
pfd.fd = fd;
|
||||
pfd.events = POLLOUT;
|
||||
pfd.revents = 0;
|
||||
|
||||
cnt = poll(&pfd, 1, poll_timeout_ms());
|
||||
if (cnt <= 0) {
|
||||
if (cnt < 0 && errno == EBADF) {
|
||||
rsyserr(FERROR, errno, "safe_write select failed on %s", what_fd_is(fd));
|
||||
if (cnt < 0 && errno != EINTR && errno != EAGAIN) {
|
||||
rsyserr(FERROR, errno, "safe_write poll failed on %s", what_fd_is(fd));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
if (io_timeout)
|
||||
@@ -352,7 +414,12 @@ static void safe_write(int fd, const char *buf, size_t len)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (FD_ISSET(fd, &w_fds)) {
|
||||
if (pfd.revents & POLLNVAL) {
|
||||
rsyserr(FERROR, EBADF, "safe_write poll failed on %s", what_fd_is(fd));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
|
||||
if (pfd.revents & POLL_WR_BITS) {
|
||||
n = write(fd, buf, len);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
@@ -561,9 +628,8 @@ static void handle_kill_signal(BOOL flush_ok)
|
||||
* unused raw data in the buf would prevent the reading of socket data. */
|
||||
static char *perform_io(size_t needed, int flags)
|
||||
{
|
||||
fd_set r_fds, e_fds, w_fds;
|
||||
struct timeval tv;
|
||||
int cnt, max_fd;
|
||||
struct pollfd pfds[3];
|
||||
int cnt, max_fd, npfds, poll_timeout, in_pollpos, out_pollpos, ff_pollpos;
|
||||
size_t empty_buf_len = 0;
|
||||
xbuf *out;
|
||||
char *data;
|
||||
@@ -656,13 +722,15 @@ static char *perform_io(size_t needed, int flags)
|
||||
}
|
||||
|
||||
max_fd = -1;
|
||||
npfds = 0;
|
||||
in_pollpos = out_pollpos = ff_pollpos = -1;
|
||||
|
||||
FD_ZERO(&r_fds);
|
||||
FD_ZERO(&e_fds);
|
||||
if (iobuf.in_fd >= 0 && iobuf.in.size - iobuf.in.len) {
|
||||
if (!read_batch || batch_fd >= 0) {
|
||||
FD_SET(iobuf.in_fd, &r_fds);
|
||||
FD_SET(iobuf.in_fd, &e_fds);
|
||||
pfds[npfds].fd = iobuf.in_fd;
|
||||
pfds[npfds].events = POLLIN | POLLPRI;
|
||||
pfds[npfds].revents = 0;
|
||||
in_pollpos = npfds++;
|
||||
}
|
||||
if (iobuf.in_fd > max_fd)
|
||||
max_fd = iobuf.in_fd;
|
||||
@@ -670,12 +738,14 @@ static char *perform_io(size_t needed, int flags)
|
||||
|
||||
/* Only do more filesfrom processing if there is enough room in the out buffer. */
|
||||
if (ff_forward_fd >= 0 && iobuf.out.size - iobuf.out.len > FILESFROM_BUFLEN*2) {
|
||||
FD_SET(ff_forward_fd, &r_fds);
|
||||
pfds[npfds].fd = ff_forward_fd;
|
||||
pfds[npfds].events = POLLIN;
|
||||
pfds[npfds].revents = 0;
|
||||
ff_pollpos = npfds++;
|
||||
if (ff_forward_fd > max_fd)
|
||||
max_fd = ff_forward_fd;
|
||||
}
|
||||
|
||||
FD_ZERO(&w_fds);
|
||||
if (iobuf.out_fd >= 0) {
|
||||
if (iobuf.raw_flushing_ends_before
|
||||
|| (!iobuf.msg.len && iobuf.out.len > iobuf.out_empty_len && !(flags & PIO_NEED_MSGROOM))) {
|
||||
@@ -715,7 +785,18 @@ static char *perform_io(size_t needed, int flags)
|
||||
} else
|
||||
out = NULL;
|
||||
if (out) {
|
||||
FD_SET(iobuf.out_fd, &w_fds);
|
||||
/* A direct daemon connection uses one fd for both
|
||||
* directions; give it a single row with both events
|
||||
* rather than two rows carrying different masks. */
|
||||
if (in_pollpos >= 0 && iobuf.out_fd == iobuf.in_fd) {
|
||||
pfds[in_pollpos].events |= POLLOUT;
|
||||
out_pollpos = in_pollpos;
|
||||
} else {
|
||||
pfds[npfds].fd = iobuf.out_fd;
|
||||
pfds[npfds].events = POLLOUT;
|
||||
pfds[npfds].revents = 0;
|
||||
out_pollpos = npfds++;
|
||||
}
|
||||
if (iobuf.out_fd > max_fd)
|
||||
max_fd = iobuf.out_fd;
|
||||
}
|
||||
@@ -749,19 +830,20 @@ static char *perform_io(size_t needed, int flags)
|
||||
|
||||
if (got_kill_signal > 0)
|
||||
handle_kill_signal(True);
|
||||
if (got_sigusr2)
|
||||
receive_sigusr2();
|
||||
|
||||
if (extra_flist_sending_enabled) {
|
||||
if (file_total - file_old_total < MAX_FILECNT_LOOKAHEAD && IN_MULTIPLEXED_AND_READY)
|
||||
tv.tv_sec = 0;
|
||||
poll_timeout = 0;
|
||||
else {
|
||||
extra_flist_sending_enabled = False;
|
||||
tv.tv_sec = select_timeout;
|
||||
poll_timeout = poll_timeout_ms();
|
||||
}
|
||||
} else
|
||||
tv.tv_sec = select_timeout;
|
||||
tv.tv_usec = 0;
|
||||
poll_timeout = poll_timeout_ms();
|
||||
|
||||
cnt = select(max_fd + 1, &r_fds, &w_fds, &e_fds, &tv);
|
||||
cnt = poll(pfds, npfds, poll_timeout);
|
||||
|
||||
if (cnt <= 0) {
|
||||
if (cnt < 0 && errno == EBADF) {
|
||||
@@ -774,11 +856,29 @@ static char *perform_io(size_t needed, int flags)
|
||||
extra_flist_sending_enabled = !flist_eof;
|
||||
} else
|
||||
check_timeout((flags & PIO_NEED_INPUT) != 0, 0);
|
||||
FD_ZERO(&r_fds); /* Just in case... */
|
||||
FD_ZERO(&w_fds);
|
||||
/* Just in case... */
|
||||
if (in_pollpos >= 0)
|
||||
pfds[in_pollpos].revents = 0;
|
||||
if (ff_pollpos >= 0)
|
||||
pfds[ff_pollpos].revents = 0;
|
||||
if (out_pollpos >= 0)
|
||||
pfds[out_pollpos].revents = 0;
|
||||
}
|
||||
|
||||
if (iobuf.in_fd >= 0 && FD_ISSET(iobuf.in_fd, &r_fds)) {
|
||||
if (cnt > 0) {
|
||||
/* poll() reports a bad fd here, not via its return value. */
|
||||
int p;
|
||||
for (p = 0; p < npfds; p++) {
|
||||
if (pfds[p].revents & POLLNVAL) {
|
||||
msgs2stderr = 1;
|
||||
rsyserr(FERROR, EBADF, "perform_io: poll reported an invalid fd");
|
||||
exit_cleanup(RERR_SOCKETIO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (iobuf.in_fd >= 0 && in_pollpos >= 0
|
||||
&& pfds[in_pollpos].revents & POLL_RD_BITS) {
|
||||
size_t len, pos = iobuf.in.pos + iobuf.in.len;
|
||||
ssize_t n;
|
||||
if (pos >= iobuf.in.size) {
|
||||
@@ -827,7 +927,7 @@ static char *perform_io(size_t needed, int flags)
|
||||
exit_cleanup(RERR_TIMEOUT);
|
||||
}
|
||||
|
||||
if (out && FD_ISSET(iobuf.out_fd, &w_fds)) {
|
||||
if (out && out_pollpos >= 0 && pfds[out_pollpos].revents & POLL_WR_BITS) {
|
||||
size_t len = iobuf.raw_flushing_ends_before ? iobuf.raw_flushing_ends_before - out->pos : out->len;
|
||||
ssize_t n;
|
||||
|
||||
@@ -878,6 +978,8 @@ static char *perform_io(size_t needed, int flags)
|
||||
|
||||
if (got_kill_signal > 0)
|
||||
handle_kill_signal(True);
|
||||
if (got_sigusr2)
|
||||
receive_sigusr2();
|
||||
|
||||
/* We need to help prevent deadlock by doing what reading
|
||||
* we can whenever we are here trying to write. */
|
||||
@@ -888,7 +990,8 @@ static char *perform_io(size_t needed, int flags)
|
||||
wait_for_receiver(); /* generator only */
|
||||
}
|
||||
|
||||
if (ff_forward_fd >= 0 && FD_ISSET(ff_forward_fd, &r_fds)) {
|
||||
if (ff_forward_fd >= 0 && ff_pollpos >= 0
|
||||
&& pfds[ff_pollpos].revents & POLL_RD_BITS) {
|
||||
/* This can potentially flush all output and enable
|
||||
* multiplexed output, so keep this last in the loop
|
||||
* and be sure to not cache anything that would break
|
||||
@@ -900,6 +1003,8 @@ static char *perform_io(size_t needed, int flags)
|
||||
|
||||
if (got_kill_signal > 0)
|
||||
handle_kill_signal(True);
|
||||
if (got_sigusr2)
|
||||
receive_sigusr2();
|
||||
|
||||
data = iobuf.in.buf + iobuf.in.pos;
|
||||
|
||||
@@ -1070,17 +1175,30 @@ void send_msg_int(enum msgcode code, int num)
|
||||
|
||||
void send_msg_success(const char *fname, int num)
|
||||
{
|
||||
/* Batch-only mode has not duplicated anything on the receiving side yet.
|
||||
* The receiver still reports success to the generator for file-list and
|
||||
* hard-link bookkeeping, but the generator must not turn that status into
|
||||
* sender-side removal. */
|
||||
if (am_generator && write_batch < 0 && remove_source_files)
|
||||
return;
|
||||
|
||||
if (local_server) {
|
||||
STRUCT_STAT st;
|
||||
|
||||
if (DEBUG_GTE(IO, 1))
|
||||
rprintf(FINFO, "[%s] send_msg_success(%d)\n", who_am_i(), num);
|
||||
|
||||
if (stat(fname, &st) < 0)
|
||||
memset(&st, 0, sizeof (STRUCT_STAT));
|
||||
/* The dev/ino is consumed only by the sender's --remove-source-files
|
||||
* same-file safety check (successful_send), so skip the per-file
|
||||
* stat entirely otherwise -- it's sent but never read. */
|
||||
if (remove_source_files && stat(fname, &st) == 0) {
|
||||
SIVAL64(num_dev_ino_buf, 4, st.st_dev);
|
||||
SIVAL64(num_dev_ino_buf, 4+8, st.st_ino);
|
||||
} else {
|
||||
SIVAL64(num_dev_ino_buf, 4, 0);
|
||||
SIVAL64(num_dev_ino_buf, 4+8, 0);
|
||||
}
|
||||
SIVAL(num_dev_ino_buf, 0, num);
|
||||
SIVAL64(num_dev_ino_buf, 4, st.st_dev);
|
||||
SIVAL64(num_dev_ino_buf, 4+8, st.st_ino);
|
||||
send_msg(MSG_SUCCESS, num_dev_ino_buf, sizeof num_dev_ino_buf, -1);
|
||||
} else
|
||||
send_msg_int(MSG_SUCCESS, num);
|
||||
@@ -1103,7 +1221,7 @@ static void got_flist_entry_status(enum festatus status, int ndx)
|
||||
|
||||
switch (status) {
|
||||
case FES_SUCCESS:
|
||||
if (remove_source_files) {
|
||||
if (remove_source_files && write_batch >= 0) {
|
||||
if (local_server)
|
||||
send_msg(MSG_SUCCESS, num_dev_ino_buf, sizeof num_dev_ino_buf, -1);
|
||||
else
|
||||
@@ -1147,8 +1265,26 @@ void io_set_sock_fds(int f_in, int f_out)
|
||||
|
||||
void set_io_timeout(int secs)
|
||||
{
|
||||
/* A negative timeout is meaningless; treat it as "no timeout" rather than
|
||||
* letting it drive allowed_lull / select_timeout negative (a tight loop).
|
||||
* (--timeout is parsed by options.c as a plain int, so it can be negative.) */
|
||||
if (secs < 0)
|
||||
secs = 0;
|
||||
io_timeout = secs;
|
||||
allowed_lull = (io_timeout + 1) / 2;
|
||||
/* Compute ceil(io_timeout/2) in a wider type: io_timeout can be INT_MAX
|
||||
* (a peer's MSG_IO_TIMEOUT -- now capped in read_a_msg() -- or an operator
|
||||
* --timeout, which options.c parses unbounded), and a plain "io_timeout + 1"
|
||||
* would overflow to a negative allowed_lull / select_timeout. poll() now
|
||||
* takes a millisecond count where negative means "wait forever", so this
|
||||
* would hang the process rather than spin it -- and it still fires a
|
||||
* keepalive flood. poll_timeout_ms() clamps as well; keep both. */
|
||||
allowed_lull = (int)(((int64)io_timeout + 1) / 2);
|
||||
/* The generator and sender derive an int loop-check limit as
|
||||
* allowed_lull * 5; keep allowed_lull small enough that that product can't
|
||||
* overflow either. The cap is invisible to real use -- allowed_lull is the
|
||||
* keep-alive half-interval and INT_MAX/5 seconds is over 13 years. */
|
||||
if (allowed_lull > INT_MAX / 5)
|
||||
allowed_lull = INT_MAX / 5;
|
||||
|
||||
if (!io_timeout || allowed_lull > SELECT_TIMEOUT)
|
||||
select_timeout = SELECT_TIMEOUT;
|
||||
@@ -1159,6 +1295,14 @@ void set_io_timeout(int secs)
|
||||
allowed_lull = 0;
|
||||
}
|
||||
|
||||
void set_daemon_handshake_timeout(int secs)
|
||||
{
|
||||
if (secs > 0)
|
||||
daemon_handshake_deadline = time(NULL) + secs;
|
||||
else
|
||||
daemon_handshake_deadline = 0;
|
||||
}
|
||||
|
||||
static void check_for_d_option_error(const char *msg)
|
||||
{
|
||||
static const char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz";
|
||||
@@ -1292,8 +1436,23 @@ int read_line(int fd, char *buf, size_t bufsiz, int flags)
|
||||
return s - buf;
|
||||
}
|
||||
|
||||
/* Reverse safe_arg()'s backslash escaping of a daemon option arg, the way a
|
||||
* remote shell un-escapes args for the ssh transport. In place; \X -> X. */
|
||||
static void unbackslash_arg(char *s)
|
||||
{
|
||||
char *f = s, *t = s;
|
||||
while (*f) {
|
||||
if (*f == '\\' && f[1])
|
||||
f++;
|
||||
*t++ = *f++;
|
||||
}
|
||||
*t = '\0';
|
||||
}
|
||||
|
||||
#define MAX_DAEMON_ARGS (MAX_ARGS * 16)
|
||||
|
||||
void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,
|
||||
char ***argv_p, int *argc_p, char **request_p)
|
||||
int unescape, char ***argv_p, int *argc_p, char **request_p)
|
||||
{
|
||||
int maxargs = MAX_ARGS;
|
||||
int dot_pos = 0, argc = 0, request_len = 0;
|
||||
@@ -1315,6 +1474,11 @@ void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,
|
||||
if (read_line(f_in, buf, bufsiz, rl_flags) == 0)
|
||||
break;
|
||||
|
||||
if (mod_name && argc >= MAX_DAEMON_ARGS - 1) {
|
||||
rprintf(FERROR, "too many daemon arguments\n");
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
|
||||
if (argc == maxargs-1) {
|
||||
maxargs += MAX_ARGS;
|
||||
argv = realloc_array(argv, char *, maxargs);
|
||||
@@ -1335,11 +1499,23 @@ void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,
|
||||
glob_expand(buf, &argv, &argc, &maxargs);
|
||||
} else {
|
||||
p = strdup(buf);
|
||||
/* An option arg the client escaped with safe_arg() (no
|
||||
* remote shell un-escapes it for a daemon). File args
|
||||
* after the dot are handled by glob_expand() below. */
|
||||
if (unescape)
|
||||
unbackslash_arg(p);
|
||||
argv[argc++] = p;
|
||||
if (*p == '.' && p[1] == '\0')
|
||||
dot_pos = argc;
|
||||
}
|
||||
}
|
||||
/* glob_expand()/glob_match() reserve glob.argc+1 slots -- room for the
|
||||
* entry being added but not for this trailing NULL. A post-dot line
|
||||
* whose " mod/" splits land argc on exactly maxargs (or any later
|
||||
* ENSURE_MEMSPACE doubling boundary) would otherwise make the next
|
||||
* store an 8-byte NULL write one slot past the argv allocation. */
|
||||
if (argc >= maxargs)
|
||||
argv = realloc_array(argv, char *, maxargs = argc + 1);
|
||||
argv[argc] = NULL;
|
||||
|
||||
glob_expand(NULL, NULL, NULL, NULL);
|
||||
@@ -1356,8 +1532,9 @@ BOOL io_start_buffering_out(int f_out)
|
||||
if (iobuf.out.buf) {
|
||||
if (iobuf.out_fd == -1)
|
||||
iobuf.out_fd = f_out;
|
||||
else
|
||||
else if (iobuf.out_fd >= 0)
|
||||
assert(f_out == iobuf.out_fd);
|
||||
/* else out_fd == -2: peer already gone; leave it dead. */
|
||||
return False;
|
||||
}
|
||||
|
||||
@@ -1375,8 +1552,9 @@ BOOL io_start_buffering_in(int f_in)
|
||||
if (iobuf.in.buf) {
|
||||
if (iobuf.in_fd == -1)
|
||||
iobuf.in_fd = f_in;
|
||||
else
|
||||
else if (iobuf.in_fd >= 0)
|
||||
assert(f_in == iobuf.in_fd);
|
||||
/* else in_fd == -2: peer already EOF'd; leave it dead. */
|
||||
return False;
|
||||
}
|
||||
|
||||
@@ -1525,16 +1703,26 @@ static void read_a_msg(void)
|
||||
if (msg_bytes != 4)
|
||||
goto invalid_msg;
|
||||
val = raw_read_int();
|
||||
iobuf.in_multiplexed = 1;
|
||||
val &= IOERR_VALID_MASK;
|
||||
io_error |= val;
|
||||
if (am_receiver)
|
||||
send_msg_int(MSG_IO_ERROR, val);
|
||||
iobuf.in_multiplexed = 1;
|
||||
break;
|
||||
case MSG_IO_TIMEOUT:
|
||||
if (msg_bytes != 4 || am_server || am_generator)
|
||||
goto invalid_msg;
|
||||
val = raw_read_int();
|
||||
iobuf.in_multiplexed = 1;
|
||||
/* The peer may only ask us to use a SHORTER timeout (a stricter cap); a
|
||||
* non-positive value would disable our --timeout entirely, letting a
|
||||
* malicious server hang the client indefinitely, so ignore it. A very
|
||||
* large value (near INT_MAX) would overflow the (io_timeout + 1) / 2
|
||||
* computation in set_io_timeout(), wrapping allowed_lull and
|
||||
* select_timeout negative -- which poll() reads as "wait forever",
|
||||
* hanging the client. Cap at 24 hours. */
|
||||
if (val <= 0 || val > 86400)
|
||||
break;
|
||||
if (!io_timeout || io_timeout > val) {
|
||||
if (INFO_GTE(MISC, 2))
|
||||
rprintf(FINFO, "Setting --timeout=%d to match server\n", val);
|
||||
@@ -1545,17 +1733,17 @@ static void read_a_msg(void)
|
||||
/* Support protocol-30 keep-alive method. */
|
||||
if (msg_bytes != 0)
|
||||
goto invalid_msg;
|
||||
iobuf.in_multiplexed = 1;
|
||||
if (am_sender)
|
||||
maybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH);
|
||||
iobuf.in_multiplexed = 1;
|
||||
break;
|
||||
case MSG_DELETED:
|
||||
if (msg_bytes >= sizeof data)
|
||||
goto overflow;
|
||||
if (am_generator) {
|
||||
raw_read_buf(data, msg_bytes);
|
||||
iobuf.in_multiplexed = 1;
|
||||
send_msg(MSG_DELETED, data, msg_bytes, 1);
|
||||
iobuf.in_multiplexed = 1;
|
||||
break;
|
||||
}
|
||||
#ifdef ICONV_OPTION
|
||||
@@ -1593,7 +1781,6 @@ static void read_a_msg(void)
|
||||
} else
|
||||
#endif
|
||||
raw_read_buf(data, msg_bytes);
|
||||
iobuf.in_multiplexed = 1;
|
||||
/* A directory name was sent with the trailing null */
|
||||
if (msg_bytes > 0 && !data[msg_bytes-1])
|
||||
log_delete(data, S_IFDIR);
|
||||
@@ -1601,6 +1788,7 @@ static void read_a_msg(void)
|
||||
data[msg_bytes] = '\0';
|
||||
log_delete(data, S_IFREG);
|
||||
}
|
||||
iobuf.in_multiplexed = 1;
|
||||
break;
|
||||
case MSG_SUCCESS:
|
||||
if (msg_bytes != (local_server ? 4+8+8 : 4)) {
|
||||
@@ -1622,11 +1810,11 @@ static void read_a_msg(void)
|
||||
if (msg_bytes != 4)
|
||||
goto invalid_msg;
|
||||
val = raw_read_int();
|
||||
iobuf.in_multiplexed = 1;
|
||||
if (am_generator)
|
||||
got_flist_entry_status(FES_NO_SEND, val);
|
||||
else
|
||||
send_msg_int(MSG_NO_SEND, val);
|
||||
iobuf.in_multiplexed = 1;
|
||||
break;
|
||||
case MSG_ERROR_SOCKET:
|
||||
case MSG_ERROR_UTF8:
|
||||
@@ -2034,6 +2222,21 @@ void read_sum_head(int f, struct sum_struct *sum)
|
||||
(long)sum->blength, who_am_i());
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
if (sum->count && sum->blength == 0) {
|
||||
rprintf(FERROR, "Invalid zero block length [%s]\n",
|
||||
who_am_i());
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
#if SIZEOF_CAPITAL_OFF_T < 8
|
||||
/* The append-mode callers compute (OFF_T)count * blength; on a 32-bit
|
||||
* OFF_T that product can wrap even though both factors are individually
|
||||
* in range, corrupting the lseek/loop bounds. Reject it early. */
|
||||
if (sum->blength > 0 && sum->count > MAX_INT32 / sum->blength) {
|
||||
rprintf(FERROR, "checksum count*blength overflows OFF_T [%s]\n",
|
||||
who_am_i());
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
#endif
|
||||
sum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f);
|
||||
if (sum->s2length < 0 || sum->s2length > xfer_sum_len) {
|
||||
rprintf(FERROR, "Invalid checksum length %d [%s]\n",
|
||||
@@ -2145,7 +2348,7 @@ void write_int(int f, int32 x)
|
||||
|
||||
void write_varint(int f, int32 x)
|
||||
{
|
||||
char b[5];
|
||||
char b[5] = {0}; /* {0} only to satisfy the analyzer: it doesn't model SIVAL initialising b[1..4] */
|
||||
uchar bit;
|
||||
int cnt;
|
||||
|
||||
@@ -2167,7 +2370,7 @@ void write_varint(int f, int32 x)
|
||||
|
||||
void write_varlong(int f, int64 x, uchar min_bytes)
|
||||
{
|
||||
char b[9];
|
||||
char b[9] = {0}; /* {0} only to satisfy the analyzer: it doesn't model SIVAL64 initialising b[1..8] */
|
||||
uchar bit;
|
||||
int cnt = 8;
|
||||
|
||||
@@ -2348,6 +2551,7 @@ int32 read_ndx(int f)
|
||||
{
|
||||
static int32 prev_positive = -1, prev_negative = 1;
|
||||
int32 *prev_ptr, num;
|
||||
uint32 unum;
|
||||
char b[4];
|
||||
|
||||
if (protocol_version < 30)
|
||||
@@ -2367,11 +2571,20 @@ int32 read_ndx(int f)
|
||||
b[3] = CVAL(b, 0) & ~0x80;
|
||||
b[0] = b[1];
|
||||
read_buf(f, b+1, 2);
|
||||
num = IVAL(b, 0);
|
||||
unum = IVAL(b, 0);
|
||||
} else
|
||||
num = (UVAL(b,0)<<8) + UVAL(b,1) + *prev_ptr;
|
||||
unum = (UVAL(b,0)<<8) + UVAL(b,1) + (uint32)*prev_ptr;
|
||||
} else
|
||||
num = UVAL(b, 0) + *prev_ptr;
|
||||
unum = UVAL(b, 0) + (uint32)*prev_ptr;
|
||||
/* A peer-supplied index that overflows a signed int32 (used unchecked as a
|
||||
* file-list index) is a protocol violation -- reject it here rather than
|
||||
* relying on every downstream consumer to bounds-check. */
|
||||
if (unum > (uint32)MAX_INT32) {
|
||||
rprintf(FERROR, "Invalid file index: %lu [%s]\n",
|
||||
(unsigned long)unum, who_am_i());
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
num = (int32)unum;
|
||||
*prev_ptr = num;
|
||||
if (prev_ptr == &prev_negative)
|
||||
num = -num;
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* POSIX ACL get/set/delete via the generic xattr syscalls.
|
||||
*
|
||||
* POSIX ACLs are stored by the kernel as the "system.posix_acl_access" and
|
||||
* "system.posix_acl_default" extended attributes, in a fixed little-endian
|
||||
* wire format (see include/acl_ea.h in the acl package). By serializing that
|
||||
* format ourselves and using fgetxattr/fsetxattr on a held O_NOFOLLOW fd -- or
|
||||
* getxattrat/setxattrat(AT_SYMLINK_NOFOLLOW) on a dirfd+leaf -- we get a
|
||||
* symlink-race-safe ACL primitive that also covers the *default* ACL, which
|
||||
* libacl's fd API (acl_get_fd/acl_set_fd, access-only) cannot.
|
||||
*
|
||||
* This file knows nothing about rsync's globals or its internal ACL form: it
|
||||
* speaks a neutral (tag, perm, id) entry array, which makes it directly
|
||||
* comparable against the system libacl in the t_acl unit test.
|
||||
*
|
||||
* Copyright (C) 2026 Wayne Davison & the rsync project
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, visit the http://fsf.org website.
|
||||
*/
|
||||
|
||||
#include "rsync.h"
|
||||
#include "acl.h"
|
||||
|
||||
#ifdef SUPPORT_ACL_FD
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h> /* AT_SYMLINK_NOFOLLOW */
|
||||
|
||||
#if defined HAVE_SYS_XATTR_H
|
||||
#include <sys/xattr.h>
|
||||
#elif defined HAVE_ATTR_XATTR_H
|
||||
#include <attr/xattr.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_XATTRAT_SYSCALLS
|
||||
#include <sys/syscall.h>
|
||||
/* Self-contained copy of the kernel's struct xattr_args (stable ABI: an
|
||||
* 8-byte-aligned u64 pointer, then two u32s). Defined locally to avoid
|
||||
* pulling <linux/xattr.h>, whose XATTR_* macros clash with <sys/xattr.h>. */
|
||||
struct rsync_xattr_args {
|
||||
uint64_t value __attribute__((aligned(8)));
|
||||
uint32_t size;
|
||||
uint32_t flags;
|
||||
};
|
||||
#endif
|
||||
|
||||
/* Linux 2.4 didn't have a distinct ENOATTR. */
|
||||
#ifndef ENOATTR
|
||||
#define ENOATTR ENODATA
|
||||
#endif
|
||||
|
||||
#define ACL_XATTR_ACCESS "system.posix_acl_access"
|
||||
#define ACL_XATTR_DEFAULT "system.posix_acl_default"
|
||||
|
||||
/* On-disk layout: a 4-byte LE version header followed by 8-byte LE entries. */
|
||||
#define ACL_EA_VERSION 0x0002
|
||||
#define ACL_EA_HDR_LEN 4
|
||||
#define ACL_EA_ENT_LEN 8
|
||||
|
||||
/* === little-endian (de)serialization (host-endianness independent) === */
|
||||
|
||||
static void put_le16(unsigned char *p, uint16_t v)
|
||||
{
|
||||
p[0] = (unsigned char)(v & 0xff);
|
||||
p[1] = (unsigned char)((v >> 8) & 0xff);
|
||||
}
|
||||
|
||||
static void put_le32(unsigned char *p, uint32_t v)
|
||||
{
|
||||
p[0] = (unsigned char)(v & 0xff);
|
||||
p[1] = (unsigned char)((v >> 8) & 0xff);
|
||||
p[2] = (unsigned char)((v >> 16) & 0xff);
|
||||
p[3] = (unsigned char)((v >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static uint16_t get_le16(const unsigned char *p)
|
||||
{
|
||||
return (uint16_t)(p[0] | (p[1] << 8));
|
||||
}
|
||||
|
||||
static uint32_t get_le32(const unsigned char *p)
|
||||
{
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8)
|
||||
| ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static int is_named_tag(uint16_t tag)
|
||||
{
|
||||
return tag == RACL_USER || tag == RACL_GROUP;
|
||||
}
|
||||
|
||||
/* Canonical order: tag ascending, then id ascending within a tag. This is
|
||||
* the order libacl's __acl_reorder_obj_p() produces and what the kernel's
|
||||
* validator expects (USER_OBJ, USER*, GROUP_OBJ, GROUP*, MASK, OTHER). */
|
||||
static int ent_compare(const void *a, const void *b)
|
||||
{
|
||||
const rsync_acl_ent *x = a, *y = b;
|
||||
if (x->tag != y->tag)
|
||||
return x->tag < y->tag ? -1 : 1;
|
||||
if (x->id != y->id)
|
||||
return x->id < y->id ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Serialize entries into a freshly-malloc'd xattr buffer (canonical order). */
|
||||
static unsigned char *acl_to_xattr(const rsync_acl_ent *ents, int count, size_t *len_out)
|
||||
{
|
||||
size_t len = ACL_EA_HDR_LEN + (size_t)count * ACL_EA_ENT_LEN;
|
||||
unsigned char *buf = malloc(len);
|
||||
rsync_acl_ent *sorted = NULL;
|
||||
unsigned char *p;
|
||||
int i;
|
||||
|
||||
if (!buf)
|
||||
return NULL;
|
||||
if (count > 1) {
|
||||
sorted = malloc((size_t)count * sizeof sorted[0]);
|
||||
if (!sorted) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(sorted, ents, (size_t)count * sizeof sorted[0]);
|
||||
qsort(sorted, count, sizeof sorted[0], ent_compare);
|
||||
ents = sorted;
|
||||
}
|
||||
|
||||
put_le32(buf, ACL_EA_VERSION);
|
||||
p = buf + ACL_EA_HDR_LEN;
|
||||
for (i = 0; i < count; i++, p += ACL_EA_ENT_LEN) {
|
||||
put_le16(p, ents[i].tag);
|
||||
put_le16(p + 2, ents[i].perm);
|
||||
put_le32(p + 4, is_named_tag(ents[i].tag) ? ents[i].id : RACL_UNDEFINED_ID);
|
||||
}
|
||||
|
||||
if (sorted)
|
||||
free(sorted);
|
||||
*len_out = len;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Parse an xattr buffer into a malloc'd entry array (canonical order). */
|
||||
static int xattr_to_acl(const unsigned char *buf, size_t len,
|
||||
rsync_acl_ent **out, int *count_out)
|
||||
{
|
||||
rsync_acl_ent *ents;
|
||||
const unsigned char *p;
|
||||
int n, i;
|
||||
|
||||
if (len < ACL_EA_HDR_LEN || (len - ACL_EA_HDR_LEN) % ACL_EA_ENT_LEN != 0
|
||||
|| get_le32(buf) != ACL_EA_VERSION) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
n = (int)((len - ACL_EA_HDR_LEN) / ACL_EA_ENT_LEN);
|
||||
|
||||
ents = n ? malloc((size_t)n * sizeof ents[0]) : NULL;
|
||||
if (n && !ents)
|
||||
return -1;
|
||||
p = buf + ACL_EA_HDR_LEN;
|
||||
for (i = 0; i < n; i++, p += ACL_EA_ENT_LEN) {
|
||||
ents[i].tag = get_le16(p);
|
||||
ents[i].perm = get_le16(p + 2);
|
||||
ents[i].id = is_named_tag(ents[i].tag) ? get_le32(p + 4) : RACL_UNDEFINED_ID;
|
||||
}
|
||||
if (n > 1)
|
||||
qsort(ents, n, sizeof ents[0], ent_compare);
|
||||
|
||||
*out = ents;
|
||||
*count_out = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* === syscall dispatchers (fd-variant vs at-variant) === */
|
||||
|
||||
/* Pre-6.13 fallback for the dirfd+leaf at-variants: address the leaf as
|
||||
* /proc/self/fd/<dirfd>/<leaf> and use the l*xattr (no-follow-leaf) calls. The
|
||||
* /proc/self/fd/<dirfd> magic symlink resolves to the pinned parent inode -- a
|
||||
* raced parent symlink cannot redirect it -- and l*xattr does not follow a raced
|
||||
* leaf symlink, so this is race-safe without the Linux 6.13 *xattrat syscalls, as
|
||||
* long as procfs is mounted. (`leaf` is a single component, <= NAME_MAX.)
|
||||
* Returns 0 and fills `buf`, or -1 with ENAMETOOLONG. */
|
||||
static int proc_fd_leaf_path(char *buf, size_t buflen, int dirfd, const char *leaf)
|
||||
{
|
||||
int n = snprintf(buf, buflen, "/proc/self/fd/%d/%s", dirfd, leaf);
|
||||
if (n < 0 || (size_t)n >= buflen) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ssize_t do_getxattr(int fd, int dirfd, const char *leaf,
|
||||
const char *name, void *val, size_t size)
|
||||
{
|
||||
char p[MAXPATHLEN];
|
||||
|
||||
if (fd >= 0)
|
||||
return fgetxattr(fd, name, val, size);
|
||||
#ifdef HAVE_XATTRAT_SYSCALLS
|
||||
{
|
||||
struct rsync_xattr_args args;
|
||||
ssize_t ret;
|
||||
args.value = (uint64_t)(uintptr_t)val;
|
||||
args.size = (uint32_t)size;
|
||||
args.flags = 0;
|
||||
ret = syscall(SYS_getxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,
|
||||
name, &args, sizeof args);
|
||||
if (ret != -1 || errno != ENOSYS)
|
||||
return ret;
|
||||
/* ENOSYS: kernel < 6.13 -- fall through to the /proc compat. */
|
||||
}
|
||||
#endif
|
||||
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
|
||||
return -1;
|
||||
return lgetxattr(p, name, val, size);
|
||||
}
|
||||
|
||||
static int do_setxattr(int fd, int dirfd, const char *leaf,
|
||||
const char *name, const void *val, size_t size)
|
||||
{
|
||||
char p[MAXPATHLEN];
|
||||
|
||||
if (fd >= 0)
|
||||
return fsetxattr(fd, name, val, size, 0);
|
||||
#ifdef HAVE_XATTRAT_SYSCALLS
|
||||
{
|
||||
struct rsync_xattr_args args;
|
||||
int ret;
|
||||
args.value = (uint64_t)(uintptr_t)val;
|
||||
args.size = (uint32_t)size;
|
||||
args.flags = 0; /* replace */
|
||||
ret = syscall(SYS_setxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,
|
||||
name, &args, sizeof args);
|
||||
if (ret != -1 || errno != ENOSYS)
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
|
||||
return -1;
|
||||
return lsetxattr(p, name, val, size, 0);
|
||||
}
|
||||
|
||||
static int do_removexattr(int fd, int dirfd, const char *leaf, const char *name)
|
||||
{
|
||||
char p[MAXPATHLEN];
|
||||
|
||||
if (fd >= 0)
|
||||
return fremovexattr(fd, name);
|
||||
#ifdef HAVE_XATTRAT_SYSCALLS
|
||||
{
|
||||
int ret = syscall(SYS_removexattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW, name);
|
||||
if (ret != -1 || errno != ENOSYS)
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
|
||||
return -1;
|
||||
return lremovexattr(p, name);
|
||||
}
|
||||
|
||||
/* Read the whole named xattr into a malloc'd buffer, growing as needed. */
|
||||
static int read_full_xattr(int fd, int dirfd, const char *leaf,
|
||||
const char *name, unsigned char **buf_out, size_t *len_out)
|
||||
{
|
||||
unsigned char *buf = NULL;
|
||||
size_t size = 0;
|
||||
int tries;
|
||||
|
||||
for (tries = 0; tries < 8; tries++) {
|
||||
ssize_t n = do_getxattr(fd, dirfd, leaf, name, size ? buf : NULL, size);
|
||||
if (n >= 0) {
|
||||
if (size == 0) {
|
||||
/* First call just learned the length. */
|
||||
size = n ? (size_t)n : 1;
|
||||
buf = malloc(size);
|
||||
if (!buf)
|
||||
return -1;
|
||||
continue;
|
||||
}
|
||||
*buf_out = buf;
|
||||
*len_out = (size_t)n;
|
||||
return 0;
|
||||
}
|
||||
if (errno == ERANGE) { /* grew under us: re-probe the size */
|
||||
if (buf)
|
||||
free(buf);
|
||||
buf = NULL;
|
||||
size = 0;
|
||||
continue;
|
||||
}
|
||||
if (buf)
|
||||
free(buf);
|
||||
return -1; /* ENODATA / EOPNOTSUPP / ENOSYS / ... in errno */
|
||||
}
|
||||
if (buf)
|
||||
free(buf);
|
||||
errno = ERANGE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* === public API === */
|
||||
|
||||
static int acl_get_common(int fd, int dirfd, const char *leaf,
|
||||
int want_default, rsync_acl_ent **entries, int *count)
|
||||
{
|
||||
const char *name = want_default ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS;
|
||||
unsigned char *buf;
|
||||
size_t len;
|
||||
int rc;
|
||||
|
||||
*entries = NULL;
|
||||
*count = 0;
|
||||
|
||||
if (read_full_xattr(fd, dirfd, leaf, name, &buf, &len) < 0) {
|
||||
if (errno == ENODATA || errno == ENOATTR)
|
||||
return 0; /* no explicit ACL present */
|
||||
return -1; /* EOPNOTSUPP / ENOSYS / real error */
|
||||
}
|
||||
|
||||
rc = xattr_to_acl(buf, len, entries, count);
|
||||
free(buf);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int xacl_get_fd(int fd, int want_default, rsync_acl_ent **entries, int *count)
|
||||
{
|
||||
return acl_get_common(fd, -1, NULL, want_default, entries, count);
|
||||
}
|
||||
|
||||
int xacl_get_at(int dirfd, const char *leaf, int want_default,
|
||||
rsync_acl_ent **entries, int *count)
|
||||
{
|
||||
return acl_get_common(-1, dirfd, leaf, want_default, entries, count);
|
||||
}
|
||||
|
||||
static int acl_set_common(int fd, int dirfd, const char *leaf,
|
||||
int want_default, const rsync_acl_ent *ents, int count)
|
||||
{
|
||||
const char *name = want_default ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS;
|
||||
unsigned char *buf;
|
||||
size_t len;
|
||||
int rc, save_errno;
|
||||
|
||||
buf = acl_to_xattr(ents, count, &len);
|
||||
if (!buf) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
rc = do_setxattr(fd, dirfd, leaf, name, buf, len);
|
||||
save_errno = errno;
|
||||
free(buf);
|
||||
errno = save_errno;
|
||||
return rc < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
int xacl_set_fd(int fd, int want_default, const rsync_acl_ent *ents, int count)
|
||||
{
|
||||
return acl_set_common(fd, -1, NULL, want_default, ents, count);
|
||||
}
|
||||
|
||||
int xacl_set_at(int dirfd, const char *leaf, int want_default,
|
||||
const rsync_acl_ent *ents, int count)
|
||||
{
|
||||
return acl_set_common(-1, dirfd, leaf, want_default, ents, count);
|
||||
}
|
||||
|
||||
static int acl_del_default_common(int fd, int dirfd, const char *leaf)
|
||||
{
|
||||
if (do_removexattr(fd, dirfd, leaf, ACL_XATTR_DEFAULT) < 0) {
|
||||
if (errno == ENODATA || errno == ENOATTR)
|
||||
return 0; /* already absent: success, like acl_delete_def_file */
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int xacl_del_default_fd(int fd)
|
||||
{
|
||||
return acl_del_default_common(fd, -1, NULL);
|
||||
}
|
||||
|
||||
int xacl_del_default_at(int dirfd, const char *leaf)
|
||||
{
|
||||
return acl_del_default_common(-1, dirfd, leaf);
|
||||
}
|
||||
|
||||
/* True iff /proc/self/fd magic symlinks are usable, so the dirfd+leaf at-variants
|
||||
* work race-safely via the /proc compat on a pre-6.13 kernel. */
|
||||
static int proc_self_fd_usable(void)
|
||||
{
|
||||
int dfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
char p[64];
|
||||
int usable = 0;
|
||||
|
||||
if (dfd < 0)
|
||||
return 0;
|
||||
if (snprintf(p, sizeof p, "/proc/self/fd/%d/.", dfd) < (int)sizeof p) {
|
||||
/* The probe attr is absent; the path resolving (any errno but
|
||||
* ENOENT/ENOTDIR -- e.g. ENODATA/ENOTSUP/EACCES) means procfs gives us
|
||||
* the magic fd-symlink we need. */
|
||||
errno = 0;
|
||||
lgetxattr(p, "user.rsync_acl_probe", NULL, 0);
|
||||
usable = !(errno == ENOENT || errno == ENOTDIR);
|
||||
}
|
||||
close(dfd);
|
||||
return usable;
|
||||
}
|
||||
|
||||
int xacl_at_available(void)
|
||||
{
|
||||
static int avail = -1;
|
||||
|
||||
if (avail < 0) {
|
||||
#ifdef HAVE_XATTRAT_SYSCALLS
|
||||
/* Probe the *xattrat syscall directly (not via do_getxattr's /proc
|
||||
* fallback): any errno other than ENOSYS means it is present (6.13+). */
|
||||
struct rsync_xattr_args args;
|
||||
args.value = 0;
|
||||
args.size = 0;
|
||||
args.flags = 0;
|
||||
errno = 0;
|
||||
syscall(SYS_getxattrat, AT_FDCWD, ".", AT_SYMLINK_NOFOLLOW,
|
||||
"user.rsync_acl_probe", &args, sizeof args);
|
||||
if (errno != ENOSYS) {
|
||||
avail = 1;
|
||||
return avail;
|
||||
}
|
||||
#endif
|
||||
/* No *xattrat syscalls (pre-6.13, or a kernel built without them): the dirfd+leaf ACL ops
|
||||
* are still race-safe via /proc/self/fd if procfs is mounted, closing
|
||||
* the parent-symlink-race gap that otherwise forces the path-based set. */
|
||||
avail = proc_self_fd_usable();
|
||||
}
|
||||
return avail;
|
||||
}
|
||||
|
||||
#endif /* SUPPORT_ACL_FD */
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* POSIX ACL get/set/delete via the generic xattr syscalls, addressing the
|
||||
* kernel "system.posix_acl_{access,default}" attributes directly so that the
|
||||
* operation can be confined to a held O_NOFOLLOW fd (fsetxattr) or a
|
||||
* dirfd+leaf with AT_SYMLINK_NOFOLLOW (setxattrat). This replaces the path-
|
||||
* based libacl acl_*_file() calls on Linux, where those would re-resolve the
|
||||
* path and could be redirected by a parent-component symlink race.
|
||||
*
|
||||
* Copyright (C) 2026 Wayne Davison & the rsync project
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, visit the http://fsf.org website.
|
||||
*/
|
||||
|
||||
#ifdef SUPPORT_ACL_FD
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* A single logical POSIX ACL entry in host-native form. The tag values are
|
||||
* the stable kernel ABI numbers (== the libacl ACL_* constants), so they map
|
||||
* straight onto the on-disk e_tag without translation. */
|
||||
typedef struct {
|
||||
uint16_t tag; /* RACL_USER_OBJ / USER / GROUP_OBJ / GROUP / MASK / OTHER */
|
||||
uint16_t perm; /* permission bits: read=4, write=2, execute=1 */
|
||||
uint32_t id; /* uid/gid for USER/GROUP entries; RACL_UNDEFINED_ID otherwise */
|
||||
} rsync_acl_ent;
|
||||
|
||||
#define RACL_USER_OBJ 0x01
|
||||
#define RACL_USER 0x02
|
||||
#define RACL_GROUP_OBJ 0x04
|
||||
#define RACL_GROUP 0x08
|
||||
#define RACL_MASK 0x10
|
||||
#define RACL_OTHER 0x20
|
||||
|
||||
#define RACL_UNDEFINED_ID ((uint32_t)-1)
|
||||
|
||||
/* Read the access (want_default==0) or default (want_default!=0) ACL.
|
||||
*
|
||||
* On success returns 0 and sets *entries to a malloc()ed array of *count
|
||||
* entries (the caller frees it with free(); *entries may be NULL when
|
||||
* *count==0, which means "no explicit ACL present" -- e.g. ENODATA).
|
||||
*
|
||||
* On failure returns -1 with errno set. Callers distinguish:
|
||||
* ENOTSUP/EOPNOTSUPP - this filesystem has no ACL support (may differ per fs)
|
||||
* ENOSYS - the at-variant syscalls are unavailable on this kernel
|
||||
* The fd-variant operates on a held, already-NOFOLLOW-opened descriptor. The
|
||||
* at-variant resolves leaf relative to dirfd and never follows a leaf symlink. */
|
||||
int xacl_get_fd(int fd, int want_default, rsync_acl_ent **entries, int *count);
|
||||
int xacl_get_at(int dirfd, const char *leaf, int want_default, rsync_acl_ent **entries, int *count);
|
||||
|
||||
/* Write the given entries as the access/default ACL. The entries are emitted
|
||||
* in canonical order; the kernel validates them (a malformed set -> EINVAL). */
|
||||
int xacl_set_fd(int fd, int want_default, const rsync_acl_ent *entries, int count);
|
||||
int xacl_set_at(int dirfd, const char *leaf, int want_default, const rsync_acl_ent *entries, int count);
|
||||
|
||||
/* Delete a directory's default ACL. A missing default ACL is success. */
|
||||
int xacl_del_default_fd(int fd);
|
||||
int xacl_del_default_at(int dirfd, const char *leaf);
|
||||
|
||||
/* Cached runtime probe: are the *xattrat syscalls usable on this kernel?
|
||||
* Returns 0 when they are absent (so callers can fall back) or unbuilt. */
|
||||
int xacl_at_available(void);
|
||||
|
||||
#endif /* SUPPORT_ACL_FD */
|
||||
@@ -34,7 +34,9 @@
|
||||
#endif
|
||||
|
||||
.text
|
||||
.align 16
|
||||
/* .balign = N bytes everywhere; bare .align means 2^N on Mach-O (would ask
|
||||
* for 64KB alignment and trip a macOS linker warning). */
|
||||
.balign 16
|
||||
|
||||
.globl md5_process_asm
|
||||
md5_process_asm:
|
||||
|
||||
+2
-2
@@ -89,8 +89,8 @@ static void copy64(uint32 *M, const uchar *in)
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MD4_DIGEST_LEN; i++) {
|
||||
M[i] = (in[i*4+3] << 24) | (in[i*4+2] << 16)
|
||||
| (in[i*4+1] << 8) | (in[i*4+0] << 0);
|
||||
M[i] = ((uint32)in[i*4+3] << 24) | ((uint32)in[i*4+2] << 16)
|
||||
| ((uint32)in[i*4+1] << 8) | ((uint32)in[i*4+0] << 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+44
-1
@@ -44,6 +44,32 @@ struct align_test {
|
||||
#define PTR_ADD(b,o) ( (void*) ((char*)(b) + (o)) )
|
||||
#define PTR_SUB(b,o) ( (void*) ((char*)(b) - (o)) )
|
||||
|
||||
/* Under AddressSanitizer, fence each pool_alloc() chunk with a poisoned
|
||||
* redzone just below it (allocations grow downward from the top of an extent).
|
||||
* A bump allocator hands out chunks from one big malloc, so ASan cannot see a
|
||||
* write that underflows one chunk into its neighbour -- e.g. a miscomputed
|
||||
* F_SUM() reaching before a file_struct's extras. The redzone turns that into
|
||||
* a hard ASan report. We unpoison a whole extent whenever its space is reused
|
||||
* (reset/reclaim), so legitimate later allocations never trip over old
|
||||
* redzones; ASan unpoisons freed extents itself via free(). */
|
||||
#if defined(__SANITIZE_ADDRESS__)
|
||||
# define POOL_ASAN 1
|
||||
#elif defined(__has_feature)
|
||||
# if __has_feature(address_sanitizer)
|
||||
# define POOL_ASAN 1
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef POOL_ASAN
|
||||
# include <sanitizer/asan_interface.h>
|
||||
# define POOL_REDZONE 16 /* >= the largest pool-relative underflow we guard */
|
||||
# define POOL_POISON(p,n) ASAN_POISON_MEMORY_REGION((p), (n))
|
||||
# define POOL_UNPOISON(p,n) ASAN_UNPOISON_MEMORY_REGION((p), (n))
|
||||
#else
|
||||
# define POOL_POISON(p,n) ((void)0)
|
||||
# define POOL_UNPOISON(p,n) ((void)0)
|
||||
#endif
|
||||
|
||||
alloc_pool_t
|
||||
pool_create(size_t size, size_t quantum, void (*bomb)(const char*, const char*, int), int flags)
|
||||
{
|
||||
@@ -165,7 +191,18 @@ pool_alloc(alloc_pool_t p, size_t len, const char *bomb_msg)
|
||||
|
||||
pool->extents->free -= len;
|
||||
|
||||
return PTR_ADD(pool->extents->start, pool->extents->free);
|
||||
{
|
||||
void *ret = PTR_ADD(pool->extents->start, pool->extents->free);
|
||||
#ifdef POOL_ASAN
|
||||
size_t rz = pool->extents->free < POOL_REDZONE
|
||||
? pool->extents->free : POOL_REDZONE;
|
||||
if (rz) {
|
||||
pool->extents->free -= rz;
|
||||
POOL_POISON(PTR_ADD(pool->extents->start, pool->extents->free), rz);
|
||||
}
|
||||
#endif
|
||||
return ret;
|
||||
}
|
||||
|
||||
bomb_out:
|
||||
if (pool->bomb)
|
||||
@@ -215,6 +252,10 @@ pool_free(alloc_pool_t p, size_t len, void *addr)
|
||||
if (!cur)
|
||||
return;
|
||||
|
||||
/* This extent's space may be reused (and POOL_CLEAR may memset it)
|
||||
* below, so drop any redzones in it first. */
|
||||
POOL_UNPOISON(cur->start, pool->size);
|
||||
|
||||
if (!prev) {
|
||||
/* The "live" extent is kept ready for more allocations. */
|
||||
if (cur->free + cur->bound + len >= pool->size) {
|
||||
@@ -272,6 +313,8 @@ pool_free_old(alloc_pool_t p, void *addr)
|
||||
if (!cur)
|
||||
return;
|
||||
|
||||
POOL_UNPOISON(cur->start, pool->size);
|
||||
|
||||
if (addr == PTR_ADD(cur->start, cur->free)) {
|
||||
if (prev) {
|
||||
prev->next = NULL;
|
||||
|
||||
+151
-7
@@ -180,6 +180,26 @@ int sys_acl_free_acl(SMB_ACL_T the_acl)
|
||||
return acl_free(the_acl);
|
||||
}
|
||||
|
||||
#ifdef HAVE_LIBACL_AT
|
||||
/* Dirfd/AT-flag ACL ops via the new libacl,
|
||||
* race-safe on every Linux kernel. at_flags is AT_SYMLINK_NOFOLLOW (dirfd+leaf)
|
||||
* or AT_EMPTY_PATH (operate on an open fd passed as dirfd, path ""). */
|
||||
SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type)
|
||||
{
|
||||
return acl_get_file_at(dirfd, path_p, at_flags, type);
|
||||
}
|
||||
|
||||
int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl)
|
||||
{
|
||||
return acl_set_file_at(dirfd, path_p, at_flags, type, theacl);
|
||||
}
|
||||
|
||||
int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags)
|
||||
{
|
||||
return acl_delete_def_file_at(dirfd, path_p, at_flags);
|
||||
}
|
||||
#endif /* HAVE_LIBACL_AT */
|
||||
|
||||
#elif defined(HAVE_TRU64_ACLS) /*--------------------------------------------*/
|
||||
/*
|
||||
* The interface to DEC/Compaq Tru64 UNIX ACLs
|
||||
@@ -479,12 +499,20 @@ SMB_ACL_T sys_acl_get_file(const char *path_p, SMB_ACL_TYPE_T type)
|
||||
return acl_d;
|
||||
}
|
||||
|
||||
#if 0
|
||||
SMB_ACL_T sys_acl_get_fd(int fd)
|
||||
#ifdef HAVE_SOLARIS_ACLS
|
||||
/* facl(2)-based ACL read on a held fd (no path re-resolution). Solaris stores
|
||||
* the access and default ACLs as one combined ACL; split out the requested half. */
|
||||
SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type)
|
||||
{
|
||||
SMB_ACL_T acl_d;
|
||||
int count; /* # of ACL entries allocated */
|
||||
int naccess; /* # of access ACL entries */
|
||||
int ndefault; /* # of default ACL entries */
|
||||
|
||||
if (type != SMB_ACL_TYPE_ACCESS && type != SMB_ACL_TYPE_DEFAULT) {
|
||||
errno = EINVAL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
count = INITIAL_ACL_SIZE;
|
||||
if ((acl_d = sys_acl_init(count)) == NULL) {
|
||||
@@ -511,17 +539,39 @@ SMB_ACL_T sys_acl_get_fd(int fd)
|
||||
}
|
||||
|
||||
/*
|
||||
* calculate the number of access ACL entries
|
||||
* calculate the number of access and default ACL entries
|
||||
*/
|
||||
for (naccess = 0; naccess < count; naccess++) {
|
||||
if (acl_d->acl[naccess].a_type & ACL_DEFAULT)
|
||||
break;
|
||||
}
|
||||
ndefault = count - naccess;
|
||||
|
||||
acl_d->count = naccess;
|
||||
if (type == SMB_ACL_TYPE_DEFAULT) {
|
||||
int i, j;
|
||||
|
||||
/*
|
||||
* Default ACL entries follow the access entries in the combined
|
||||
* Solaris ACL; move them to the front of the wrapper and clear
|
||||
* ACL_DEFAULT so the caller sees a plain default ACL.
|
||||
*/
|
||||
for (i = 0, j = naccess; i < ndefault; i++, j++) {
|
||||
acl_d->acl[i] = acl_d->acl[j];
|
||||
acl_d->acl[i].a_type &= ~ACL_DEFAULT;
|
||||
}
|
||||
|
||||
acl_d->count = ndefault;
|
||||
} else {
|
||||
acl_d->count = naccess;
|
||||
}
|
||||
|
||||
return acl_d;
|
||||
}
|
||||
|
||||
SMB_ACL_T sys_acl_get_fd(int fd)
|
||||
{
|
||||
return sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
int sys_acl_get_info(SMB_ACL_ENTRY_T entry, SMB_ACL_TAG_T *tag_type_p, uint32 *bits_p, id_t *u_g_id_p)
|
||||
@@ -728,14 +778,108 @@ int sys_acl_set_file(const char *name, SMB_ACL_TYPE_T type, SMB_ACL_T acl_d)
|
||||
return ret;
|
||||
}
|
||||
|
||||
#if 0
|
||||
int sys_acl_set_fd(int fd, SMB_ACL_T acl_d)
|
||||
#ifdef HAVE_SOLARIS_ACLS
|
||||
/* facl(2)-based ACL write on a held fd (no path re-resolution). Setting an ACL
|
||||
* on a directory replaces the combined access+default set, so for a dir read the
|
||||
* other half through the fd, merge, and write the combined ACL back. Mirrors the
|
||||
* path-based sys_acl_set_file() below. */
|
||||
int sys_acl_set_fd_type(int fd, SMB_ACL_TYPE_T type, SMB_ACL_T acl_d)
|
||||
{
|
||||
struct stat s;
|
||||
struct acl *acl_p;
|
||||
int acl_count;
|
||||
struct acl *acl_buf = NULL;
|
||||
int ret;
|
||||
|
||||
if (type != SMB_ACL_TYPE_ACCESS && type != SMB_ACL_TYPE_DEFAULT) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (acl_sort(acl_d) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return facl(fd, SETACL, acl_d->count, &acl_d->acl[0]);
|
||||
acl_p = &acl_d->acl[0];
|
||||
acl_count = acl_d->count;
|
||||
|
||||
if (fstat(fd, &s) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (S_ISDIR(s.st_mode)) {
|
||||
SMB_ACL_T acc_acl;
|
||||
SMB_ACL_T def_acl;
|
||||
SMB_ACL_T tmp_acl;
|
||||
int i;
|
||||
|
||||
if (type == SMB_ACL_TYPE_ACCESS) {
|
||||
acc_acl = acl_d;
|
||||
def_acl = tmp_acl = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_DEFAULT);
|
||||
} else {
|
||||
def_acl = acl_d;
|
||||
acc_acl = tmp_acl = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS);
|
||||
}
|
||||
|
||||
if (tmp_acl == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
acl_count = acc_acl->count + def_acl->count;
|
||||
acl_p = acl_buf = SMB_MALLOC_ARRAY(struct acl, acl_count);
|
||||
|
||||
if (acl_buf == NULL) {
|
||||
sys_acl_free_acl(tmp_acl);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Concatenate access + default, then mark the default half. */
|
||||
memcpy(&acl_buf[0], &acc_acl->acl[0],
|
||||
acc_acl->count * sizeof acl_buf[0]);
|
||||
memcpy(&acl_buf[acc_acl->count], &def_acl->acl[0],
|
||||
def_acl->count * sizeof acl_buf[0]);
|
||||
|
||||
for (i = acc_acl->count; i < acl_count; i++) {
|
||||
acl_buf[i].a_type |= ACL_DEFAULT;
|
||||
}
|
||||
|
||||
sys_acl_free_acl(tmp_acl);
|
||||
|
||||
} else if (type != SMB_ACL_TYPE_ACCESS) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = facl(fd, SETACL, acl_count, acl_p);
|
||||
|
||||
SAFE_FREE(acl_buf);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sys_acl_set_fd(int fd, SMB_ACL_T acl_d)
|
||||
{
|
||||
return sys_acl_set_fd_type(fd, SMB_ACL_TYPE_ACCESS, acl_d);
|
||||
}
|
||||
|
||||
int sys_acl_delete_def_fd(int fd)
|
||||
{
|
||||
SMB_ACL_T acl_d;
|
||||
int ret;
|
||||
|
||||
/*
|
||||
* Fetching the access ACL through the fd and rewriting it deletes the
|
||||
* default ACL, without re-resolving the path.
|
||||
*/
|
||||
if ((acl_d = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS)) == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = facl(fd, SETACL, acl_d->count, acl_d->acl);
|
||||
|
||||
sys_acl_free_acl(acl_d);
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -301,7 +301,18 @@ int sys_acl_valid(SMB_ACL_T theacl);
|
||||
int sys_acl_set_file(const char *name, SMB_ACL_TYPE_T acltype, SMB_ACL_T theacl);
|
||||
int sys_acl_set_fd(int fd, SMB_ACL_T theacl);
|
||||
int sys_acl_delete_def_file(const char *name);
|
||||
#ifdef HAVE_SOLARIS_ACLS
|
||||
SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type);
|
||||
int sys_acl_set_fd_type(int fd, SMB_ACL_TYPE_T type, SMB_ACL_T theacl);
|
||||
int sys_acl_delete_def_fd(int fd);
|
||||
#endif
|
||||
int sys_acl_free_acl(SMB_ACL_T the_acl);
|
||||
int no_acl_syscall_error(int err);
|
||||
|
||||
#ifdef HAVE_LIBACL_AT
|
||||
SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type);
|
||||
int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl);
|
||||
int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags);
|
||||
#endif
|
||||
|
||||
#endif /* SUPPORT_ACLS */
|
||||
+145
-29
@@ -45,16 +45,31 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t
|
||||
return lsetxattr(path, name, value, size, 0);
|
||||
}
|
||||
|
||||
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)
|
||||
{
|
||||
return fsetxattr(filedes, name, value, size, 0);
|
||||
}
|
||||
|
||||
int sys_lremovexattr(const char *path, const char *name)
|
||||
{
|
||||
return lremovexattr(path, name);
|
||||
}
|
||||
|
||||
int sys_fremovexattr(int filedes, const char *name)
|
||||
{
|
||||
return fremovexattr(filedes, name);
|
||||
}
|
||||
|
||||
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
|
||||
{
|
||||
return llistxattr(path, list, size);
|
||||
}
|
||||
|
||||
ssize_t sys_flistxattr(int filedes, char *list, size_t size)
|
||||
{
|
||||
return flistxattr(filedes, list, size);
|
||||
}
|
||||
|
||||
#elif HAVE_OSX_XATTRS
|
||||
|
||||
ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size)
|
||||
@@ -89,16 +104,31 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t
|
||||
return setxattr(path, name, value, size, 0, XATTR_NOFOLLOW);
|
||||
}
|
||||
|
||||
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)
|
||||
{
|
||||
return fsetxattr(filedes, name, value, size, 0, 0);
|
||||
}
|
||||
|
||||
int sys_lremovexattr(const char *path, const char *name)
|
||||
{
|
||||
return removexattr(path, name, XATTR_NOFOLLOW);
|
||||
}
|
||||
|
||||
int sys_fremovexattr(int filedes, const char *name)
|
||||
{
|
||||
return fremovexattr(filedes, name, 0);
|
||||
}
|
||||
|
||||
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
|
||||
{
|
||||
return listxattr(path, list, size, XATTR_NOFOLLOW);
|
||||
}
|
||||
|
||||
ssize_t sys_flistxattr(int filedes, char *list, size_t size)
|
||||
{
|
||||
return flistxattr(filedes, list, size, 0);
|
||||
}
|
||||
|
||||
#elif HAVE_FREEBSD_XATTRS
|
||||
|
||||
ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size)
|
||||
@@ -116,32 +146,42 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t
|
||||
return extattr_set_link(path, EXTATTR_NAMESPACE_USER, name, value, size);
|
||||
}
|
||||
|
||||
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)
|
||||
{
|
||||
return extattr_set_fd(filedes, EXTATTR_NAMESPACE_USER, name, value, size);
|
||||
}
|
||||
|
||||
int sys_lremovexattr(const char *path, const char *name)
|
||||
{
|
||||
return extattr_delete_link(path, EXTATTR_NAMESPACE_USER, name);
|
||||
}
|
||||
|
||||
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
|
||||
int sys_fremovexattr(int filedes, const char *name)
|
||||
{
|
||||
return extattr_delete_fd(filedes, EXTATTR_NAMESPACE_USER, name);
|
||||
}
|
||||
|
||||
/* Turn the FreeBSD extattr_list_xx() output (a single length byte before each
|
||||
* name, no '\0' terminator) into the series of null-terminated strings that the
|
||||
* rest of rsync expects. Since the size is unchanged, transform in place.
|
||||
* Shared by the path and fd list variants. */
|
||||
static ssize_t freebsd_list_finish(char *list, size_t size, ssize_t len)
|
||||
{
|
||||
unsigned char keylen;
|
||||
ssize_t off, len = extattr_list_link(path, EXTATTR_NAMESPACE_USER, list, size);
|
||||
ssize_t off;
|
||||
|
||||
if (len <= 0 || size == 0)
|
||||
return len;
|
||||
|
||||
if ((size_t)len >= size) {
|
||||
/* FreeBSD extattr_list_xx() returns 'size' as 'len' in case there are
|
||||
more data available, truncating the output, we solve this by signalling
|
||||
ERANGE in case len == size so that the code in xattrs.c will retry with
|
||||
a bigger buffer */
|
||||
/* FreeBSD extattr_list_xx() returns 'size' as 'len' in case there are
|
||||
more data available, truncating the output, we solve this by signalling
|
||||
ERANGE in case len == size so that the code in xattrs.c will retry with
|
||||
a bigger buffer */
|
||||
errno = ERANGE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* FreeBSD puts a single-byte length before each string, with no '\0'
|
||||
* terminator. We need to change this into a series of null-terminted
|
||||
* strings. Since the size is the same, we can simply transform the
|
||||
* output in place. */
|
||||
for (off = 0; off < len; off += keylen + 1) {
|
||||
keylen = ((unsigned char*)list)[off];
|
||||
if (off + keylen >= len) {
|
||||
@@ -156,6 +196,18 @@ ssize_t sys_llistxattr(const char *path, char *list, size_t size)
|
||||
return len;
|
||||
}
|
||||
|
||||
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
|
||||
{
|
||||
return freebsd_list_finish(list, size,
|
||||
extattr_list_link(path, EXTATTR_NAMESPACE_USER, list, size));
|
||||
}
|
||||
|
||||
ssize_t sys_flistxattr(int filedes, char *list, size_t size)
|
||||
{
|
||||
return freebsd_list_finish(list, size,
|
||||
extattr_list_fd(filedes, EXTATTR_NAMESPACE_USER, list, size));
|
||||
}
|
||||
|
||||
#elif HAVE_SOLARIS_XATTRS
|
||||
|
||||
static ssize_t read_xattr(int attrfd, void *buf, size_t buflen)
|
||||
@@ -217,29 +269,59 @@ ssize_t sys_fgetxattr(int filedes, const char *name, void *value, size_t size)
|
||||
return read_xattr(attrfd, value, size);
|
||||
}
|
||||
|
||||
int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size)
|
||||
/* Write a datum to the already-opened attribute fd, closing it. Shared by the
|
||||
* path- and fd-keyed setters below. */
|
||||
static int write_xattr(int attrfd, const void *value, size_t size)
|
||||
{
|
||||
int attrfd;
|
||||
size_t bufpos;
|
||||
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
|
||||
|
||||
if ((attrfd = attropen(path, name, O_CREAT|O_TRUNC|O_WRONLY, mode)) < 0)
|
||||
return -1;
|
||||
int ret = 0, saved_errno = 0;
|
||||
|
||||
for (bufpos = 0; bufpos < size; ) {
|
||||
ssize_t cnt = write(attrfd, (char*)value + bufpos, size);
|
||||
if (cnt <= 0) {
|
||||
if (cnt < 0 && errno == EINTR)
|
||||
ssize_t cnt = write(attrfd, (const char *)value + bufpos, size - bufpos);
|
||||
if (cnt < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
bufpos = -1;
|
||||
ret = -1;
|
||||
saved_errno = errno;
|
||||
break;
|
||||
}
|
||||
if (cnt == 0) {
|
||||
ret = -1;
|
||||
saved_errno = EIO;
|
||||
break;
|
||||
}
|
||||
bufpos += cnt;
|
||||
}
|
||||
|
||||
close(attrfd);
|
||||
/* Don't let close() clobber the write error; do report a close() failure. */
|
||||
if (close(attrfd) < 0 && ret == 0)
|
||||
return -1;
|
||||
if (ret < 0 && saved_errno)
|
||||
errno = saved_errno;
|
||||
|
||||
return bufpos > 0 ? 0 : -1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size)
|
||||
{
|
||||
int attrfd;
|
||||
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
|
||||
|
||||
if ((attrfd = attropen(path, name, O_CREAT|O_TRUNC|O_WRONLY, mode)) < 0)
|
||||
return -1;
|
||||
|
||||
return write_xattr(attrfd, value, size);
|
||||
}
|
||||
|
||||
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)
|
||||
{
|
||||
int attrfd;
|
||||
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
|
||||
|
||||
if ((attrfd = openat(filedes, name, O_CREAT|O_TRUNC|O_WRONLY|O_XATTR, mode)) < 0)
|
||||
return -1;
|
||||
|
||||
return write_xattr(attrfd, value, size);
|
||||
}
|
||||
|
||||
int sys_lremovexattr(const char *path, const char *name)
|
||||
@@ -257,18 +339,29 @@ int sys_lremovexattr(const char *path, const char *name)
|
||||
return ret;
|
||||
}
|
||||
|
||||
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
|
||||
int sys_fremovexattr(int filedes, const char *name)
|
||||
{
|
||||
int attrdirfd;
|
||||
int ret;
|
||||
|
||||
if ((attrdirfd = openat(filedes, ".", O_RDONLY|O_XATTR, 0)) < 0)
|
||||
return -1;
|
||||
|
||||
ret = unlinkat(attrdirfd, name, 0);
|
||||
|
||||
close(attrdirfd);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* List the names in an already-opened attribute-dir fd, consuming it. Shared
|
||||
* by the path- and fd-keyed listers below. */
|
||||
static ssize_t list_xattr(int attrdirfd, char *list, size_t size)
|
||||
{
|
||||
DIR *dirp;
|
||||
struct dirent *dp;
|
||||
ssize_t ret = 0;
|
||||
|
||||
if ((attrdirfd = attropen(path, ".", O_RDONLY)) < 0) {
|
||||
errno = ENOTSUP;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ((dirp = fdopendir(attrdirfd)) == NULL) {
|
||||
close(attrdirfd);
|
||||
return -1;
|
||||
@@ -296,11 +389,34 @@ ssize_t sys_llistxattr(const char *path, char *list, size_t size)
|
||||
}
|
||||
|
||||
closedir(dirp);
|
||||
close(attrdirfd);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
|
||||
{
|
||||
int attrdirfd;
|
||||
|
||||
if ((attrdirfd = attropen(path, ".", O_RDONLY)) < 0) {
|
||||
errno = ENOTSUP;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return list_xattr(attrdirfd, list, size);
|
||||
}
|
||||
|
||||
ssize_t sys_flistxattr(int filedes, char *list, size_t size)
|
||||
{
|
||||
int attrdirfd;
|
||||
|
||||
if ((attrdirfd = openat(filedes, ".", O_RDONLY|O_XATTR, 0)) < 0) {
|
||||
errno = ENOTSUP;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return list_xattr(attrdirfd, list, size);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#error You need to create xattr compatibility functions.
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size);
|
||||
ssize_t sys_fgetxattr(int filedes, const char *name, void *value, size_t size);
|
||||
int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size);
|
||||
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size);
|
||||
int sys_lremovexattr(const char *path, const char *name);
|
||||
int sys_fremovexattr(int filedes, const char *name);
|
||||
ssize_t sys_llistxattr(const char *path, char *list, size_t size);
|
||||
ssize_t sys_flistxattr(int filedes, char *list, size_t size);
|
||||
|
||||
#else
|
||||
|
||||
|
||||
+15
-2
@@ -89,6 +89,11 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)
|
||||
p_ch = *++p;
|
||||
/* FALLTHROUGH */
|
||||
default:
|
||||
/* iwildmatch() folds the text to lower case above; fold the pattern
|
||||
* char too so matching is truly case-insensitive (not just text-side).
|
||||
* Without this an upper-case "hosts deny" token fails OPEN. */
|
||||
if (force_lower_case && ISUPPER(p_ch))
|
||||
p_ch = tolower(p_ch);
|
||||
if (t_ch != p_ch)
|
||||
return FALSE;
|
||||
continue;
|
||||
@@ -150,6 +155,8 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)
|
||||
p_ch = *++p;
|
||||
if (!p_ch)
|
||||
return ABORT_ALL;
|
||||
if (force_lower_case && ISUPPER(p_ch))
|
||||
p_ch = tolower(p_ch);
|
||||
if (t_ch == p_ch)
|
||||
matched = TRUE;
|
||||
} else if (p_ch == '-' && prev_ch && p[1] && p[1] != ']') {
|
||||
@@ -159,6 +166,8 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)
|
||||
if (!p_ch)
|
||||
return ABORT_ALL;
|
||||
}
|
||||
if (force_lower_case && ISUPPER(p_ch))
|
||||
p_ch = tolower(p_ch);
|
||||
if (t_ch <= p_ch && t_ch >= prev_ch)
|
||||
matched = TRUE;
|
||||
p_ch = 0; /* This makes "prev_ch" get set to 0. */
|
||||
@@ -216,8 +225,12 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)
|
||||
} else /* malformed [:class:] string */
|
||||
return ABORT_ALL;
|
||||
p_ch = 0; /* This makes "prev_ch" get set to 0. */
|
||||
} else if (t_ch == p_ch)
|
||||
matched = TRUE;
|
||||
} else {
|
||||
if (force_lower_case && ISUPPER(p_ch))
|
||||
p_ch = tolower(p_ch);
|
||||
if (t_ch == p_ch)
|
||||
matched = TRUE;
|
||||
}
|
||||
} while (prev_ch = p_ch, (p_ch = *++p) != ']');
|
||||
if (matched == special || t_ch == '/')
|
||||
return FALSE;
|
||||
|
||||
+124
-5
@@ -164,11 +164,81 @@ static const struct enum_list enum_syslog_facility[] = {
|
||||
|
||||
/* Expand %VAR% references. Any unknown vars or unrecognized
|
||||
* syntax leaves the raw chars unchanged. */
|
||||
static char *expand_vars(const char *str)
|
||||
enum shell_quote_context {
|
||||
SHELL_UNQUOTED,
|
||||
SHELL_SINGLE_QUOTED,
|
||||
SHELL_DOUBLE_QUOTED
|
||||
};
|
||||
|
||||
/* Characters that can turn a substituted value into shell syntax rather than
|
||||
* data, in any quoting context. Quoting alone cannot be relied on here:
|
||||
* context-aware escaping is correct for exactly one level of shell parsing,
|
||||
* and a hook such as `sh -c '... %RSYNC_USER_NAME% ...'` re-parses the word in
|
||||
* a second shell that sees the value bare. Peer-supplied values carrying any
|
||||
* of these are refused instead. */
|
||||
static int shell_unsafe_value(const char *val)
|
||||
{
|
||||
const char *s;
|
||||
|
||||
for (s = val; *s; s++) {
|
||||
/* '!' negates in command position (a hook `sh -c '%VAR% false'`
|
||||
* becomes `! false` and reports success, inverting an access
|
||||
* check); '~' is tilde-expanded; '{' and '}' brace-expand in
|
||||
* bash and zsh. None of them execute anything on their own,
|
||||
* which is why a set built from the obvious metacharacters
|
||||
* missed them. */
|
||||
if (strchr("'\"`$\\;&|<>()*?[]# !~{}", *s)
|
||||
|| (unsigned char)*s < 0x20 || (unsigned char)*s == 0x7f)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *expand_vars_shell_escape(const char *val, int quote_context)
|
||||
{
|
||||
const char *s;
|
||||
char *ret, *t;
|
||||
/* A double-quoted value is deliberately BOTH backslash-escaped and
|
||||
* wrapped in single quotes. The wrap is redundant for one level of
|
||||
* shell parsing (and shows up as literal quotes in the value), but a
|
||||
* hook such as `sh -c "... %RSYNC_USER_NAME% ..."` re-parses the word
|
||||
* in a second shell, where the backslashes are already gone and only
|
||||
* the quotes still protect it. */
|
||||
size_t len = quote_context == SHELL_SINGLE_QUOTED ? 0 : 2;
|
||||
|
||||
for (s = val; *s; s++) {
|
||||
if (quote_context == SHELL_DOUBLE_QUOTED
|
||||
&& strchr("\\\"`$", *s))
|
||||
len += 2;
|
||||
else
|
||||
len += *s == '\'' ? 4 : 1;
|
||||
}
|
||||
ret = new_array(char, len + 1);
|
||||
t = ret;
|
||||
if (quote_context != SHELL_SINGLE_QUOTED)
|
||||
*t++ = '\'';
|
||||
for (s = val; *s; s++) {
|
||||
if (quote_context == SHELL_DOUBLE_QUOTED
|
||||
&& strchr("\\\"`$", *s)) {
|
||||
*t++ = '\\';
|
||||
*t++ = *s;
|
||||
} else if (*s == '\'') {
|
||||
memcpy(t, "'\\''", 4);
|
||||
t += 4;
|
||||
} else
|
||||
*t++ = *s;
|
||||
}
|
||||
if (quote_context != SHELL_SINGLE_QUOTED)
|
||||
*t++ = '\'';
|
||||
*t = '\0';
|
||||
return ret;
|
||||
}
|
||||
|
||||
static char *expand_vars(const char *str, int shell_escape)
|
||||
{
|
||||
char *buf, *t;
|
||||
const char *f;
|
||||
int bufsize;
|
||||
int bufsize, quote_context = SHELL_UNQUOTED, escaped_char = 0;
|
||||
|
||||
if (!str || !strchr(str, '%'))
|
||||
return (char *)str; /* TODO change return value to const char* at some point. */
|
||||
@@ -184,7 +254,29 @@ static char *expand_vars(const char *str)
|
||||
strlcpy(t, f+1, percent - f);
|
||||
val = getenv(t);
|
||||
if (val) {
|
||||
int len = strlcpy(t, val, bufsize+1);
|
||||
char *escaped = NULL;
|
||||
int len;
|
||||
/* %RSYNC_*% values originate from the peer request/args.
|
||||
* When the result is fed to a shell-executed hook, escape it
|
||||
* for the template's current shell quote context so a value
|
||||
* containing shell metacharacters can't inject. For ordinary string
|
||||
* params (path, uid, gid, ...) leave them verbatim --
|
||||
* quoting there would corrupt the value (e.g. a documented
|
||||
* `path = /home/%RSYNC_USER_NAME%` would become /home/'x'). */
|
||||
if (shell_escape && strncmp(t, "RSYNC_", 6) == 0) {
|
||||
if (shell_unsafe_value(val)) {
|
||||
/* Fail closed: the hook may be an access
|
||||
* check, so skipping it is not an option. */
|
||||
rprintf(FLOG,
|
||||
"refusing to run shell hook: %%%s%% holds a shell metacharacter\n",
|
||||
t);
|
||||
exit_cleanup(RERR_UNSUPPORTED);
|
||||
}
|
||||
val = escaped = expand_vars_shell_escape(val, quote_context);
|
||||
}
|
||||
len = strlcpy(t, val, bufsize+1);
|
||||
if (escaped)
|
||||
free(escaped);
|
||||
if (len > bufsize)
|
||||
break;
|
||||
bufsize -= len;
|
||||
@@ -194,6 +286,28 @@ static char *expand_vars(const char *str)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shell_escape) {
|
||||
if (quote_context == SHELL_SINGLE_QUOTED) {
|
||||
/* Nothing is special inside '...', not even a backslash;
|
||||
* only the closing quote ends it. */
|
||||
if (*f == '\'')
|
||||
quote_context = SHELL_UNQUOTED;
|
||||
} else if (escaped_char)
|
||||
escaped_char = 0;
|
||||
else if (*f == '\\')
|
||||
escaped_char = 1;
|
||||
else if (quote_context == SHELL_DOUBLE_QUOTED) {
|
||||
/* A single quote inside "..." is literal and must not be
|
||||
* taken as opening a single-quoted run -- doing so would
|
||||
* de-sync the tracker and escape a later value for the
|
||||
* wrong context. */
|
||||
if (*f == '"')
|
||||
quote_context = SHELL_UNQUOTED;
|
||||
} else if (*f == '\'')
|
||||
quote_context = SHELL_SINGLE_QUOTED;
|
||||
else if (*f == '"')
|
||||
quote_context = SHELL_DOUBLE_QUOTED;
|
||||
}
|
||||
*t++ = *f++;
|
||||
bufsize--;
|
||||
}
|
||||
@@ -213,7 +327,10 @@ static char *expand_vars(const char *str)
|
||||
/* Each "char* foo" has an associated "BOOL foo_EXP" that tracks if the string has been expanded yet or not. */
|
||||
|
||||
/* NOTE: use this function and all the FN_{GLOBAL,LOCAL} ones WITHOUT a trailing semicolon! */
|
||||
#define RETURN_EXPANDED(val) {if (!val ## _EXP) {val = expand_vars(val); val ## _EXP = True;} return val ? val : "";}
|
||||
#define RETURN_EXPANDED(val) {if (!val ## _EXP) {val = expand_vars(val, 0); val ## _EXP = True;} return val ? val : "";}
|
||||
/* Variant for params whose expansion is fed to a shell-executed hook: quote
|
||||
* %RSYNC_*% peer-controlled values to prevent shell injection. */
|
||||
#define RETURN_EXPANDED_SHELL(val) {if (!val ## _EXP) {val = expand_vars(val, 1); val ## _EXP = True;} return val ? val : "";}
|
||||
|
||||
/* In this section all the functions that are used to access the
|
||||
* parameters from the rest of the program are defined. */
|
||||
@@ -229,6 +346,8 @@ static char *expand_vars(const char *str)
|
||||
|
||||
#define FN_LOCAL_STRING(fn_name, val) \
|
||||
char *fn_name(int i) {if (LP_SNUM_OK(i) && iSECTION(i).val) RETURN_EXPANDED(iSECTION(i).val) else RETURN_EXPANDED(Vars.l.val)}
|
||||
#define FN_LOCAL_STRING_SHELL(fn_name, val) \
|
||||
char *fn_name(int i) {if (LP_SNUM_OK(i) && iSECTION(i).val) RETURN_EXPANDED_SHELL(iSECTION(i).val) else RETURN_EXPANDED_SHELL(Vars.l.val)}
|
||||
#define FN_LOCAL_BOOL(fn_name, val) \
|
||||
BOOL fn_name(int i) {return LP_SNUM_OK(i)? iSECTION(i).val : Vars.l.val;}
|
||||
#define FN_LOCAL_CHAR(fn_name, val) \
|
||||
@@ -410,7 +529,7 @@ static BOOL do_parameter(char *parmname, char *parmvalue)
|
||||
break;
|
||||
default:
|
||||
/* expand any %VAR% strings now */
|
||||
parmvalue = expand_vars(parmvalue);
|
||||
parmvalue = expand_vars(parmvalue, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "rsync.h"
|
||||
#include "itypes.h"
|
||||
#include "inums.h"
|
||||
#include "rounding.h" /* EXTRA_ROUNDING, so log_delete() aligns its file_struct */
|
||||
|
||||
extern int dry_run;
|
||||
extern int am_daemon;
|
||||
@@ -119,12 +120,20 @@ static char const *rerr_name(int code)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, int escape_c1, char end_char);
|
||||
|
||||
static void logit(int priority, const char *buf)
|
||||
{
|
||||
if (logfile_was_closed)
|
||||
logfile_reopen();
|
||||
if (logfile_fp) {
|
||||
fprintf(logfile_fp, "%s [%d] %s", timestring(time(NULL)), (int)getpid(), buf);
|
||||
/* Escape control chars in the message so an attacker-controlled
|
||||
* filename can't inject terminal escapes into the log an admin later
|
||||
* cat's (CWE-117); keep the trailing newline raw via end_char. */
|
||||
int len = strlen(buf);
|
||||
char trailing = len && (buf[len-1] == '\n' || buf[len-1] == '\r') ? buf[--len] : '\0';
|
||||
fprintf(logfile_fp, "%s [%d] ", timestring(time(NULL)), (int)getpid());
|
||||
filtered_fwrite(logfile_fp, buf, len, 0, 1, trailing);
|
||||
fflush(logfile_fp);
|
||||
} else {
|
||||
syslog(priority, "%s", buf);
|
||||
@@ -153,7 +162,15 @@ static void syslog_init()
|
||||
static void logfile_open(void)
|
||||
{
|
||||
mode_t old_umask = umask(022 | orig_umask);
|
||||
logfile_fp = fopen(logfile_name, "a");
|
||||
/* --log-file/`log file =` are operator-supplied paths that may transit
|
||||
* attacker-writable dirs; a planted symlink could redirect root's log
|
||||
* into e.g. /root/.ssh/authorized_keys. Refuse symlinks not owned by
|
||||
* uid 0 or our euid. */
|
||||
int fd = open_no_attacker_symlinks(logfile_name,
|
||||
O_WRONLY | O_APPEND | O_CREAT, 0644);
|
||||
logfile_fp = fd >= 0 ? fdopen(fd, "a") : NULL;
|
||||
if (!logfile_fp && fd >= 0)
|
||||
close(fd);
|
||||
umask(old_umask);
|
||||
if (!logfile_fp) {
|
||||
int fopen_errno = errno;
|
||||
@@ -222,7 +239,7 @@ void logfile_reopen(void)
|
||||
}
|
||||
}
|
||||
|
||||
static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, char end_char)
|
||||
static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, int escape_c1, char end_char)
|
||||
{
|
||||
char outbuf[1024], *ob = outbuf;
|
||||
const char *end = in_buf + in_len;
|
||||
@@ -234,7 +251,8 @@ static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isp
|
||||
}
|
||||
if ((in_buf < end - 4 && *in_buf == '\\' && in_buf[1] == '#'
|
||||
&& isDigit(in_buf + 2) && isDigit(in_buf + 3) && isDigit(in_buf + 4))
|
||||
|| (*in_buf != '\t' && ((use_isprint && !isPrint(in_buf)) || *(uchar*)in_buf < ' ')))
|
||||
|| (*in_buf != '\t' && ((use_isprint && !isPrint(in_buf)) || *(uchar*)in_buf < ' '
|
||||
|| (escape_c1 && *(uchar*)in_buf >= 0x80 && *(uchar*)in_buf <= 0x9f))))
|
||||
ob += snprintf(ob, 6, "\\#%03o", *(uchar*)in_buf++);
|
||||
else
|
||||
*ob++ = *in_buf++;
|
||||
@@ -272,8 +290,12 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)
|
||||
if (am_daemon > 0 && code != FCLIENT)
|
||||
code = FLOG;
|
||||
} else if (send_msgs_to_gen) {
|
||||
assert(!is_utf8);
|
||||
/* Pass the message to our sibling in native charset. */
|
||||
/* Pass the message to our sibling in native charset. is_utf8
|
||||
* may be set here if a malicious peer sends MSG_INFO/MSG_ERROR
|
||||
* to a daemon receiver (read_a_msg passes !am_generator); the
|
||||
* old assert(!is_utf8) made that a remotely-reachable abort.
|
||||
* Forwarding the bytes raw is safe -- the generator's rwrite()
|
||||
* gets is_utf8=0 and filtered_fwrite escapes non-printables. */
|
||||
send_msg((enum msgcode)code, buf, len, 0);
|
||||
return;
|
||||
}
|
||||
@@ -297,7 +319,12 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)
|
||||
in_block = 1;
|
||||
if (!log_initialised)
|
||||
log_init(0);
|
||||
strlcpy(msg, buf, MIN((int)sizeof msg, len + 1));
|
||||
/* buf holds exactly len bytes and is not necessarily NUL-terminated
|
||||
* (e.g. a forwarded MSG_* payload from read_a_msg), so copy by length
|
||||
* rather than strlcpy(), which would strlen() past the end of buf. */
|
||||
int mlen = MIN((int)sizeof msg - 1, len);
|
||||
memcpy(msg, buf, mlen);
|
||||
msg[mlen] = '\0';
|
||||
logit(priority, msg);
|
||||
in_block = 0;
|
||||
|
||||
@@ -372,7 +399,7 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)
|
||||
ierrno = errno;
|
||||
if (outbuf.len) {
|
||||
char trailing = inbuf.len ? '\0' : trailing_CR_or_NL;
|
||||
filtered_fwrite(f, convbuf, outbuf.len, 0, trailing);
|
||||
filtered_fwrite(f, convbuf, outbuf.len, 0, 0, trailing);
|
||||
if (trailing) {
|
||||
trailing_CR_or_NL = '\0';
|
||||
fflush(f);
|
||||
@@ -395,7 +422,7 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
filtered_fwrite(f, buf, len, !allow_8bit_chars, trailing_CR_or_NL);
|
||||
filtered_fwrite(f, buf, len, !allow_8bit_chars, 0, trailing_CR_or_NL);
|
||||
if (trailing_CR_or_NL)
|
||||
fflush(f);
|
||||
}
|
||||
@@ -499,12 +526,17 @@ void remember_initial_stats(void)
|
||||
initial_data_written = total_data_written;
|
||||
}
|
||||
|
||||
/* Size of log_formatted()'s per-escape "fmt" scratch buffer. log_format_has()
|
||||
* must bound its width-digit scan to the same limit so the two parsers agree on
|
||||
* where an escape letter falls (see the digit loop in each). */
|
||||
#define LOG_FMT_SIZE 32
|
||||
|
||||
/* A generic logging routine for send/recv, with parameter substitiution. */
|
||||
static void log_formatted(enum logcode code, const char *format, const char *op,
|
||||
struct file_struct *file, const char *fname, int iflags,
|
||||
const char *hlink)
|
||||
{
|
||||
char buf[MAXPATHLEN+1024], buf2[MAXPATHLEN], fmt[32];
|
||||
char buf[MAXPATHLEN+1024], buf2[MAXPATHLEN], fmt[LOG_FMT_SIZE];
|
||||
char *p, *s, *c;
|
||||
const char *n;
|
||||
size_t len, total;
|
||||
@@ -685,7 +717,7 @@ static void log_formatted(enum logcode code, const char *format, const char *op,
|
||||
case 'C':
|
||||
n = NULL;
|
||||
if (S_ISREG(file->mode)) {
|
||||
if (always_checksum)
|
||||
if (always_checksum && !(iflags & ITEM_DELETED))
|
||||
n = sum_as_hex(file_sum_nni->num, F_SUM(file), 1);
|
||||
else if (iflags & ITEM_TRANSFER)
|
||||
n = sum_as_hex(xfer_sum_nni->num, sender_file_sum, 0);
|
||||
@@ -750,6 +782,9 @@ static void log_formatted(enum logcode code, const char *format, const char *op,
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '%':
|
||||
n = "%";
|
||||
break;
|
||||
}
|
||||
|
||||
/* "n" is the string to be inserted in place of this % code. */
|
||||
@@ -793,21 +828,33 @@ static void log_formatted(enum logcode code, const char *format, const char *op,
|
||||
int log_format_has(const char *format, char esc)
|
||||
{
|
||||
const char *p;
|
||||
int width;
|
||||
|
||||
if (!format)
|
||||
return 0;
|
||||
|
||||
for (p = format; (p = strchr(p, '%')) != NULL; ) {
|
||||
for (p++; *p == '\''; p++) {} /*SHARED ITERATOR*/
|
||||
if (*p == '-')
|
||||
/* Mirror log_formatted()'s width-digit scan exactly (c starts at
|
||||
* fmt+1, so width starts at 1): both must stop at the same digit
|
||||
* or they disagree on where the escape letter is, which for %C
|
||||
* can leave sender_keeps_checksum unset and over-read F_SUM. */
|
||||
width = 1;
|
||||
if (*p == '-') {
|
||||
p++;
|
||||
while (isDigit(p))
|
||||
width++;
|
||||
}
|
||||
while (isDigit(p) && width < LOG_FMT_SIZE - 8) {
|
||||
p++;
|
||||
width++;
|
||||
}
|
||||
while (*p == '\'') p++;
|
||||
if (!*p)
|
||||
break;
|
||||
if (*p == esc)
|
||||
return 1;
|
||||
if (*p == '%') /* %% is a literal '%', not the start of an escape */
|
||||
p++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
#ifdef __TANDEM
|
||||
#include <floss.h(floss_execlp)>
|
||||
#endif
|
||||
#ifdef HAVE_SYS_RESOURCE_H
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
|
||||
extern int dry_run;
|
||||
extern int list_only;
|
||||
@@ -48,6 +51,7 @@ extern int called_from_signal_handler;
|
||||
extern int need_messages_from_generator;
|
||||
extern int kluge_around_eof;
|
||||
extern int got_xfer_error;
|
||||
extern volatile sig_atomic_t got_sigusr2;
|
||||
extern int old_style_args;
|
||||
extern int msgs2stderr;
|
||||
extern int module_id;
|
||||
@@ -712,20 +716,24 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
|
||||
dest_path = dot_dir_or_error();
|
||||
|
||||
if (daemon_filter_list.head) {
|
||||
char *slash = strrchr(dest_path, '/');
|
||||
/* Collapse ".." for the NAME-based daemon filter check so a "../excluded"
|
||||
* destination is matched by name, as stock rsync does on its sanitized
|
||||
* arg. Done on a copy: the daemon exclude/filter is name-based (a symlink
|
||||
* whose own name is not excluded is still followed -- see rsyncd.conf(5)
|
||||
* "munge symlinks"), and the real dest_path is left for the resolver. */
|
||||
char cleaned[MAXPATHLEN], *slash;
|
||||
if (!sanitize_path(cleaned, dest_path, NULL, 0, SP_KEEP_DOT_DIRS))
|
||||
strlcpy(cleaned, dest_path, sizeof cleaned);
|
||||
slash = strrchr(cleaned, '/');
|
||||
if (slash && (slash[1] == '\0' || (slash[1] == '.' && slash[2] == '\0')))
|
||||
*slash = '\0';
|
||||
else
|
||||
slash = NULL;
|
||||
if ((*dest_path != '.' || dest_path[1] != '\0')
|
||||
&& (check_filter(&daemon_filter_list, FLOG, dest_path, 0) < 0
|
||||
|| check_filter(&daemon_filter_list, FLOG, dest_path, 1) < 0)) {
|
||||
if ((*cleaned != '.' || cleaned[1] != '\0')
|
||||
&& (check_filter(&daemon_filter_list, FLOG, cleaned, 0) < 0
|
||||
|| check_filter(&daemon_filter_list, FLOG, cleaned, 1) < 0)) {
|
||||
rprintf(FERROR, "ERROR: daemon has excluded destination \"%s\"\n",
|
||||
dest_path);
|
||||
exit_cleanup(RERR_FILESELECT);
|
||||
}
|
||||
if (slash)
|
||||
*slash = '/';
|
||||
}
|
||||
|
||||
/* See what currently exists at the destination. */
|
||||
@@ -739,10 +747,10 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
|
||||
if (ret < 0)
|
||||
goto mkdir_error;
|
||||
if (ret && (INFO_GTE(NAME, 1) || stdout_format_has_i)) {
|
||||
if (file_total == 1 || trailing_slash)
|
||||
if (cp && (file_total == 1 || trailing_slash))
|
||||
*cp = '\0';
|
||||
rprintf(FINFO, "created %d director%s for %s\n", ret, ret == 1 ? "y" : "ies", dest_path);
|
||||
if (file_total == 1 || trailing_slash)
|
||||
if (cp && (file_total == 1 || trailing_slash))
|
||||
*cp = '/';
|
||||
}
|
||||
if (ret)
|
||||
@@ -832,7 +840,16 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
|
||||
dest_path = "/";
|
||||
|
||||
*cp = '\0';
|
||||
if (!change_dir(dest_path, CD_NORMAL)) {
|
||||
if (dry_run && mkpath_dest_arg && do_stat(dest_path, &st) < 0) {
|
||||
/* --mkpath would have created this parent dir, but a dry run did
|
||||
* not, so don't chdir into it; flag the destination as not yet
|
||||
* present (as the dir-creation path above does) so the generator
|
||||
* doesn't try to compare against the missing tree (#880). Only
|
||||
* the missing-parent case is touched, so an ordinary file-to-file
|
||||
* dry run still itemizes against an existing destination. */
|
||||
dry_run++;
|
||||
change_dir(dest_path, CD_SKIP_CHDIR);
|
||||
} else if (!change_dir(dest_path, CD_NORMAL)) {
|
||||
rsyserr(FERROR, errno, "change_dir#3 %s failed",
|
||||
full_fname(dest_path));
|
||||
exit_cleanup(RERR_FILESELECT);
|
||||
@@ -855,13 +872,20 @@ static void check_alt_basis_dirs(void)
|
||||
|
||||
for (j = 0; j < basis_dir_cnt; j++) {
|
||||
char *bdir = basis_dir[j];
|
||||
assert(bdir != NULL); /* option-supplied root; never NULL */
|
||||
int bd_len = strlen(bdir);
|
||||
if (bd_len > 1 && bdir[bd_len-1] == '/')
|
||||
bdir[--bd_len] = '\0';
|
||||
if (dry_run > 1 && *bdir != '/') {
|
||||
/* Make a relative --link-dest/--copy-dest/--compare-dest absolute
|
||||
* (vs the destination curr_dir). These are operator-trusted roots, so
|
||||
* an absolute path makes the do_*_at() wrappers use plain resolution
|
||||
* rather than reject an operator '..' outside the dest tree (e.g.
|
||||
* --copy-dest=../to). Skipped when sanitize_paths already confined
|
||||
* them; the dry_run>1 case keeps its leading-"../"-strip. */
|
||||
if (*bdir != '/' && (dry_run > 1 || !sanitize_paths)) {
|
||||
int len = curr_dir_len + 1 + bd_len + 1;
|
||||
char *new = new_array(char, len);
|
||||
if (slash && strncmp(bdir, "../", 3) == 0) {
|
||||
if (dry_run > 1 && slash && strncmp(bdir, "../", 3) == 0) {
|
||||
/* We want to remove only one leading "../" prefix for
|
||||
* the directory we couldn't create in dry-run mode:
|
||||
* this ensures that any other ".." references get
|
||||
@@ -1088,11 +1112,13 @@ static int do_recv(int f_in, int f_out, char *local_name)
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
|
||||
/* Finally, we go to sleep until our parent kills us with a
|
||||
* USR2 signal. We sleep for a short time, as on some OSes
|
||||
* a signal won't interrupt a sleep! */
|
||||
while (1)
|
||||
/* Finally, we go to sleep until our parent tells us to wrap up
|
||||
* with a USR2 signal. We sleep for a short time, as on some OSes
|
||||
* a signal won't interrupt a sleep, then act on the flag the
|
||||
* (async-signal-safe) handler set. */
|
||||
while (!got_sigusr2)
|
||||
msleep(20);
|
||||
receive_sigusr2();
|
||||
}
|
||||
|
||||
am_generator = 1;
|
||||
@@ -1218,15 +1244,25 @@ static void do_server_recv(int f_in, int f_out, int argc, char *argv[])
|
||||
char **dir_p;
|
||||
filter_rule_list *elp = &daemon_filter_list;
|
||||
|
||||
/* Collapse ".." and strip the module-dir prefix to get the module-relative
|
||||
* name, but keep a leading "/" for a "path = /" module (module_dirlen <= 1)
|
||||
* so an absolute (module-rooted) filter rule still matches. */
|
||||
char clean[MAXPATHLEN], *dir;
|
||||
for (dir_p = basis_dir; *dir_p; dir_p++) {
|
||||
char *dir = *dir_p;
|
||||
if (*dir == '/')
|
||||
dir += module_dirlen;
|
||||
if (!sanitize_path(clean, *dir_p, "/", 0, SP_DEFAULT))
|
||||
strlcpy(clean, *dir_p, sizeof clean);
|
||||
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
|
||||
if (check_filter(elp, FLOG, dir, 1) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
if (partial_dir && *partial_dir == '/'
|
||||
&& check_filter(elp, FLOG, partial_dir + module_dirlen, 1) < 0) {
|
||||
if (partial_dir && *partial_dir == '/') {
|
||||
if (!sanitize_path(clean, partial_dir, "/", 0, SP_DEFAULT))
|
||||
strlcpy(clean, partial_dir, sizeof clean);
|
||||
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
|
||||
if (check_filter(elp, FLOG, dir, 1) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
if (0) {
|
||||
options_rejected:
|
||||
rprintf(FERROR, "Your options have been rejected by the server.\n");
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
@@ -1260,6 +1296,17 @@ void start_server(int f_in, int f_out, int argc, char *argv[])
|
||||
|
||||
if (am_sender) {
|
||||
keep_dirlinks = 0; /* Must be disabled on the sender. */
|
||||
|
||||
/* Mirror client_run()'s sender_keeps_checksum check: a daemon-
|
||||
* as-sender with -c and a `log format` containing %C will read
|
||||
* F_SUM(file) in log_formatted(), so make_file() must allocate
|
||||
* SUM_EXTRA_CNT. Without this, F_SUM() reads past the pool slot
|
||||
* and hex-encodes adjacent heap into the transfer log. */
|
||||
if (always_checksum
|
||||
&& (log_format_has(stdout_format, 'C')
|
||||
|| log_format_has(logfile_format, 'C')))
|
||||
sender_keeps_checksum = 1;
|
||||
|
||||
if (need_messages_from_generator)
|
||||
io_start_multiplex_in(f_in);
|
||||
else
|
||||
@@ -1323,7 +1370,7 @@ int client_run(int f_in, int f_out, pid_t pid, int argc, char *argv[])
|
||||
|
||||
become_copy_as_user();
|
||||
|
||||
flist = send_file_list(f_out, argc, argv);
|
||||
send_file_list(f_out, argc, argv);
|
||||
if (DEBUG_GTE(FLIST, 3))
|
||||
rprintf(FINFO,"file list sent\n");
|
||||
|
||||
@@ -1613,11 +1660,26 @@ static void sigusr1_handler(UNUSED(int val))
|
||||
exit_cleanup(RERR_SIGNAL1);
|
||||
}
|
||||
|
||||
/* SIGUSR2 tells the receiver child to wrap up. A signal handler must be
|
||||
* async-signal-safe, so it only sets a flag here; receive_sigusr2() does the
|
||||
* actual summary + shutdown (which use stdio/malloc/close) at a safe point in
|
||||
* the receiver's post-transfer wait loops (read_final_goodbye via perform_io,
|
||||
* and the trailing sleep). */
|
||||
static void sigusr2_handler(UNUSED(int val))
|
||||
{
|
||||
got_sigusr2 = 1;
|
||||
}
|
||||
|
||||
void receive_sigusr2(void)
|
||||
{
|
||||
if (!am_server)
|
||||
output_summary();
|
||||
close_all();
|
||||
#ifdef GCOV_COVERAGE
|
||||
/* The receiver child exits with _exit() here, bypassing the gcov atexit
|
||||
* flush; without this it writes no .gcda. */
|
||||
{ extern void __gcov_dump(void); __gcov_dump(); }
|
||||
#endif
|
||||
if (got_xfer_error)
|
||||
_exit(RERR_PARTIAL);
|
||||
_exit(0);
|
||||
@@ -1720,6 +1782,31 @@ static void unset_env_var(const char *var)
|
||||
}
|
||||
|
||||
|
||||
/* The symlink-race-safe path resolver (secure_relative_open) holds one open
|
||||
* dirfd per path component while it walks a path, plus an ancestor-dirfd cache
|
||||
* -- far more descriptors than legacy rsync's single open(). On a host with a
|
||||
* low default soft limit (e.g. OpenBSD's 128) a deep tree can hit EMFILE.
|
||||
* Raise the soft RLIMIT_NOFILE toward the hard limit (unprivileged, per
|
||||
* process; inherited by the sender/generator/receiver forks and daemon
|
||||
* children), but cap it: some systems set an enormous hard limit (2^20+) that
|
||||
* we don't want to adopt wholesale. */
|
||||
static void raise_fd_limit(void)
|
||||
{
|
||||
#if defined HAVE_GETRLIMIT && defined HAVE_SETRLIMIT && defined RLIMIT_NOFILE
|
||||
struct rlimit rl;
|
||||
rlim_t want = 4096; /* covers a MAXPATHLEN-deep walk + cache + headroom */
|
||||
|
||||
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
|
||||
return;
|
||||
if (want > rl.rlim_max)
|
||||
want = rl.rlim_max; /* never exceed the (admin-set) hard limit */
|
||||
if (rl.rlim_cur < want) { /* only ever raise, never lower an inherited limit */
|
||||
rl.rlim_cur = want;
|
||||
(void)setrlimit(RLIMIT_NOFILE, &rl); /* best-effort */
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc,char *argv[])
|
||||
{
|
||||
int ret;
|
||||
@@ -1727,6 +1814,8 @@ int main(int argc,char *argv[])
|
||||
raw_argc = argc;
|
||||
raw_argv = argv;
|
||||
|
||||
raise_fd_limit();
|
||||
|
||||
#ifdef HAVE_SIGACTION
|
||||
# ifdef HAVE_SIGPROCMASK
|
||||
sigset_t sigmask;
|
||||
@@ -1840,7 +1929,7 @@ int main(int argc,char *argv[])
|
||||
if (am_server && protect_args) {
|
||||
char buf[MAXPATHLEN];
|
||||
protect_args = 2;
|
||||
read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, &argv, &argc, NULL);
|
||||
read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, 0, &argv, &argc, NULL);
|
||||
if (!parse_arguments(&argc, (const char ***) &argv)) {
|
||||
option_error();
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
|
||||
@@ -44,6 +44,29 @@ extern struct stats stats;
|
||||
|
||||
#define TRADITIONAL_TABLESIZE (1<<16)
|
||||
|
||||
/* The maximum number of same-weak-checksum candidates we will compare
|
||||
* against at a single file offset before giving up and rolling forward a
|
||||
* byte. A weak checksum that collides thousands of times (very common in
|
||||
* disk/VM images, which contain large runs of identical blocks) would
|
||||
* otherwise turn hash_search()'s inner loop into an O(file_size *
|
||||
* chain_length) scan, pegging a CPU at 100% for hours with no apparent
|
||||
* progress (issue #217).
|
||||
*
|
||||
* Concretely, a synthetic 40000-block basis whose blocks all share one weak
|
||||
* checksum took ~18.4s to sync a 60KB source on a modern x86_64 box before
|
||||
* this cap and ~0.7s after it -- and the unbounded cost grows with the
|
||||
* square of the file size, which is what produced the multi-hour "hangs"
|
||||
* reported against real multi-GB images.
|
||||
*
|
||||
* Capping the per-offset work keeps the search bounded; any block we skip
|
||||
* over is simply sent as literal data, so the result is always correct --
|
||||
* only the transfer size is (slightly) affected. This is purely a
|
||||
* sender-side search limit: it changes no checksum, emitted byte, or
|
||||
* protocol field, so a capped sender interoperates with any receiver. */
|
||||
#ifndef MAX_CHAIN_LEN
|
||||
#define MAX_CHAIN_LEN 1024
|
||||
#endif
|
||||
|
||||
static uint32 tablesize;
|
||||
static int32 *hash_table;
|
||||
|
||||
@@ -182,6 +205,7 @@ static void hash_search(int f,struct sum_struct *s,
|
||||
int done_csum2 = 0;
|
||||
uint32 hash_entry;
|
||||
int32 i, *prev;
|
||||
int32 chain_len = 0;
|
||||
|
||||
if (DEBUG_GTE(DELTASUM, 4)) {
|
||||
rprintf(FINFO, "offset=%s sum=%04x%04x\n",
|
||||
@@ -218,6 +242,14 @@ static void hash_search(int f,struct sum_struct *s,
|
||||
if (sum != s->sums[i].sum1)
|
||||
continue;
|
||||
|
||||
/* Bound the work spent on a single pathological hash
|
||||
* bucket. If this weak checksum matches more than
|
||||
* MAX_CHAIN_LEN records, stop scanning and treat this
|
||||
* offset as a non-match (issue #217). The skipped data
|
||||
* is sent literally, never corrupted. */
|
||||
if (++chain_len > MAX_CHAIN_LEN)
|
||||
break;
|
||||
|
||||
/* also make sure the two blocks are the same length */
|
||||
l = (int32)MIN((OFF_T)s->blength, len-offset);
|
||||
if (l != s->sums[i].len)
|
||||
@@ -293,6 +325,7 @@ static void hash_search(int f,struct sum_struct *s,
|
||||
&& (!updating_basis_file || s->sums[want_i].offset >= offset
|
||||
|| s->sums[want_i].flags & SUMFLG_SAME_OFFSET)
|
||||
&& sum == s->sums[want_i].sum1
|
||||
&& l == s->sums[want_i].len
|
||||
&& memcmp(sum2, sum2_at(s, want_i), s->s2length) == 0) {
|
||||
/* we've found an adjacent match - the RLL coder
|
||||
* will be happy */
|
||||
@@ -370,6 +403,14 @@ void match_sums(int f, struct sum_struct *s, struct map_struct *buf, OFF_T len)
|
||||
sum_init(xfer_sum_nni, checksum_seed);
|
||||
|
||||
if (append_mode > 0) {
|
||||
if (s->flength > len) {
|
||||
/* A hostile or confused peer can claim a verified-prefix
|
||||
* length that exceeds what we have on disk -- including
|
||||
* for an empty local file, where buf is NULL and the
|
||||
* map_ptr() calls below would dereference it. Clamp to
|
||||
* what we can actually read. */
|
||||
s->flength = len;
|
||||
}
|
||||
if (append_mode == 2) {
|
||||
OFF_T j = 0;
|
||||
for (j = CHUNK_SIZE; j < s->flength; j += CHUNK_SIZE) {
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ if [ ! -f "$flagfile" ]; then
|
||||
if "$srcdir/md-convert" --test "$srcdir/rsync-ssl.1.md" >/dev/null 2>&1; then
|
||||
touch $flagfile
|
||||
else
|
||||
outname=`echo "$inname" | sed 's/\.md$//'`
|
||||
outname=`basename "$inname" .md`
|
||||
if [ -f "$outname" ]; then
|
||||
exit 0
|
||||
elif [ -f "$srcdir/$outname" ]; then
|
||||
|
||||
@@ -7,10 +7,20 @@ if [ ! -f git-version.h ]; then
|
||||
fi
|
||||
|
||||
if test -d "$srcdir/.git" || test -f "$srcdir/.git"; then
|
||||
gitver=`git describe --abbrev=8 2>/dev/null`
|
||||
# NOTE: I'm avoiding "|" in sed since I'm not sure if sed -r is portable and "\|" fails on some OSes.
|
||||
verchk=`echo "$gitver-" | sed -n '/^v3\.[0-9][0-9]*\.[0-9][0-9]*\(pre[0-9]*\)*-/p'`
|
||||
if [ -n "$verchk" ]; then
|
||||
# Identify a git build by the development version from version.h plus the
|
||||
# exact commit (e.g. "3.5.0dev-g1234abcd"), rather than the nearest release
|
||||
# tag that `git describe` would pick: that tag can sit far behind a rebased
|
||||
# development branch and then misnames the line you are actually on (showing,
|
||||
# say, 3.4.3 for a 3.5.0dev tree). This also works in a shallow/tag-less
|
||||
# clone. A release tarball has no .git, so git-version.h stays empty and
|
||||
# rsync prints the plain RSYNC_VERSION.
|
||||
# cd into the subshell rather than "git -C" (avoids needing a newer git).
|
||||
gitsha=`(cd "$srcdir" && git rev-parse --short=8 HEAD) 2>/dev/null`
|
||||
# Tolerate any preprocessor spacing and a trailing comment; capture only the
|
||||
# quoted value. Empty (define missing/unmatched) -> leave RSYNC_GITVER unset.
|
||||
rsyncver=`sed -n 's/^[[:space:]]*#[[:space:]]*define[[:space:]][[:space:]]*RSYNC_VERSION[[:space:]][[:space:]]*"\([^"]*\)".*/\1/p' "$srcdir/version.h"`
|
||||
if [ -n "$gitsha" ] && [ -n "$rsyncver" ]; then
|
||||
gitver="$rsyncver-g$gitsha"
|
||||
echo "#define RSYNC_GITVER \"$gitver\"" >git-version.h.new
|
||||
if ! diff git-version.h.new git-version.h >/dev/null; then
|
||||
echo "Updating git-version.h"
|
||||
|
||||
@@ -18,6 +18,7 @@ inheader {
|
||||
sub(/^CHAR\(/, "char ")
|
||||
sub(/^INTEGER\(/, "int ")
|
||||
sub(/^STRING\(/, "char *")
|
||||
sub(/^STRING_SHELL\(/, "char *")
|
||||
protos = protos "\n" $0 (local ? "(int module_id);" : "(void);")
|
||||
next
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Old rsync version archive
|
||||
|
||||
Static rsync binaries built from historical release tags. Two uses:
|
||||
|
||||
1. **Cross-version behaviour checks** — confirming whether a behaviour a user
|
||||
reported on an old release is version-specific or option-driven.
|
||||
2. **The version-mixing test suite** — `runtests.py --rsync-bin2=...` runs the
|
||||
current code against one of these as the daemon / remote-shell peer; CI
|
||||
(`.github/workflows/ubuntu-version-mix.yml`) does this for every binary
|
||||
here against the per-version manifests in `testsuite/expect/`.
|
||||
|
||||
Binaries are **statically linked** so they run regardless of the host's
|
||||
shared libraries, and named `rsync_<version>`:
|
||||
|
||||
| Binary | Version | Protocol | Notes |
|
||||
|----------------|---------|----------|-----------------------------------------|
|
||||
| `rsync_2.6.0` | 2.6.0 | 27 | 2004; needs autoconf regen (see below) |
|
||||
| `rsync_3.0.0` | 3.0.0 | 30 | 2008 |
|
||||
| `rsync_3.1.0` | 3.1.0 | 31 | 2013 |
|
||||
| `rsync_3.1.3` | 3.1.3 | 31 | Ubuntu 18.04 / Debian buster era (2018) |
|
||||
| `rsync_3.2.0` | 3.2.0 | 31 | 2020 (zstd/lz4/xxhash negotiation added)|
|
||||
| `rsync_3.2.7` | 3.2.7 | 31 | 2022 |
|
||||
| `rsync_3.3.0` | 3.3.0 | 31 | 2024 |
|
||||
| `rsync_3.4.0` | 3.4.0 | 32 | 2025 |
|
||||
| `rsync_3.4.1` | 3.4.1 | 32 | 2025 |
|
||||
|
||||
These are every `x.y.0` release from 2.6.0 (2004) onward plus a few point
|
||||
releases. 2.6.0 is the practical floor: older tags need progressively more
|
||||
porting to build on a current toolchain.
|
||||
|
||||
All built `--disable-openssl` and with `_FORTIFY_SOURCE` disabled (see below);
|
||||
xxhash/zstd/lz4 are compiled in where the version supports them.
|
||||
|
||||
## Adding a version
|
||||
|
||||
```bash
|
||||
./build_static.sh 3.2.7 # uses git tag v3.2.7
|
||||
./build_static.sh 3.0.9 v3.0.9 # explicit tag if naming differs
|
||||
```
|
||||
|
||||
The script checks out the tag into a throwaway `git worktree`, applies the
|
||||
minimal patches needed to compile old sources on a modern toolchain, links
|
||||
statically, verifies the result is static and reports the requested version,
|
||||
then installs `rsync_<version>` here and removes the worktree.
|
||||
|
||||
Override the source repo with `RSYNC_REPO=/path/to/rsync ./build_static.sh ...`
|
||||
(defaults to `../rsync.4`).
|
||||
|
||||
## Why the patches?
|
||||
|
||||
Modern GCC (>= 14, C23 default) and glibc reject things old rsync relied on.
|
||||
`build_static.sh` handles these, each guarded so it's a no-op when not needed:
|
||||
|
||||
1. **K&R `lseek64()` redeclaration** in `syscall.c` clashes with glibc's real
|
||||
prototype — removed.
|
||||
2. **`gettimeofday()`** — glibc only has the 2-arg form; configure misdetects
|
||||
the 1-arg form, so `HAVE_GETTIMEOFDAY_TZ` is forced on in `config.h`.
|
||||
3. **C23 `()` == `(void)`** breaks K&R prototypes called with arguments
|
||||
(`qsort` comparator, `pool->bomb`, etc.) — built with `-std=gnu11`.
|
||||
4. Assorted modern `-Werror` promotions (incompatible pointer types, implicit
|
||||
declarations) downgraded to warnings; bundled zlib/popt used to keep the
|
||||
static link self-contained.
|
||||
|
||||
5. **OpenSSL (3.2+)** is disabled with `--disable-openssl`: linking
|
||||
`libcrypto.a` statically drags in jitterentropy (`jent_*`) and zlib's
|
||||
`uncompress` (OpenSSL's COMP module), which don't resolve here. OpenSSL only
|
||||
provided optional MD4/MD5, which rsync implements natively, so checksum
|
||||
behaviour is unaffected.
|
||||
|
||||
6. **`_FORTIFY_SOURCE` disabled** (`-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0`):
|
||||
modern Ubuntu defaults it to `=3`, whose stricter object-size checks turn
|
||||
latent (historically benign) over-reads in OLD rsync into hard
|
||||
`*** buffer overflow detected ***` aborts when the binary runs as a
|
||||
server/daemon — which made e.g. 3.1.3 and 3.2.7 unusable as peers. Disabling
|
||||
it makes the archival binaries behave as the released versions did.
|
||||
|
||||
7. **Pre-3.0 tags (e.g. 2.6.0)** ship `configure.in`, not a generated
|
||||
`configure`. The script runs `autoheader`/`autoconf` to generate it, after
|
||||
neutralizing the `AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...))` fallbacks for
|
||||
`inet_ntop`/`inet_pton`/`getaddrinfo`/`getnameinfo` — modern autoconf emits
|
||||
broken shell for those never-taken branches (the funcs exist in glibc). It
|
||||
also generates `proto.h` (no make rule in that era) and stubs the vendored
|
||||
`lib/addrinfo.h` the tag dropped (modern glibc supplies `struct addrinfo`).
|
||||
All guarded so they no-op on 3.x.
|
||||
|
||||
Newer versions may need fewer or different tweaks; if a build fails, the
|
||||
script prints the first compiler errors from its log.
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
# Build a static rsync binary from a historical git tag, for cross-version
|
||||
# behaviour testing. Produces ./rsync_<version> in this directory.
|
||||
#
|
||||
# Usage: ./build_static.sh <version> [git-tag]
|
||||
# Example: ./build_static.sh 3.1.3 # uses tag v3.1.3
|
||||
# ./build_static.sh 3.2.7 v3.2.7
|
||||
#
|
||||
# Old rsync releases don't compile cleanly on a modern toolchain (GCC >= 14
|
||||
# defaults to C23, where an empty () prototype means (void); glibc dropped the
|
||||
# 1-arg gettimeofday; lseek64 K&R redeclarations clash). This script applies
|
||||
# the minimal, best-effort workarounds and links statically so the result is
|
||||
# self-contained and reproducible regardless of the host's shared libraries.
|
||||
#
|
||||
# Each workaround is guarded so it's a no-op on versions that don't need it.
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?usage: build_static.sh <version> [git-tag]}"
|
||||
TAG="${2:-v$VERSION}"
|
||||
|
||||
ARCHIVE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="${RSYNC_REPO:-/home/tridge/project/rsync/rsync.4}" # any rsync worktree
|
||||
WORKTREE="$(mktemp -d /tmp/rsync-build-XXXXXX)"
|
||||
OUT="$ARCHIVE_DIR/rsync_$VERSION"
|
||||
|
||||
# C standard restores K&R () semantics; permissive flags downgrade the pile of
|
||||
# modern -Werror promotions (incompatible pointers, implicit decls) to warnings.
|
||||
# _FORTIFY_SOURCE is forced OFF: modern Ubuntu defaults it to =3, whose stricter
|
||||
# object-size checks turn latent (historically benign) over-reads in OLD rsync
|
||||
# into hard "*** buffer overflow detected ***" aborts when the binary acts as a
|
||||
# server/daemon. Disabling it makes these archival binaries behave the way the
|
||||
# released versions did, which is the whole point of the archive.
|
||||
CFLAGS_OLD="-I. -I./zlib -O2 -g -std=gnu11 -fcommon -DHAVE_CONFIG_H -Wno-error \
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||
-Wno-incompatible-pointer-types -Wno-implicit-function-declaration -Wno-int-conversion"
|
||||
|
||||
cleanup() {
|
||||
cd "$REPO"
|
||||
git worktree remove --force "$WORKTREE" 2>/dev/null || true
|
||||
git worktree prune 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ">>> checking out $TAG into $WORKTREE"
|
||||
# prefer an exact tag to avoid ambiguity with similarly-named branches
|
||||
REF="$TAG"
|
||||
if git -C "$REPO" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||
REF="refs/tags/$TAG"
|
||||
fi
|
||||
git -C "$REPO" worktree add --detach "$WORKTREE" "$REF"
|
||||
cd "$WORKTREE"
|
||||
|
||||
# --- workaround 1: K&R lseek64 redeclaration clashes with glibc's prototype ---
|
||||
if grep -q 'off64_t lseek64();' syscall.c 2>/dev/null; then
|
||||
echo ">>> patching syscall.c lseek64 redeclaration"
|
||||
perl -0pi -e 's/#ifdef HAVE_LSEEK64\n#if !SIZEOF_OFF64_T\n\tOFF_T lseek64\(\);\n#else\n\toff64_t lseek64\(\);\n#endif\n\treturn lseek64/#ifdef HAVE_LSEEK64\n\treturn lseek64/' syscall.c
|
||||
fi
|
||||
|
||||
# --- workaround 0: pre-3.0 tags ship configure.in, not a generated configure.
|
||||
# Generate it. Modern autoconf emits broken shell for their
|
||||
# AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...)) fallbacks -- but those branches are
|
||||
# dead on a modern host (glibc has inet_ntop/inet_pton/getaddrinfo/getnameinfo),
|
||||
# so neutralize the AC_LIBOBJ replacements before regenerating.
|
||||
OLD_TREE=0
|
||||
if [ ! -f ./configure ] && { [ -f configure.in ] || [ -f configure.ac ]; }; then
|
||||
OLD_TREE=1
|
||||
acsrc=configure.ac; [ -f configure.in ] && acsrc=configure.in
|
||||
echo ">>> generating configure for an old tag (autoheader/autoconf)"
|
||||
sed -i 's#AC_LIBOBJ(lib/[a-zA-Z_]*)#:#g' "$acsrc"
|
||||
autoheader 2>/dev/null || true
|
||||
autoconf 2>/dev/null || { echo "autoconf failed"; exit 1; }
|
||||
fi
|
||||
|
||||
CONF_ARGS=(--disable-md2man --with-included-zlib=yes --with-included-popt=yes)
|
||||
# OpenSSL (3.2+) only adds optional MD4/MD5 that rsync already implements, but
|
||||
# linking libcrypto.a statically drags in jitterentropy + zlib's uncompress,
|
||||
# which aren't resolvable here. Drop it when the flag exists.
|
||||
if ./configure --help 2>/dev/null | grep -q -- '--disable-openssl'; then
|
||||
echo ">>> disabling openssl for self-contained static link"
|
||||
CONF_ARGS+=(--disable-openssl)
|
||||
fi
|
||||
|
||||
echo ">>> configure (bundled zlib + popt, static-friendly)"
|
||||
./configure "${CONF_ARGS[@]}" \
|
||||
>"$WORKTREE/conf.log" 2>&1 || { tail -20 "$WORKTREE/conf.log"; exit 1; }
|
||||
|
||||
# --- workaround 2: modern glibc only has the 2-arg gettimeofday ---------------
|
||||
if grep -q '/\* #undef HAVE_GETTIMEOFDAY_TZ \*/' config.h; then
|
||||
echo ">>> forcing HAVE_GETTIMEOFDAY_TZ (configure misdetects it)"
|
||||
sed -i 's|/\* #undef HAVE_GETTIMEOFDAY_TZ \*/|#define HAVE_GETTIMEOFDAY_TZ 1|' config.h
|
||||
fi
|
||||
|
||||
# --- workaround 4 (old trees only): generate proto.h if the tree has no make
|
||||
# rule for it, and stub a vendored lib/addrinfo.h that the git tag dropped
|
||||
# (modern glibc supplies struct addrinfo / sockaddr_storage, so empty is right).
|
||||
if [ "$OLD_TREE" = 1 ]; then
|
||||
if [ ! -f proto.h ] && [ -f mkproto.awk ]; then
|
||||
echo ">>> generating proto.h"
|
||||
cat ./*.c ./lib/compat.c 2>/dev/null | awk -f ./mkproto.awk > proto.h
|
||||
fi
|
||||
if grep -q 'include "lib/addrinfo.h"' rsync.h 2>/dev/null && [ ! -f lib/addrinfo.h ]; then
|
||||
echo ">>> stubbing lib/addrinfo.h"
|
||||
echo '/* emptied: modern glibc provides struct addrinfo */' > lib/addrinfo.h
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ">>> building (static)"
|
||||
make -j"$(nproc)" CFLAGS="$CFLAGS_OLD" LDFLAGS="-static" \
|
||||
>"$WORKTREE/make.log" 2>&1 || { grep -E 'error:|\*\*\*' "$WORKTREE/make.log" | head; exit 1; }
|
||||
|
||||
# verify it's actually static before we keep it
|
||||
if ldd ./rsync 2>&1 | grep -qv 'not a dynamic executable'; then
|
||||
echo "ERROR: binary is not statically linked:" >&2
|
||||
ldd ./rsync >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GOT="$(./rsync --version | head -1 | awk '{print $3}')"
|
||||
if [ "$GOT" != "$VERSION" ]; then
|
||||
echo "ERROR: built version '$GOT' != requested '$VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp ./rsync "$OUT"
|
||||
strip "$OUT"
|
||||
echo ">>> installed $OUT"
|
||||
"$OUT" --version | head -1
|
||||
file "$OUT"
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -27,6 +27,7 @@
|
||||
extern int module_id;
|
||||
extern int local_server;
|
||||
extern int sanitize_paths;
|
||||
extern int operator_path_resolve;
|
||||
extern int trust_sender_args;
|
||||
extern int trust_sender_filter;
|
||||
extern unsigned int module_dirlen;
|
||||
@@ -59,6 +60,9 @@ int preserve_perms = 0;
|
||||
int preserve_executability = 0;
|
||||
int preserve_devices = 0;
|
||||
int preserve_specials = 0;
|
||||
int drop_devices = 0;
|
||||
char *confine_root = NULL; /* --confine-root: see syscall.c */
|
||||
unsigned int confine_rootlen = 0;
|
||||
int preserve_uid = 0;
|
||||
int preserve_gid = 0;
|
||||
int preserve_mtimes = 0;
|
||||
@@ -87,6 +91,7 @@ int preallocate_files = 0;
|
||||
int do_compression = 0;
|
||||
int do_compression_level = CLVL_NOT_SPECIFIED;
|
||||
int do_compression_threads = 0; /*n = 0 use rsync thread, n >= 1 spawn n threads for compression */
|
||||
#define MAX_DAEMON_COMPRESSION_THREADS 8
|
||||
int am_root = 0; /* 0 = normal, 1 = root, 2 = --super, -1 = --fake-super */
|
||||
int am_server = 0;
|
||||
int am_sender = 0;
|
||||
@@ -126,6 +131,7 @@ int connect_timeout = 0;
|
||||
int keep_partial = 0;
|
||||
int safe_symlinks = 0;
|
||||
int copy_unsafe_links = 0;
|
||||
int insecure_links = 0;
|
||||
int munge_symlinks = 0;
|
||||
int use_secure_symlinks = 0;
|
||||
int size_only = 0;
|
||||
@@ -451,7 +457,10 @@ static void parse_output_words(struct output_struct *words, short *levels, const
|
||||
len--;
|
||||
}
|
||||
lev = isDigit(str+len) ? atoi(str+len) : 1;
|
||||
if (lev > MAX_OUT_LEVEL)
|
||||
/* atoi() of an overflowing positive digit string can return a
|
||||
* negative int (LONG_MAX truncated on LP64); a negative lev
|
||||
* here later indexes counts[lev] in make_output_option(). */
|
||||
if (lev > MAX_OUT_LEVEL || lev < 0)
|
||||
lev = MAX_OUT_LEVEL;
|
||||
if (len == 4 && strncasecmp(str, "help", 4) == 0) {
|
||||
output_item_help(words);
|
||||
@@ -676,12 +685,17 @@ static struct poptOption long_options[] = {
|
||||
{"no-write-devices", 0, POPT_ARG_VAL, &write_devices, 0, 0, 0 },
|
||||
{"specials", 0, POPT_ARG_VAL, &preserve_specials, 1, 0, 0 },
|
||||
{"no-specials", 0, POPT_ARG_VAL, &preserve_specials, 0, 0, 0 },
|
||||
{"drop-D", 0, POPT_ARG_VAL, &drop_devices, 1, 0, 0 },
|
||||
{"no-drop-D", 0, POPT_ARG_VAL, &drop_devices, 0, 0, 0 },
|
||||
{"confine-root", 0, POPT_ARG_STRING, &confine_root, 0, 0, 0 },
|
||||
{"links", 'l', POPT_ARG_VAL, &preserve_links, 1, 0, 0 },
|
||||
{"no-links", 0, POPT_ARG_VAL, &preserve_links, 0, 0, 0 },
|
||||
{"no-l", 0, POPT_ARG_VAL, &preserve_links, 0, 0, 0 },
|
||||
{"copy-links", 'L', POPT_ARG_NONE, ©_links, 0, 0, 0 },
|
||||
{"copy-unsafe-links",0, POPT_ARG_NONE, ©_unsafe_links, 0, 0, 0 },
|
||||
{"safe-links", 0, POPT_ARG_NONE, &safe_symlinks, 0, 0, 0 },
|
||||
{"insecure-links", 0, POPT_ARG_VAL, &insecure_links, 1, 0, 0 },
|
||||
{"no-insecure-links",0, POPT_ARG_VAL, &insecure_links, 0, 0, 0 },
|
||||
{"munge-links", 0, POPT_ARG_VAL, &munge_symlinks, 1, 0, 0 },
|
||||
{"no-munge-links", 0, POPT_ARG_VAL, &munge_symlinks, 0, 0, 0 },
|
||||
{"copy-dirlinks", 'k', POPT_ARG_NONE, ©_dirlinks, 0, 0, 0 },
|
||||
@@ -904,9 +918,54 @@ void option_error(void)
|
||||
}
|
||||
|
||||
|
||||
/* Does this row store a compile-time constant, and if so which?
|
||||
*
|
||||
* popt's `val` is not comparable across argInfo kinds. For POPT_ARG_VAL it IS
|
||||
* the value stored in `arg`; for the others a nonzero `val` is an action code
|
||||
* handed to the parser's switch, and POPT_ARG_NONE with a destination stores 1
|
||||
* regardless. Comparing the raw field therefore misses aliases spelled with
|
||||
* different table shapes -- --del is POPT_ARG_NONE/&delete_during/0 and
|
||||
* --delete-during is POPT_ARG_VAL/&delete_during/1, and both set it to 1. */
|
||||
static int refuse_const_assign(const struct poptOption *op, int *valp)
|
||||
{
|
||||
if (!op->arg)
|
||||
return 0;
|
||||
if (op->argInfo == POPT_ARG_VAL) {
|
||||
*valp = op->val;
|
||||
return 1;
|
||||
}
|
||||
/* A nonzero val here means the row ALSO runs a parser action, so it is
|
||||
* not merely an assignment and must not be folded in with one. */
|
||||
if (op->argInfo == POPT_ARG_NONE && op->val == 0) {
|
||||
*valp = 1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Do two table rows name the same capability? An exact refuse rule names a
|
||||
* capability, not one spelling of it. */
|
||||
static int same_refuse_action(const struct poptOption *a, const struct poptOption *b)
|
||||
{
|
||||
int a_val, b_val;
|
||||
|
||||
/* Constant assignments: same destination, same resulting value. The
|
||||
* value check keeps opposite switches such as --foo and --no-foo apart,
|
||||
* since they differ only in what they store. */
|
||||
if (refuse_const_assign(a, &a_val) && refuse_const_assign(b, &b_val))
|
||||
return a->arg == b->arg && a_val == b_val;
|
||||
|
||||
/* Anything else has to match as a table entry: a row storing a runtime
|
||||
* value (POPT_ARG_INT, POPT_ARG_STRING) needs the same destination and
|
||||
* action code, and an action-only row the same nonzero code. */
|
||||
if (a->argInfo != b->argInfo || a->val != b->val)
|
||||
return 0;
|
||||
return a->arg ? a->arg == b->arg : !b->arg && a->val != 0;
|
||||
}
|
||||
|
||||
static void parse_one_refuse_match(int negated, const char *ref, const struct poptOption *list_end)
|
||||
{
|
||||
struct poptOption *op;
|
||||
struct poptOption *op, *matched_op = NULL;
|
||||
char shortName[2];
|
||||
int is_wild = strpbrk(ref, "*?[") != NULL;
|
||||
int found_match = 0;
|
||||
@@ -927,8 +986,21 @@ static void parse_one_refuse_match(int negated, const char *ref, const struct po
|
||||
else if (!is_wild)
|
||||
op->descrip = negated ? "a=" : "r=";
|
||||
found_match = 1;
|
||||
if (!is_wild)
|
||||
if (!is_wild) {
|
||||
matched_op = op;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matched_op) {
|
||||
for (op = long_options; op != list_end; op++) {
|
||||
if (op == matched_op || !same_refuse_action(op, matched_op))
|
||||
continue;
|
||||
if (op->descrip[1] == '*')
|
||||
op->descrip = negated ? "a*" : "r*";
|
||||
else
|
||||
op->descrip = negated ? "a=" : "r=";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,6 +1080,11 @@ static void set_refuse_options(void)
|
||||
parse_one_refuse_match(0, "iconv", list_end);
|
||||
#endif
|
||||
parse_one_refuse_match(0, "log-file*", list_end);
|
||||
/* A client must never disable the daemon's symlink confinement:
|
||||
* --insecure-links is a local-only flag, so the daemon hard-refuses it
|
||||
* (dropping the connection). The daemon's own opt-out is the
|
||||
* "insecure links" module parameter, not this flag. */
|
||||
parse_one_refuse_match(0, "insecure-links", list_end);
|
||||
}
|
||||
|
||||
#ifndef SUPPORT_ATIMES
|
||||
@@ -1089,6 +1166,8 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op
|
||||
int reps, mult, len;
|
||||
const char *arg, *err = "invalid", *min_max = NULL;
|
||||
ssize_t limit = -1, size = 1;
|
||||
ssize_t size_max = max_value >= 0 ? max_value : (ssize_t)(SIZE_MAX / 2);
|
||||
double dsize;
|
||||
|
||||
for (arg = size_arg; isDigit(arg); arg++) {}
|
||||
if (*arg == '.' || *arg == get_decimal_point()) /* backward compatibility: always allow '.' */
|
||||
@@ -1123,11 +1202,38 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op
|
||||
mult = 1024, arg += 2;
|
||||
else
|
||||
goto failure;
|
||||
while (reps--)
|
||||
while (reps--) {
|
||||
if (size > size_max / mult) {
|
||||
err = "too large";
|
||||
min_max = "max";
|
||||
limit = max_value;
|
||||
goto failure;
|
||||
}
|
||||
size *= mult;
|
||||
size *= atof(size_arg);
|
||||
if ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != size_arg)
|
||||
size += atoi(arg), arg += 2;
|
||||
}
|
||||
errno = 0;
|
||||
dsize = strtod(size_arg, NULL);
|
||||
if (errno == ERANGE || dsize < 0 || dsize > (double)size_max / size
|
||||
|| (max_value < 0 && dsize >= (double)size_max / size)) {
|
||||
err = "too large";
|
||||
min_max = "max";
|
||||
limit = max_value;
|
||||
goto failure;
|
||||
}
|
||||
size = (ssize_t)(dsize * size);
|
||||
if ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != size_arg) {
|
||||
if (*arg == '+') {
|
||||
if (size == size_max) {
|
||||
err = "too large";
|
||||
min_max = "max";
|
||||
limit = max_value;
|
||||
goto failure;
|
||||
}
|
||||
size++;
|
||||
} else
|
||||
size--;
|
||||
arg += 2;
|
||||
}
|
||||
if (*arg)
|
||||
goto failure;
|
||||
if (size < 0 || (max_value >= 0 && size > max_value)) {
|
||||
@@ -1151,6 +1257,8 @@ failure:
|
||||
min_max, do_big_num(limit, 3, NULL),
|
||||
unlimited_0 && min_max[1] == 'i' ? " or 0 for unlimited" : "");
|
||||
}
|
||||
if (len < 0 || len > (int)sizeof err_buf - 2)
|
||||
len = sizeof err_buf - 2;
|
||||
err_buf[len] = '\n';
|
||||
err_buf[len+1] = '\0';
|
||||
return -1;
|
||||
@@ -1489,7 +1597,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
*argc_p = 0;
|
||||
} else if (poptDupArgv(argc, argv, argc_p, argv_p) != 0)
|
||||
out_of_memory("parse_arguments");
|
||||
argv = *argv_p;
|
||||
poptFreeContext(pc);
|
||||
|
||||
am_starting_up = 0;
|
||||
@@ -1960,6 +2067,10 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
ssize_t size = parse_size_arg(max_alloc_arg, 'B', "max-alloc", 1024*1024, -1, True);
|
||||
if (size < 0)
|
||||
goto cleanup;
|
||||
if (size == 0) {
|
||||
snprintf(err_buf, sizeof err_buf, "max-alloc must be greater than zero\n");
|
||||
goto cleanup;
|
||||
}
|
||||
max_alloc = size;
|
||||
}
|
||||
if (!max_alloc)
|
||||
@@ -2024,6 +2135,12 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
}
|
||||
if (do_compression_threads < 0)
|
||||
do_compression_threads = 0;
|
||||
/* A daemon client controls the server-side sender arguments. Keep one
|
||||
* unauthenticated connection from asking Zstandard to materialize its
|
||||
* implementation maximum (currently hundreds) of worker threads. Local
|
||||
* and remote-shell invocations retain the operator-requested value. */
|
||||
if (am_daemon && do_compression_threads > MAX_DAEMON_COMPRESSION_THREADS)
|
||||
do_compression_threads = MAX_DAEMON_COMPRESSION_THREADS;
|
||||
}
|
||||
|
||||
#ifdef HAVE_SETVBUF
|
||||
@@ -2262,6 +2379,26 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
}
|
||||
}
|
||||
|
||||
if (confine_root) {
|
||||
/* A daemon already has module_dir for this job, and honouring a
|
||||
* peer-supplied root there could only loosen the module boundary. */
|
||||
if (am_daemon)
|
||||
confine_root = NULL;
|
||||
else if (*confine_root != '/') {
|
||||
snprintf(err_buf, sizeof err_buf,
|
||||
"--confine-root must be an absolute path\n");
|
||||
return 0;
|
||||
} else if (insecure_links) {
|
||||
/* The opt-out restores the legacy open, which short-circuits the
|
||||
* walk that enforces the root -- so the pair would silently mean
|
||||
* no confinement at all. Say so instead. */
|
||||
snprintf(err_buf, sizeof err_buf,
|
||||
"--insecure-links cannot be combined with --confine-root\n");
|
||||
return 0;
|
||||
} else
|
||||
confine_root = normalize_path(confine_root, True, &confine_rootlen);
|
||||
}
|
||||
|
||||
if (sanitize_paths) {
|
||||
int i;
|
||||
for (i = argc; i-- > 0; )
|
||||
@@ -2273,21 +2410,26 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
}
|
||||
if (daemon_filter_list.head && !am_sender) {
|
||||
filter_rule_list *elp = &daemon_filter_list;
|
||||
/* Strip the module-dir prefix to get the module-relative name, but keep a
|
||||
* leading "/" for a "path = /" module (module_dirlen <= 1) so an absolute
|
||||
* (module-rooted) filter rule still matches. */
|
||||
if (tmpdir) {
|
||||
char *dir;
|
||||
char clean[MAXPATHLEN], *dir;
|
||||
if (!*tmpdir)
|
||||
goto options_rejected;
|
||||
dir = tmpdir + (*tmpdir == '/' ? module_dirlen : 0);
|
||||
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
|
||||
if (!sanitize_path(clean, tmpdir, "/", 0, SP_DEFAULT))
|
||||
strlcpy(clean, tmpdir, sizeof clean);
|
||||
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
|
||||
if (check_filter(elp, FLOG, dir, 1) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
if (backup_dir) {
|
||||
char *dir;
|
||||
char clean[MAXPATHLEN], *dir;
|
||||
if (!*backup_dir)
|
||||
goto options_rejected;
|
||||
dir = backup_dir + (*backup_dir == '/' ? module_dirlen : 0);
|
||||
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
|
||||
if (!sanitize_path(clean, backup_dir, "/", 0, SP_DEFAULT))
|
||||
strlcpy(clean, backup_dir, sizeof clean);
|
||||
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
|
||||
if (check_filter(elp, FLOG, dir, 1) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
@@ -2464,7 +2606,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
|
||||
if (files_from) {
|
||||
char *h, *p;
|
||||
int q;
|
||||
int q = 0;
|
||||
if (argc > 2 || (!am_daemon && !am_server && argc == 1)) {
|
||||
usage(FERROR);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
@@ -2498,7 +2640,19 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
if (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
filesfrom_fd = open(files_from, O_RDONLY|O_BINARY);
|
||||
/* Operator-supplied path that may transit attacker-writable
|
||||
* parents; refuse symlinks not owned by uid 0 or our euid,
|
||||
* as for --exclude-from/--include-from/--filter in exclude.c.
|
||||
* A daemon reads this list from a CLIENT-requested path
|
||||
* (--files-from=:LIST) and it must stay inside the module:
|
||||
* operator_path_resolve makes the ownership walk also refuse a
|
||||
* (trusted-owned) symlink that redirects the list outside the
|
||||
* module root -- e.g. a root-owned backup symlink. No-op off a
|
||||
* daemon (the module-root check only fires when am_daemon). */
|
||||
int save_opr = operator_path_resolve;
|
||||
operator_path_resolve = 1;
|
||||
filesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0);
|
||||
operator_path_resolve = save_opr;
|
||||
if (filesfrom_fd < 0) {
|
||||
snprintf(err_buf, sizeof err_buf,
|
||||
"failed to open files-from file %s: %s\n",
|
||||
@@ -2538,7 +2692,7 @@ static char SPLIT_ARG_WHEN_OLD[1];
|
||||
**/
|
||||
char *safe_arg(const char *opt, const char *arg)
|
||||
{
|
||||
#define SHELL_CHARS "!#$&;|<>(){}\"'` \t\\"
|
||||
#define SHELL_CHARS "!#$&;|<>(){}\"\'` \t\n\r\\"
|
||||
#define WILD_CHARS "*?[]" /* We don't allow remote brace expansion */
|
||||
BOOL is_filename_arg = !opt;
|
||||
char *escapes = is_filename_arg ? SHELL_CHARS : WILD_CHARS SHELL_CHARS;
|
||||
@@ -2557,7 +2711,16 @@ char *safe_arg(const char *opt, const char *arg)
|
||||
escape_leading_tilde = 1;
|
||||
}
|
||||
for (f = arg; *f; f++) {
|
||||
if (strchr(escapes, *f))
|
||||
if (*f == '\\') {
|
||||
/* Mirror the writer below: in filename mode a backslash
|
||||
* before a wildcard is not doubled, so don't reserve a slot
|
||||
* for it. The "f[1] &&" also avoids the strchr(WILD_CHARS,
|
||||
* '\0') footgun (which matches the terminator) on a trailing
|
||||
* backslash -- otherwise the counter and writer disagree and
|
||||
* an uninitialized heap byte leaks into the result. */
|
||||
if (!is_filename_arg || !(f[1] && strchr(WILD_CHARS, f[1])))
|
||||
extras++;
|
||||
} else if (strchr(escapes, *f))
|
||||
extras++;
|
||||
}
|
||||
}
|
||||
@@ -2582,7 +2745,7 @@ char *safe_arg(const char *opt, const char *arg)
|
||||
*t++ = '\\';
|
||||
while (*f) {
|
||||
if (*f == '\\') {
|
||||
if (!is_filename_arg || !strchr(WILD_CHARS, f[1]))
|
||||
if (!is_filename_arg || !(f[1] && strchr(WILD_CHARS, f[1])))
|
||||
*t++ = '\\';
|
||||
} else if (strchr(escapes, *f))
|
||||
*t++ = '\\';
|
||||
@@ -2622,7 +2785,10 @@ void server_options(char **args, int *argc_p)
|
||||
if (protect_args)
|
||||
argstr[x++] = 's';
|
||||
|
||||
for (i = 0; i < verbose; i++)
|
||||
/* `verbose` is unbounded (one increment per -v on our own command
|
||||
* line), so an uncapped loop walks past argstr[64]. Anything beyond
|
||||
* level ~5 is meaningless to the server anyway. */
|
||||
for (i = 0; i < verbose && i < 9; i++)
|
||||
argstr[x++] = 'v';
|
||||
|
||||
if (quiet && msgs2stderr)
|
||||
@@ -2899,6 +3065,11 @@ void server_options(char **args, int *argc_p)
|
||||
if (copy_unsafe_links)
|
||||
args[ac++] = "--copy-unsafe-links";
|
||||
|
||||
/* --insecure-links is NOT forwarded: it is a local-only opt-out. A daemon
|
||||
* governs its own confinement via the "insecure links" module parameter and
|
||||
* drops a connection that sends --insecure-links; a remote-shell peer that
|
||||
* wants it must be given it on its own side (e.g. via --rsync-path). */
|
||||
|
||||
if (safe_symlinks)
|
||||
args[ac++] = "--safe-links";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
TARGETS := all install install-ssl-daemon install-all install-strip conf gen reconfigure restatus \
|
||||
TARGETS := all install install-ssl-daemon install-all install-strip uninstall uninstall-ssl-daemon uninstall-all conf gen reconfigure restatus \
|
||||
proto man clean cleantests distclean test check check29 check30 installcheck splint \
|
||||
doxygen doxygen-upload finddead rrsync
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- /generated-files/
|
||||
- /binaries/
|
||||
@@ -1,6 +1,6 @@
|
||||
Summary: A fast, versatile, remote (and local) file-copying tool
|
||||
Name: rsync
|
||||
Version: 3.4.3
|
||||
Version: 3.5.0
|
||||
%define fullversion %{version}
|
||||
Release: 1
|
||||
%define srcdir src
|
||||
@@ -79,5 +79,5 @@ rm -rf $RPM_BUILD_ROOT
|
||||
%dir /etc/rsync-ssl/certs
|
||||
|
||||
%changelog
|
||||
* Wed May 20 2026 Rsync Project <rsync.project@gmail.com>
|
||||
Released 3.4.3.
|
||||
* Thu Aug 13 2026 Rsync Project <rsync.project@gmail.com>
|
||||
Released 3.5.0.
|
||||
+9
-2
@@ -206,7 +206,14 @@ def get_rsync_version():
|
||||
die("Unable to find RSYNC_VERSION define in version.h")
|
||||
|
||||
|
||||
def get_NEWS_version_info():
|
||||
def get_NEWS_version_info(skip_version=None):
|
||||
"""Return (last_version, its protocol version, {version: protocol-change date}).
|
||||
|
||||
skip_version lets the caller exclude the version it is about to release.
|
||||
Its NEWS entry may already carry a release date -- dated by hand, or by an
|
||||
earlier run of --step-3-tweak -- and would otherwise be reported as the
|
||||
PREVIOUS release, which is both wrong and fatal when it has no table row yet.
|
||||
"""
|
||||
rel_re = re.compile(r'^\| \S{2} \w{3} \d{4}\s+\|\s+(?P<ver>\d+\.\d+\.\d+)\s+\|\s+(?P<pdate>\d{2} \w{3} \d{4})?\s+\|\s+(?P<pver>\d+)\s+\|')
|
||||
last_version = last_protocol_version = None
|
||||
pdate = { }
|
||||
@@ -215,7 +222,7 @@ def get_NEWS_version_info():
|
||||
for line in fh:
|
||||
if not last_version: # Find the first non-dev|pre version with a release date.
|
||||
m = re.search(r'rsync (\d+\.\d+\.\d+) .*\d\d\d\d', line)
|
||||
if m:
|
||||
if m and m[1] != skip_version:
|
||||
last_version = m[1]
|
||||
m = rel_re.match(line)
|
||||
if m:
|
||||
|
||||
+40
-29
@@ -8,7 +8,7 @@
|
||||
# the rsync git checkout):
|
||||
#
|
||||
# ../release/rsync-ftp/ mirror of samba.org:/home/ftp/pub/rsync
|
||||
# ../release/rsync-html/ git checkout of rsync-web (the html site)
|
||||
# ../release/rsync-html/ release-time snapshot of the html site
|
||||
# ../release/work/ scratch space for tarball / diff staging
|
||||
# ../release/release-state.json info shared between steps
|
||||
#
|
||||
@@ -35,10 +35,11 @@ HTML_DIR = os.path.join(RELEASE_DIR, 'rsync-html')
|
||||
WORK_DIR = os.path.join(RELEASE_DIR, 'work')
|
||||
STATE_FILE = os.path.join(RELEASE_DIR, 'release-state.json')
|
||||
|
||||
# Local rsync-web checkout (sibling of rsync-git) is the source-of-truth for
|
||||
# the git-tracked html content. The maintainer pulls/commits/pushes there;
|
||||
# step-1-fetch just snapshots it into HTML_DIR for the release flow.
|
||||
HTML_SRC = os.path.realpath('../rsync-web')
|
||||
# The rsync-web/ subdirectory in the rsync source tree is the source-of-truth
|
||||
# for the git-tracked html content. step-1-fetch snapshots it into HTML_DIR
|
||||
# for the release flow, where it can be edited or augmented with server-side
|
||||
# content before step-11-push-html sends it to samba.org.
|
||||
HTML_SRC = os.path.realpath('rsync-web')
|
||||
|
||||
FTP_REMOTE_PATH = '/home/ftp/pub/rsync'
|
||||
HTML_REMOTE_PATH = '/home/httpd/html/rsync'
|
||||
@@ -60,7 +61,7 @@ GEN_FILES = [
|
||||
# ---------- Step registry ----------
|
||||
|
||||
STEPS = [
|
||||
('step-1-fetch', 'mirror ../release/rsync-ftp from samba.org and snapshot ../release/rsync-html from ../rsync-web'),
|
||||
('step-1-fetch', 'mirror ../release/rsync-ftp from samba.org and snapshot ../release/rsync-html from rsync-web/'),
|
||||
('step-2-prepare', 'gather release info interactively and write release-state.json'),
|
||||
('step-3-tweak', 'update version.h, rsync.h, NEWS.md, and packaging/*.spec'),
|
||||
('step-4-build', 'run smart-make + make gen'),
|
||||
@@ -103,8 +104,8 @@ def require_samba_host():
|
||||
def require_top_of_checkout():
|
||||
if not os.path.isfile('packaging/release.py'):
|
||||
die("Run this script from the top of your rsync checkout.")
|
||||
if not os.path.isdir('.git'):
|
||||
die("There is no .git dir in the current directory.")
|
||||
if not os.path.exists('.git'):
|
||||
die("There is no .git in the current directory (run from the top of a git checkout or worktree).")
|
||||
|
||||
|
||||
def replace_or_die(regex, repl, txt, die_msg):
|
||||
@@ -136,27 +137,29 @@ def step_1_fetch(args):
|
||||
section(f"Fetching ftp dir into {FTP_DIR}")
|
||||
if not os.path.isdir(FTP_DIR):
|
||||
os.makedirs(FTP_DIR)
|
||||
# The .filt file lives in the ftp dir on the server; mirror down using the
|
||||
# transmitted filter, falling back to no filter on the very first pull.
|
||||
# packaging/ftp.filt is the authoritative copy of the .filt filter file
|
||||
# that controls which subtrees rsync excludes from the FTP mirror.
|
||||
# Seed FTP_DIR/.filt from it so the bundled version is what step-1's
|
||||
# rsync uses here, and so step-10-push-ftp propagates it back to the
|
||||
# server. --exclude=/.filt below stops the server's copy from
|
||||
# overwriting our bundled one on the way down.
|
||||
filt = os.path.join(FTP_DIR, '.filt')
|
||||
if os.path.exists(filt):
|
||||
opts = ['-aivOHP', f'-f:_{filt}']
|
||||
else:
|
||||
opts = ['-aivOHP']
|
||||
cmd_chk(['rsync', *opts, f'{host}:{FTP_REMOTE_PATH}/', f'{FTP_DIR}/'])
|
||||
bundled_filt = os.path.realpath('packaging/ftp.filt')
|
||||
if not os.path.isfile(bundled_filt):
|
||||
die(f"{bundled_filt} not found; cannot seed .filt for the FTP pull.")
|
||||
shutil.copyfile(bundled_filt, filt)
|
||||
cmd_chk(['rsync', '-aivOHP', f'-f:_{filt}', '--exclude=/.filt',
|
||||
f'{host}:{FTP_REMOTE_PATH}/', f'{FTP_DIR}/'])
|
||||
|
||||
section(f"Snapshotting html dir from {HTML_SRC} into {HTML_DIR}")
|
||||
if not os.path.isdir(HTML_SRC):
|
||||
die(f"{HTML_SRC} not found. Clone the rsync-web repo there first.")
|
||||
if not os.path.isdir(os.path.join(HTML_SRC, '.git')):
|
||||
die(f"{HTML_SRC} exists but is not a git checkout.")
|
||||
print(f"(Make sure {HTML_SRC} is up to date — this script does not 'git pull' for you.)")
|
||||
die(f"{HTML_SRC} not found. This should be the in-tree rsync-web/ "
|
||||
f"subdirectory; something is wrong with your checkout.")
|
||||
os.makedirs(HTML_DIR, exist_ok=True)
|
||||
cmd_chk(['rsync', '-aiv', '--exclude=/.git',
|
||||
f'{HTML_SRC}/', f'{HTML_DIR}/'])
|
||||
cmd_chk(['rsync', '-aiv', f'{HTML_SRC}/', f'{HTML_DIR}/'])
|
||||
|
||||
# Then mirror non-git html content from the server (mirroring samba-rsync's
|
||||
# behavior: skip files that the html git already provides).
|
||||
# Then mirror non-git html content from the server, skipping files that
|
||||
# the html git already provides (driven by the 'filt' file in HTML_DIR).
|
||||
filt = os.path.join(HTML_DIR, 'filt')
|
||||
if os.path.exists(filt):
|
||||
tmp_filt = os.path.join(HTML_DIR, 'tmp-filt')
|
||||
@@ -186,7 +189,10 @@ def step_2_prepare(args):
|
||||
tz_num = tz_now[0:1].replace('+', '') + str(float(tz_now[1:3]) + float(tz_now[3:]) / 60)
|
||||
|
||||
curversion = get_rsync_version()
|
||||
lastversion, last_protocol_version, pdate = get_NEWS_version_info()
|
||||
# Skip the version we are releasing: its NEWS entry may already be dated,
|
||||
# in which case it would otherwise be taken for the previous release.
|
||||
lastversion, last_protocol_version, pdate = get_NEWS_version_info(
|
||||
skip_version=re.sub(r'(pre\d+|dev)$', '', curversion))
|
||||
protocol_version, subprotocol_version = get_protocol_versions()
|
||||
|
||||
# Default next version: bump preN, or move dev -> pre1.
|
||||
@@ -334,8 +340,11 @@ def step_3_tweak(args):
|
||||
f"Unable to find SUBPROTOCOL_VERSION in {fn}")
|
||||
elif fn == 'NEWS.md':
|
||||
efv = re.escape(finalversion)
|
||||
# Accept either "(UNRELEASED)" or an already-filled date, so a
|
||||
# release entry that was dated by hand (or by an earlier run of
|
||||
# this step) does not have to be reverted before releasing.
|
||||
x_re = re.compile(
|
||||
r'^# NEWS for rsync %s \(UNRELEASED\)\s+## Changes in this version:\n' % efv
|
||||
r'^# NEWS for rsync %s \((?:UNRELEASED|\d+ \w{3} \d{4})\)\s+## Changes in this version:\n' % efv
|
||||
+ r'(\n### PROTOCOL NUMBER:\s+- The protocol number was changed to \d+\.\n)?')
|
||||
rel_day = 'UNRELEASED' if pre else today
|
||||
repl = (f'# NEWS for rsync {finalversion} ({rel_day})\n\n'
|
||||
@@ -343,7 +352,8 @@ def step_3_tweak(args):
|
||||
if proto_changed:
|
||||
repl += f'\n### PROTOCOL NUMBER:\n\n - The protocol number was changed to {protocol_version}.\n'
|
||||
good_top = re.sub(r'\(.*?\)', '(UNRELEASED)', repl, 1)
|
||||
msg = (f"The top of {fn} is not in the right format. It should be:\n" + good_top)
|
||||
msg = (f"The top of {fn} is not in the right format. It should be:\n" + good_top
|
||||
+ "(an already-filled release date in place of UNRELEASED is also accepted)")
|
||||
txt = replace_or_die(x_re, repl, txt, msg)
|
||||
x_re = re.compile(
|
||||
r'^(\| )(\S{2} \S{3} \d{4})(\s+\|\s+%s\s+\| ).{11}(\s+\| )\S{2}(\s+\|+)$' % efv,
|
||||
@@ -631,9 +641,10 @@ If you have a 'samba' remote configured (git.samba.org:/data/git/rsync.git):
|
||||
git push samba {master_branch}
|
||||
git push samba {v_ver}
|
||||
|
||||
Then upload the tarball + .asc to the GitHub release for {v_ver}, run
|
||||
packaging/send-news (when convenient), and announce on rsync-announce@,
|
||||
rsync@, and Discord.
|
||||
Then upload the tarball + .asc to the GitHub release for {v_ver},
|
||||
and announce on rsync-announce@, rsync@, and Discord.
|
||||
|
||||
NOTE! Also update the PPAs if needed
|
||||
""")
|
||||
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This script makes it easy to update the ftp & html directories on the samba.org server.
|
||||
# It expects the 2 *_DEST directories to contain updated files that need to be sent to
|
||||
# the remote server. If these directories don't exist yet, they will be copied from the
|
||||
# remote server (while also making the html dir a git checkout).
|
||||
|
||||
FTP_SRC="$HOME/samba-rsync-ftp"
|
||||
HTML_SRC="$HOME/samba-rsync-html"
|
||||
|
||||
FTP_DEST="/home/ftp/pub/rsync"
|
||||
HTML_DEST="/home/httpd/html/rsync"
|
||||
|
||||
HTML_GIT='git.samba.org:/data/git/rsync-web.git'
|
||||
|
||||
export RSYNC_PARTIAL_DIR=''
|
||||
|
||||
case "$RSYNC_SAMBA_HOST" in
|
||||
*.samba.org) ;;
|
||||
*)
|
||||
echo "You must set RSYNC_SAMBA_HOST in your environment to the samba hostname to use." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
MODE=''
|
||||
REVERSE=''
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
-R|--reverse) REVERSE=yes ;;
|
||||
f|ftp) MODE=ftp ;;
|
||||
h|html) MODE=html ;;
|
||||
-h|--help)
|
||||
echo "Usage: [-R] [f|ftp|h|html]"
|
||||
echo "-R --reverse Copy the files from the server to the local host."
|
||||
echo " The default is to update the remote files."
|
||||
echo "-h --help Output this help message."
|
||||
echo " "
|
||||
echo "The script will prompt if ftp or html is not specified on the command line."
|
||||
echo "Only one category can be copied at a time. When pulling html files, a git"
|
||||
echo "checkout will be either created or updated prior to the rsync copy."
|
||||
exit
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
while [ ! "$MODE" ]; do
|
||||
if [ "$REVERSE" = yes ]; then
|
||||
DIRECTION=FROM
|
||||
else
|
||||
DIRECTION=TO
|
||||
fi
|
||||
echo -n "Copy which files $DIRECTION the server? ftp or html? "
|
||||
read ans
|
||||
case "$ans" in
|
||||
f*) MODE=ftp ;;
|
||||
h*) MODE=html ;;
|
||||
'') exit 1 ;;
|
||||
*) echo "You must answer f or h to copy the ftp or html data." ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$MODE" = ftp ]; then
|
||||
SRC_DIR="$FTP_SRC"
|
||||
DEST_DIR="$FTP_DEST"
|
||||
FILT=".filt"
|
||||
else
|
||||
SRC_DIR="$HTML_SRC"
|
||||
DEST_DIR="$HTML_DEST"
|
||||
FILT="filt"
|
||||
fi
|
||||
|
||||
function do_rsync {
|
||||
rsync --dry-run "${@}" | grep -v 'is uptodate$'
|
||||
echo ''
|
||||
echo -n "Run without --dry-run? [n] "
|
||||
read ans
|
||||
case "$ans" in
|
||||
y*) rsync "${@}" | grep -v 'is uptodate$' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ -d "$SRC_DIR" ]; then
|
||||
REVERSE_RSYNC=do_rsync
|
||||
else
|
||||
echo "The directory $SRC_DIR does not exist yet."
|
||||
echo -n "Do you want to create it? [n] "
|
||||
read ans
|
||||
case "$ans" in
|
||||
y*) ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
REVERSE=yes
|
||||
REVERSE_RSYNC=rsync
|
||||
fi
|
||||
|
||||
if [ "$REVERSE" = yes ]; then
|
||||
OPTS='-aivOHP'
|
||||
TMP_FILT="$SRC_DIR/tmp-filt"
|
||||
echo "Copying files from $RSYNC_SAMBA_HOST to $SRC_DIR ..."
|
||||
if [ "$MODE" = html ]; then
|
||||
if [ $REVERSE_RSYNC = rsync ]; then
|
||||
git clone "$HTML_GIT" "$SRC_DIR" || exit 1
|
||||
else
|
||||
cd "$SRC_DIR" || exit 1
|
||||
git pull || exit 1
|
||||
fi
|
||||
sed -n -e 's/[-P]/H/p' "$SRC_DIR/$FILT" >"$TMP_FILT"
|
||||
OPTS="${OPTS}f._$TMP_FILT"
|
||||
else
|
||||
OPTS="${OPTS}f:_$FILT"
|
||||
fi
|
||||
$REVERSE_RSYNC "$OPTS" "$RSYNC_SAMBA_HOST:$DEST_DIR/" "$SRC_DIR/"
|
||||
rm -f "$TMP_FILT"
|
||||
exit
|
||||
fi
|
||||
|
||||
cd "$SRC_DIR" || exit 1
|
||||
echo "Copying files from $SRC_DIR to $RSYNC_SAMBA_HOST ..."
|
||||
do_rsync -aivOHP --chown=:rsync --del -f._$FILT . "$RSYNC_SAMBA_HOST:$DEST_DIR/"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
# This script expects the ~/src/rsync directory to contain the rsync
|
||||
# source that has been updated. It also expects the auto-build-save
|
||||
# directory to have been created prior to the running of configure so
|
||||
# that each branch has its own build directory underneath. This supports
|
||||
# the maintainer workflow for the rsync-patches files maintenace.
|
||||
|
||||
FTP_SRC="$HOME/samba-rsync-ftp"
|
||||
FTP_DEST="/home/ftp/pub/rsync"
|
||||
MD_FILES="README.md INSTALL.md NEWS.md"
|
||||
|
||||
case "$RSYNC_SAMBA_HOST" in
|
||||
*.samba.org) ;;
|
||||
*)
|
||||
echo "You must set RSYNC_SAMBA_HOST in your environment to the samba hostname to use." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -d "$FTP_SRC" ]; then
|
||||
packaging/samba-rsync ftp # Ask to initialize the local ftp dir
|
||||
fi
|
||||
|
||||
cd ~/src/rsync
|
||||
|
||||
make man
|
||||
./md-convert --dest="$FTP_SRC" $MD_FILES
|
||||
rsync -aiic $MD_FILES auto-build-save/master/*.?.html "$FTP_SRC"
|
||||
|
||||
cd "$FTP_SRC"
|
||||
|
||||
rsync -aiic README.* INSTALL.* NEWS.* *.?.html "$RSYNC_SAMBA_HOST:$FTP_DEST/"
|
||||
@@ -580,7 +580,14 @@ static FILE *OpenConfFile( char *FileName )
|
||||
return( NULL );
|
||||
}
|
||||
|
||||
OpenedFile = fopen( FileName, "r" );
|
||||
/* rsyncd.conf path (--config or default): a planted symlink could redirect
|
||||
* the daemon's config read. Refuse symlinks not owned by uid 0 or euid. */
|
||||
{
|
||||
int cfg_fd = open_no_attacker_symlinks( FileName, O_RDONLY, 0 );
|
||||
OpenedFile = cfg_fd >= 0 ? fdopen( cfg_fd, "r" ) : NULL;
|
||||
if( !OpenedFile && cfg_fd >= 0 )
|
||||
close( cfg_fd );
|
||||
}
|
||||
if( NULL == OpenedFile )
|
||||
{
|
||||
rsyserr(FLOG, errno, "unable to open config file \"%s\"",
|
||||
|
||||
+373
-23
@@ -25,6 +25,7 @@
|
||||
extern int dry_run;
|
||||
extern int do_xfers;
|
||||
extern int am_root;
|
||||
extern int am_daemon;
|
||||
extern int am_server;
|
||||
extern int inc_recurse;
|
||||
extern int log_before_transfer;
|
||||
@@ -71,6 +72,7 @@ extern int fuzzy_basis;
|
||||
extern struct name_num_item *xfer_sum_nni;
|
||||
extern int xfer_sum_len;
|
||||
extern int use_secure_symlinks;
|
||||
extern int operator_path_resolve;
|
||||
|
||||
static struct bitbag *delayed_bits = NULL;
|
||||
static int phase = 0, redoing = 0;
|
||||
@@ -83,6 +85,207 @@ static int updating_basis_or_equiv;
|
||||
#define MAX_UNIQUE_NUMBER 999999
|
||||
#define MAX_UNIQUE_LOOP 100
|
||||
|
||||
/* Open a basis/output path that may legitimately be an operator-trusted
|
||||
* ABSOLUTE path -- e.g. an absolute --partial-dir ("a directory reserved for
|
||||
* partial-dir work") or --backup-dir. secure_relative_open() deliberately
|
||||
* rejects an absolute relpath, so feeding it the whole absolute partialptr
|
||||
* (with a NULL basedir) returns EINVAL: the basis fd is then -1, no basis is
|
||||
* mapped, and receive_data() omits every matched block from the whole-file
|
||||
* verification checksum -> a spurious "failed verification" that strands the
|
||||
* (correct) data in the partial-dir forever.
|
||||
*
|
||||
* The operator's directory is trusted; only the leaf basename is peer-supplied.
|
||||
* So when basedir is NULL and relpath is absolute, split it into its directory
|
||||
* (trusted) and leaf and confine just the leaf -- exactly how secure_relative_
|
||||
* open already trusts an absolute basedir while O_NOFOLLOW-confining the leaf.
|
||||
* Anything else is a straight pass-through that preserves the strict contract. */
|
||||
static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
{
|
||||
extern int am_daemon, am_chrooted;
|
||||
extern unsigned int module_dirlen;
|
||||
|
||||
/* "insecure links = yes": restore the 3.2.7 plain open so an operator/peer
|
||||
* alt-dest basis follows symlinks like legacy rsync, the same opt-out the
|
||||
* other daemon symlink sites honour. */
|
||||
if (symlink_optout_allowed()) {
|
||||
if (basedir) {
|
||||
char fullpath[MAXPATHLEN];
|
||||
if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return do_open(fullpath, flags, mode);
|
||||
}
|
||||
return do_open(relpath, flags, mode);
|
||||
}
|
||||
|
||||
/* A peer-supplied --partial-dir basis/staging path (operator_path_resolve set
|
||||
* by recv_files) may be absolute (module_dir-prefixed on a non-chroot daemon)
|
||||
* and traverse a symlink the secure_relative_open path can't confine: resolve
|
||||
* it with the ownership walk, which follows a uid0/euid-owned symlink but
|
||||
* refuses a foreign one AND (via abspath_excluded_by_module) refuses a target
|
||||
* the module's exclude hides -- closing the partial-dir exclude bypass. */
|
||||
if (operator_path_resolve) {
|
||||
char fullpath[MAXPATHLEN];
|
||||
const char *p = relpath;
|
||||
if (basedir) {
|
||||
if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
p = fullpath;
|
||||
}
|
||||
return open_no_attacker_symlinks(p, flags, mode);
|
||||
}
|
||||
|
||||
/* The confined resolver is needed for the sanitizing daemon
|
||||
* (am_daemon && !am_chrooted) and for a /./ inner-module chroot
|
||||
* (am_chrooted && module_dirlen) -- in the latter the kernel chroot confines
|
||||
* only the outer path, so a peer-chosen alt-dest basis index (fnamecmp_type)
|
||||
* could otherwise reach an outside-inner-module file through a symlinked
|
||||
* parent. Local / remote-shell mode has no module boundary, and a plain
|
||||
* "use chroot = yes" makes the kernel root the boundary, so there an alt-dest
|
||||
* basis like --link-dest=../01 must resolve against the cwd as a bare open did
|
||||
* before the hardening (confining it would reject the legitimate sibling
|
||||
* "..", #915). The re-anchoring in secure_relative_open() covers the
|
||||
* in-module ".." climb for the inner-module case too. */
|
||||
if (!am_daemon || (am_chrooted && !module_dirlen)) {
|
||||
if (basedir) {
|
||||
char fullpath[MAXPATHLEN];
|
||||
if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return do_open(fullpath, flags, mode);
|
||||
}
|
||||
return do_open(relpath, flags, mode);
|
||||
}
|
||||
|
||||
if (!basedir && relpath && *relpath == '/') {
|
||||
const char *slash = strrchr(relpath, '/');
|
||||
const char *leaf = slash + 1;
|
||||
char dirbuf[MAXPATHLEN];
|
||||
const char *dir;
|
||||
if (slash == relpath)
|
||||
dir = "/";
|
||||
else {
|
||||
size_t dlen = slash - relpath;
|
||||
if (dlen >= sizeof dirbuf) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
memcpy(dirbuf, relpath, dlen);
|
||||
dirbuf[dlen] = '\0';
|
||||
dir = dirbuf;
|
||||
}
|
||||
return secure_relative_open(dir, leaf, flags, mode);
|
||||
}
|
||||
return secure_relative_open(basedir, relpath, flags, mode);
|
||||
}
|
||||
|
||||
/* Keep the ownership policy for every attempt to open a one-inplace partial
|
||||
* output. In particular, Linux's protected_regular compatibility retries
|
||||
* must not downgrade an operator-path open to the ordinary path resolver. */
|
||||
static int secure_recv_open(const char *path, int flags, mode_t mode, int owner_walk)
|
||||
{
|
||||
int fd, save = operator_path_resolve;
|
||||
|
||||
if (owner_walk)
|
||||
operator_path_resolve = 1;
|
||||
fd = secure_basis_open(NULL, path, flags, mode);
|
||||
operator_path_resolve = save;
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* Open a read-only regular file for an in-place update without leaving its
|
||||
* mode relaxed while protocol data is received. Once a writable descriptor is
|
||||
* open, later writes no longer depend on the pathname's permission bits, so
|
||||
* restore the exact prior mode before returning to the transfer loop -- an
|
||||
* abort (peer EOF, checksum failure, a signal) must not strand the file at
|
||||
* 0600. Requires a regular file: O_NOFOLLOW refuses a symlink at the leaf but
|
||||
* not a directory swapped in after the type probe. */
|
||||
static int open_readonly_inplace(const char *fname, int one_inplace)
|
||||
{
|
||||
STRUCT_STAT cst;
|
||||
mode_t prior_mode;
|
||||
int cfd = -1, fd = -1;
|
||||
int open_errno, restore_errno;
|
||||
|
||||
if (use_secure_symlinks || one_inplace) {
|
||||
#ifdef O_NOFOLLOW
|
||||
cfd = secure_recv_open(fname, O_RDONLY|O_NOFOLLOW, 0, one_inplace);
|
||||
if (cfd < 0)
|
||||
goto failed;
|
||||
if (do_fstat(cfd, &cst) < 0 || !S_ISREG(cst.st_mode)) {
|
||||
errno = EACCES; /* refused: not the read-only regular file we recover */
|
||||
goto failed;
|
||||
}
|
||||
prior_mode = cst.st_mode & CHMOD_BITS;
|
||||
if (prior_mode & S_IWUSR) {
|
||||
/* Already owner-writable, so adding S_IWUSR cannot be what an
|
||||
* EACCES is about (an ACL, or the parent dir). Don't touch the
|
||||
* mode for nothing: each chmod risks losing a special bit. */
|
||||
errno = EACCES;
|
||||
goto failed;
|
||||
}
|
||||
if (fchmod(cfd, prior_mode | S_IWUSR) < 0)
|
||||
goto failed;
|
||||
fd = secure_recv_open(fname, O_WRONLY, 0600, one_inplace);
|
||||
open_errno = errno;
|
||||
if (fchmod(cfd, prior_mode) < 0) {
|
||||
restore_errno = errno;
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
fd = -1;
|
||||
open_errno = restore_errno;
|
||||
}
|
||||
close(cfd);
|
||||
errno = open_errno;
|
||||
return fd;
|
||||
#else
|
||||
/* Without O_NOFOLLOW the resolver's oldest fallback would follow a
|
||||
* raced symlink, so fail closed rather than chmod through it. */
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Local and chrooted transfers retain the existing pathname semantics.
|
||||
* Note the S_ISREG test here is a type check on a stable path, NOT race
|
||||
* protection: do_stat() follows a leaf symlink and each call below
|
||||
* re-resolves the name. The fd-based branch above is the one that
|
||||
* pins an inode; a chroot is what confines this one. */
|
||||
if (do_stat(fname, &cst) < 0) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
if (!S_ISREG(cst.st_mode) || (cst.st_mode & CHMOD_BITS & S_IWUSR)) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
prior_mode = cst.st_mode & CHMOD_BITS;
|
||||
if (do_chmod_at(fname, prior_mode | S_IWUSR) < 0)
|
||||
return -1;
|
||||
fd = do_open(fname, O_WRONLY, 0600);
|
||||
open_errno = errno;
|
||||
if (do_chmod_at(fname, prior_mode) < 0) {
|
||||
restore_errno = errno;
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
fd = -1;
|
||||
open_errno = restore_errno;
|
||||
}
|
||||
errno = open_errno;
|
||||
return fd;
|
||||
|
||||
failed:
|
||||
open_errno = errno;
|
||||
if (cfd >= 0)
|
||||
close(cfd);
|
||||
errno = open_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* get_tmpname() - create a tmp filename for a given filename
|
||||
*
|
||||
* If a tmpdir is defined, use that as the directory to put it in. Otherwise,
|
||||
@@ -215,11 +418,23 @@ int open_tmpfile(char *fnametmp, const char *fname, struct file_struct *file)
|
||||
* access to ensure that there is no race condition. They will be
|
||||
* correctly updated after the right owner and group info is set.
|
||||
* (Thanks to snabb@epipe.fi for pointing this out.) */
|
||||
/* When use_secure_symlinks is on (non-chroot daemon with munge_symlinks),
|
||||
* use secure_mkstemp to prevent symlink race attacks on parent directories. */
|
||||
if (use_secure_symlinks)
|
||||
fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
|
||||
else
|
||||
/* For any non-chrooted receiver (secure_relpath_active()), create the
|
||||
* temp file securely so a parent-symlink race can't redirect it. When
|
||||
* the temp lives in the entry's own dir (the common case, no --temp-dir)
|
||||
* use the cached held dir fd; otherwise fall back to secure_mkstemp. An
|
||||
* operator-supplied --temp-dir (tmpdir) gets the ownership-walk resolver
|
||||
* (it may legitimately point outside the tree); the deep-entry-dir fallback,
|
||||
* when the held-dirfd cache declines, gets the strict transfer-path one. */
|
||||
if (secure_relpath_active()) {
|
||||
int dfd = held_dfd_for(fnametmp, file);
|
||||
if (dfd >= 0) {
|
||||
char *slash = strrchr(fnametmp, '/');
|
||||
fd = do_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp,
|
||||
(file->mode|added_perms) & INITACCESSPERMS);
|
||||
} else
|
||||
fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS,
|
||||
tmpdir != NULL);
|
||||
} else
|
||||
fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
|
||||
|
||||
#if 0
|
||||
@@ -364,6 +579,34 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
|
||||
|
||||
stats.matched_data += len;
|
||||
|
||||
/* A block match with no mapped basis is a protocol inconsistency
|
||||
* ONLY when we are actually producing output (fd != -1): the
|
||||
* generator told the sender a basis existed but the receiver could
|
||||
* not open it, so honoring the match would silently omit these
|
||||
* bytes from the verification checksum (a spurious failure) or
|
||||
* leave a hole in the output. Fail cleanly in that case.
|
||||
*
|
||||
* On the DISCARD path (fd == -1, fname == NULL) there is no output
|
||||
* and no verification: discard_receive_data() deliberately drains a
|
||||
* delta the receiver never intends to write (basis fstat failed,
|
||||
* basis is a directory, output open failed, batch skip, ...). The
|
||||
* sender does not know the data is being discarded and streams an
|
||||
* ordinary delta, so a match token here is NORMAL protocol, not
|
||||
* malformed. Absorb it benignly (advance the offset and continue),
|
||||
* as the pre-existing "if (mapbuf)" guards did before this check was
|
||||
* added in 31fbb17d -- erroring would wrongly break legitimate
|
||||
* transfers, and full_fname(fname) with fname==NULL would
|
||||
* dereference NULL (a receiver crash on a normal transfer). */
|
||||
if (!mapbuf) {
|
||||
if (fd != -1) {
|
||||
rprintf(FERROR, "got a block match with no basis file for %s [%s]\n",
|
||||
full_fname(fname), who_am_i());
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
offset += len;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DEBUG_GTE(DELTASUM, 3)) {
|
||||
rprintf(FINFO,
|
||||
"chunk[%d] of size %ld at %s offset=%s%s\n",
|
||||
@@ -455,8 +698,15 @@ static void handle_delayed_updates(char *local_name)
|
||||
partialptr, fname);
|
||||
}
|
||||
/* We don't use robust_rename() here because the
|
||||
* partial-dir must be on the same drive. */
|
||||
if (do_rename_at(partialptr, fname) < 0) {
|
||||
* partial-dir must be on the same drive. Resolve the
|
||||
* --partial-dir source through the exclude-aware ownership
|
||||
* walk so a symlinked partial-dir can't move a file out of
|
||||
* an excluded subtree. */
|
||||
int rret;
|
||||
operator_path_resolve = 1;
|
||||
rret = do_rename_at(partialptr, fname);
|
||||
operator_path_resolve = 0;
|
||||
if (rret < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"rename failed for %s (from %s)",
|
||||
full_fname(fname), partialptr);
|
||||
@@ -561,7 +811,8 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
#ifdef SUPPORT_ACLS
|
||||
const char *parent_dirname = "";
|
||||
#endif
|
||||
int ndx, recv_ok, one_inplace;
|
||||
int ndx, recv_ok, one_inplace, write_to_device;
|
||||
mode_t write_devices_saved_mode = 0;
|
||||
|
||||
if (DEBUG_GTE(RECV, 1))
|
||||
rprintf(FINFO, "recv_files(%d) starting\n", cur_flist->used);
|
||||
@@ -612,10 +863,22 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
|
||||
if (ndx - cur_flist->ndx_start >= 0)
|
||||
file = cur_flist->files[ndx - cur_flist->ndx_start];
|
||||
else if (cur_flist->parent_ndx < 0)
|
||||
else if (cur_flist->parent_ndx < 0
|
||||
|| cur_flist->parent_ndx >= dir_flist->used)
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
else
|
||||
file = dir_flist->files[cur_flist->parent_ndx];
|
||||
if (!F_IS_ACTIVE(file)) {
|
||||
/* A peer that sends duplicate file-list entries gets
|
||||
* one of them clear_file()'d by flist_sort_and_clean();
|
||||
* referencing that slot here yields fname == NULL and
|
||||
* a crash in the first deref (daemon filter check,
|
||||
* set_file_attrs → full_fname, …). */
|
||||
rprintf(FERROR,
|
||||
"rsync: refusing transfer of cleared file index %d\n",
|
||||
ndx);
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
fname = local_name ? local_name : f_name(file, fbuf);
|
||||
|
||||
if (DEBUG_GTE(RECV, 1))
|
||||
@@ -793,8 +1056,55 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
fnamecmp = fname;
|
||||
}
|
||||
|
||||
/* open the file */
|
||||
fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
/* Open the delta basis. When it lives in the entry's own dir (no
|
||||
* alternate basedir), read it via the held dir fd with O_NOFOLLOW: a
|
||||
* regular-file basis is never legitimately a symlink, and refusing a
|
||||
* planted one avoids both an escape and a basis-content info-leak.
|
||||
* Otherwise use secure_basis_open, which also tolerates an operator-
|
||||
* trusted absolute fnamecmp (e.g. an absolute --partial-dir basis). */
|
||||
{
|
||||
int bdfd;
|
||||
if (fnamecmp_type == FNAMECMP_PARTIAL_DIR
|
||||
&& fnamecmp && *fnamecmp != '/') {
|
||||
/* The relative partial path contains peer-derived directory
|
||||
* components. It is not an operator-trusted path as a whole. */
|
||||
fd1 = secure_relative_open(NULL, fnamecmp, O_RDONLY, 0);
|
||||
} else if (!basedir && (bdfd = held_dfd_for(fnamecmp, file)) >= 0) {
|
||||
const char *slash;
|
||||
assert(fnamecmp != NULL); /* set on every path above */
|
||||
slash = strrchr(fnamecmp, '/');
|
||||
fd1 = do_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0);
|
||||
} else {
|
||||
/* An operator-supplied basis -- a --partial-dir, or an
|
||||
* alt-dest basedir (--copy-dest/--compare-dest/--link-dest) --
|
||||
* is a peer/operator path: resolve it with the exclude-aware
|
||||
* ownership walk so a flipped foreign-owned parent symlink can't
|
||||
* read (and feed back as delta) an out-of-tree / excluded file.
|
||||
* The walk still allows the legitimate "../sibling" basis (#915)
|
||||
* and the operator's own uid0/euid symlinks. A daemon keeps its
|
||||
* stronger confinement branch in secure_basis_open(), so only
|
||||
* route the alt-dest basedir read through the walk off-daemon. */
|
||||
if ((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR)
|
||||
operator_path_resolve = 1;
|
||||
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
operator_path_resolve = 0;
|
||||
}
|
||||
}
|
||||
if (fnamecmp_type == FNAMECMP_PARTIAL_DIR && fd1 == -1) {
|
||||
/* The sender may claim a partial basis even when the generator's
|
||||
* confined lookup rejected it. Drop that path as the delta basis
|
||||
* and in-place output target. A daemon that negotiated in-place
|
||||
* partial updates rejects the unsafe peer option; otherwise (a pre-30
|
||||
* peer, where no in-place partial redirect is possible, or a pull
|
||||
* client) fall back to a safe no-basis update. */
|
||||
if (am_daemon && inplace_partial) {
|
||||
rprintf(FERROR,
|
||||
"rsync: refusing unconfined partial basis for %s\n", fname);
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
fnamecmp = fname;
|
||||
fnamecmp_type = FNAMECMP_FNAME;
|
||||
}
|
||||
|
||||
if (fd1 == -1 && protocol_version < 29) {
|
||||
if (fnamecmp != fname) {
|
||||
@@ -808,7 +1118,10 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
basedir = basis_dir[0];
|
||||
fnamecmp = fname;
|
||||
fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
|
||||
fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
if (!am_daemon)
|
||||
operator_path_resolve = 1;
|
||||
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
operator_path_resolve = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,7 +1132,10 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
fnamecmp = fnamecmpbuf;
|
||||
}
|
||||
|
||||
one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR;
|
||||
/* A peer's basis selector cannot enable direct output through a path
|
||||
* that the confined basis open did not validate. */
|
||||
one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR
|
||||
&& fd1 != -1;
|
||||
updating_basis_or_equiv = one_inplace
|
||||
|| (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP));
|
||||
|
||||
@@ -851,11 +1167,10 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (write_devices && IS_DEVICE(st.st_mode)) {
|
||||
write_to_device = write_devices && IS_DEVICE(st.st_mode);
|
||||
if (write_to_device) {
|
||||
if (fd1 != -1 && st.st_size == 0)
|
||||
st.st_size = get_device_size(fd1, fname);
|
||||
/* Mark the file entry as a device so that we don't try to truncate it later on. */
|
||||
file->mode = S_IFBLK | (file->mode & ACCESSPERMS);
|
||||
} else if (fd1 != -1 && !(S_ISREG(st.st_mode))) {
|
||||
close(fd1);
|
||||
fd1 = -1;
|
||||
@@ -879,23 +1194,34 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
/* We now check to see if we are writing the file "inplace" */
|
||||
if (inplace || one_inplace) {
|
||||
fnametmp = one_inplace ? partialptr : fname;
|
||||
/* When use_secure_symlinks is on (non-chroot daemon),
|
||||
/* For any non-chrooted receiver (secure_relpath_active()),
|
||||
* use secure open to prevent symlink race attacks where an
|
||||
* attacker could switch a directory to a symlink between
|
||||
* path validation and file open. */
|
||||
if (use_secure_symlinks)
|
||||
fd2 = secure_relative_open(NULL, fnametmp, O_WRONLY|O_CREAT, 0600);
|
||||
/* one_inplace stages into the operator/peer --partial-dir path:
|
||||
* resolve it with the ownership walk (exclude-aware) so it can't be
|
||||
* redirected through a symlink into an excluded subtree. */
|
||||
if (secure_relpath_active())
|
||||
fd2 = secure_recv_open(fnametmp, O_WRONLY|O_CREAT, 0600,
|
||||
one_inplace);
|
||||
else
|
||||
fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
|
||||
#ifdef linux
|
||||
if (fd2 == -1 && errno == EACCES) {
|
||||
/* Maybe the error was due to protected_regular setting? */
|
||||
if (use_secure_symlinks)
|
||||
fd2 = secure_relative_open(NULL, fname, O_WRONLY, 0600);
|
||||
if (use_secure_symlinks || one_inplace)
|
||||
fd2 = secure_recv_open(fnametmp, O_WRONLY, 0600,
|
||||
one_inplace);
|
||||
else
|
||||
fd2 = do_open(fname, O_WRONLY, 0600);
|
||||
fd2 = do_open(fnametmp, O_WRONLY, 0600);
|
||||
}
|
||||
#endif
|
||||
if (fd2 == -1 && errno == EACCES) {
|
||||
/* Temporarily add owner-write access only long enough to open
|
||||
* a writable descriptor; the helper restores the old mode
|
||||
* before any network data is consumed, including on failure. */
|
||||
fd2 = open_readonly_inplace(fnametmp, one_inplace);
|
||||
}
|
||||
if (fd2 == -1) {
|
||||
rsyserr(FERROR_XFER, errno, "open %s failed",
|
||||
full_fname(fnametmp));
|
||||
@@ -923,9 +1249,27 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
else if (!am_server && INFO_GTE(NAME, 1) && INFO_EQ(PROGRESS, 1))
|
||||
rprintf(FINFO, "%s\n", fname);
|
||||
|
||||
/* --write-devices writes a regular source file's content into an
|
||||
* existing destination device. Flip file->mode to S_IFBLK just for
|
||||
* receive_data()'s ftruncate gate (!IS_DEVICE), then restore it right
|
||||
* after -- the file_struct was built as a regular file with no
|
||||
* DEV_EXTRA_CNT, so leaving it S_IFBLK would make set_stat_xattr's
|
||||
* F_RDEV_P() read past the allocation. Kept tight here (after the
|
||||
* pre-transfer log and the fd2 bail-out) so no continue skips the
|
||||
* restore and dest_mode() above ran on the real mode. */
|
||||
if (write_to_device) {
|
||||
write_devices_saved_mode = file->mode;
|
||||
file->mode = S_IFBLK | (file->mode & ACCESSPERMS);
|
||||
}
|
||||
|
||||
/* recv file data */
|
||||
recv_ok = receive_data(f_in, fnamecmp, fd1, st.st_size, fname, fd2, file, inplace || one_inplace);
|
||||
|
||||
if (write_to_device) {
|
||||
file->mode = write_devices_saved_mode;
|
||||
write_devices_saved_mode = 0;
|
||||
}
|
||||
|
||||
log_item(log_code, file, iflags, NULL);
|
||||
if (want_progress_now)
|
||||
instant_progress(fname);
|
||||
@@ -944,8 +1288,14 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
if (!finish_transfer(fname, fnametmp, fnamecmp, partialptr, file, recv_ok, 1))
|
||||
recv_ok = -1;
|
||||
else if (fnamecmp == partialptr) {
|
||||
if (!one_inplace)
|
||||
if (!one_inplace) {
|
||||
/* Unlink the consumed --partial-dir basis through the
|
||||
* exclude-aware ownership walk (a symlinked partial-dir
|
||||
* must not delete a file in an excluded subtree). */
|
||||
operator_path_resolve = 1;
|
||||
do_unlink_at(partialptr);
|
||||
operator_path_resolve = 0;
|
||||
}
|
||||
handle_partial_dir(partialptr, PDIR_DELETE);
|
||||
}
|
||||
} else if (keep_partial && partialptr && (!one_inplace || delay_updates)) {
|
||||
|
||||
@@ -26,7 +26,7 @@ function rsync_ssl_run {
|
||||
;;
|
||||
esac
|
||||
|
||||
exec rsync --rsh="$0 --HELPER" "${@}"
|
||||
exec rsync --rsh="'$0' --HELPER" "${@}"
|
||||
}
|
||||
|
||||
function rsync_ssl_helper {
|
||||
@@ -90,6 +90,9 @@ function rsync_ssl_helper {
|
||||
# openssl:
|
||||
caopt="-verify_return_error -verify 4"
|
||||
# gnutls:
|
||||
# gnutls-cli has no reliable "use the system trust store AND make
|
||||
# verification fatal" switch across versions, so the gnutls path
|
||||
# refuses below unless RSYNC_SSL_ALLOW_INSECURE_GNUTLS is set.
|
||||
gnutls_opts=""
|
||||
# stunnel:
|
||||
# Since there is no way of using the default CA certificate collection,
|
||||
@@ -135,26 +138,59 @@ function rsync_ssl_helper {
|
||||
echo "Usage: rsync-ssl --HELPER HOSTNAME rsync --server --daemon ." 1>&2
|
||||
exit 1
|
||||
fi
|
||||
validate_ssl_hostname "$hostname"
|
||||
|
||||
if [[ $RSYNC_SSL_TYPE == stunnel && -z ${RSYNC_SSL_CA_CERT+x} && "$RSYNC_SSL_ALLOW_INSECURE_STUNNEL" != 1 ]]; then
|
||||
echo "stunnel requires RSYNC_SSL_CA_CERT for server certificate validation (or set RSYNC_SSL_ALLOW_INSECURE_STUNNEL=1 to opt out)" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Bind the server certificate to the requested host (openssl -verify_hostname,
|
||||
# stunnel checkHost) so a cert that is CA-valid for a *different* name can't be
|
||||
# used to MITM. Set RSYNC_SSL_SKIP_HOSTNAME_CHECK=1 to keep CA chain
|
||||
# verification but skip the identity check (e.g. bare-IP or internal-CA setups).
|
||||
# checkHost only applies while the chain is being verified.
|
||||
openssl_host_opt="-verify_hostname $hostname"
|
||||
stunnel_host_opt=""
|
||||
[[ "$verify" == "verifyChain = yes" ]] && stunnel_host_opt="checkHost = $hostname"
|
||||
if [[ "$RSYNC_SSL_SKIP_HOSTNAME_CHECK" == 1 ]]; then
|
||||
openssl_host_opt=""
|
||||
stunnel_host_opt=""
|
||||
fi
|
||||
|
||||
if [[ $RSYNC_SSL_TYPE == gnutls && -z ${RSYNC_SSL_CA_CERT+x} && "$RSYNC_SSL_ALLOW_INSECURE_GNUTLS" != 1 ]]; then
|
||||
echo "gnutls-cli requires RSYNC_SSL_CA_CERT for server certificate validation (or set RSYNC_SSL_ALLOW_INSECURE_GNUTLS=1 to opt out)" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $RSYNC_SSL_TYPE == openssl ]]; then
|
||||
exec $RSYNC_SSL_OPENSSL s_client $caopt $certopt $keyopt -quiet -verify_quiet -servername $hostname -verify_hostname $hostname -connect $hostname:$port
|
||||
exec "$RSYNC_SSL_OPENSSL" s_client $caopt $certopt $keyopt -quiet -verify_quiet -servername $hostname $openssl_host_opt -connect $hostname:$port
|
||||
elif [[ $RSYNC_SSL_TYPE == gnutls ]]; then
|
||||
exec $RSYNC_SSL_GNUTLS --logfile=/dev/null $gnutls_cert_opt $gnutls_key_opt $gnutls_opts $hostname:$port
|
||||
exec "$RSYNC_SSL_GNUTLS" --logfile=/dev/null $gnutls_cert_opt $gnutls_key_opt $gnutls_opts $hostname:$port
|
||||
else
|
||||
# devzero@web.de came up with this no-tmpfile calling syntax:
|
||||
exec $RSYNC_SSL_STUNNEL -fd 10 11<&0 <<EOF 10<&0 0<&11 11<&-
|
||||
exec "$RSYNC_SSL_STUNNEL" -fd 10 11<&0 <<EOF 10<&0 0<&11 11<&-
|
||||
foreground = yes
|
||||
debug = crit
|
||||
connect = $hostname:$port
|
||||
client = yes
|
||||
TIMEOUTclose = 0
|
||||
$verify
|
||||
$stunnel_host_opt
|
||||
$certopt
|
||||
$cafile
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
function validate_ssl_hostname {
|
||||
local host="$1"
|
||||
if [[ -z "$host" || "$host" == -* || "$host" =~ [^A-Za-z0-9._:-] ]]; then
|
||||
echo "invalid rsync-ssl hostname: $host" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function path_search {
|
||||
IFS_SAVE="$IFS"
|
||||
IFS=:
|
||||
|
||||
+22
-1
@@ -66,7 +66,28 @@ The ssl helper scripts are affected by the following environment variables:
|
||||
0. `RSYNC_SSL_CA_CERT`
|
||||
|
||||
If specified, the value is a filename that contains a certificate authority
|
||||
certificate that is used to validate the connection.
|
||||
certificate that is used to validate the connection. When set, the server
|
||||
certificate is verified against this CA **and** its name is checked against
|
||||
the host you are connecting to (the chain and the identity), for all of the
|
||||
openssl, gnutls, and stunnel backends. Set it to an empty string to disable
|
||||
certificate validation entirely (an encrypt-only connection).
|
||||
|
||||
0. `RSYNC_SSL_ALLOW_INSECURE_STUNNEL`
|
||||
|
||||
Set to `1` to allow the stunnel backend to run without a CA certificate (and
|
||||
thus with no server-certificate validation at all). Without this, stunnel
|
||||
mode refuses to start unless `RSYNC_SSL_CA_CERT` is set, since an unvalidated
|
||||
TLS connection can be silently man-in-the-middled.
|
||||
|
||||
0. `RSYNC_SSL_SKIP_HOSTNAME_CHECK`
|
||||
|
||||
Set to `1` to verify the certificate **chain** but skip the **hostname**
|
||||
(identity) check, for the openssl and stunnel backends. Use this only when
|
||||
the server certificate legitimately cannot match the host you connect to --
|
||||
for example connecting by bare IP address, or to a server whose internal-CA
|
||||
certificate carries a different name. The CA chain is still validated, so
|
||||
this is much narrower than disabling validation with an empty
|
||||
`RSYNC_SSL_CA_CERT`.
|
||||
|
||||
0. `RSYNC_SSL_OPENSSL`
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/.xvpics
|
||||
/doxygen/
|
||||
/tech_report/IMG_PARAMS.dir
|
||||
/tech_report/IMG_PARAMS.pag
|
||||
/upload
|
||||
/netware
|
||||
/pre-change
|
||||
/index.html-*
|
||||
/rsync-and-debian/rsync-and-debian.html
|
||||
/rsync-and-debian/rsync-and-debian.ps
|
||||
/badge.svg
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <<a href="https://fsf.org/">https://fsf.org/</a>>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <<a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<<a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<<a href="https://www.gnu.org/philosophy/why-not-lgpl.html">https://www.gnu.org/philosophy/why-not-lgpl.html</a>>.
|
||||
@@ -0,0 +1,340 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
<hr>
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
<hr>
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
<hr>
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
<hr>
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
<hr>
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
@@ -0,0 +1,284 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>rsync FAQ</TITLE>
|
||||
</HEAD>
|
||||
<!--#include virtual="header.html" -->
|
||||
|
||||
<H2 align="center">Frequently Asked Questions</H2>
|
||||
|
||||
<table><tr valign=top><td><ul>
|
||||
<li><a href="#1">the transfer fails to finish</a><br>
|
||||
<li><a href="#2">rsync recopies the same files</a><br>
|
||||
<li><a href="#3">is your shell clean</a><br>
|
||||
<li><a href="#4">memory usage</a><br>
|
||||
<li><a href="#5">out of memory</a><br>
|
||||
<li><a href="#6">rsync through a firewall</a><br>
|
||||
<li><a href="#7">rsync and cron</a><br>
|
||||
</ul></td><td> </td><td><ul>
|
||||
<li><a href="#8">rsync: Command not found</a><br>
|
||||
<li><a href="#9">spaces in filenames</a><br>
|
||||
<li><a href="#10">ignore "vanished files" warning</a><br>
|
||||
<li><a href="#11">read-only file system</a><br>
|
||||
<li><a href="#12">multiplexing overflow 101:7104843</a><br>
|
||||
<li><a href="#13">inflate (token) returned -5</a><br>
|
||||
</ul></td></tr></table>
|
||||
|
||||
<hr>
|
||||
<h3><a name=1>the transfer fails to finish</a></h3>
|
||||
|
||||
<p>If you get an error like one of these:
|
||||
|
||||
<pre>rsync: error writing 4 unbuffered bytes - exiting: Broken pipe
|
||||
rsync error: error in rsync protocol data stream (code 12) at io.c(463)
|
||||
</pre>
|
||||
|
||||
<p>or
|
||||
|
||||
<pre>rsync: connection unexpectedly closed (24 bytes read so far)
|
||||
rsync error: error in rsync protocol data stream (code 12) at io.c(342)
|
||||
</pre>
|
||||
|
||||
<p>please read the <a href="issues.html">issues and debugging page</a>
|
||||
for details on how you can try to figure out what is going wrong.
|
||||
|
||||
<hr>
|
||||
<h3><a name=2>rsync recopies the same files</a></h3>
|
||||
|
||||
<p>Some people occasionally report that rsync copies too many files when
|
||||
they expect it to copy only a few. In most cases the explanation is
|
||||
that you forgot to include the --times (-t) option in the original copy,
|
||||
so rsync is forced to (efficiently) transfer every file that differs in
|
||||
its modified time to discover what data (if any) has changed.
|
||||
|
||||
<p>Another common cause involves sending files to an Microsoft filesystem:
|
||||
if the file's modified time is an odd value but the receiving filesystem
|
||||
can only store even values, then rsync will re-transfer too many files.
|
||||
You can avoid this by specifying the --modify-window=1 option.
|
||||
|
||||
<p>Yet another periodic case can happen when daylight-savings time
|
||||
changes if your OS+filesystem saves file times in local time instead of
|
||||
UTC. For a full explanation of this and some suggestions on how to
|
||||
avoid them problem, see <a href="daylight-savings.html">this document</a>.
|
||||
|
||||
<p>Something else that can trip up rsync is a filesystem changeing the
|
||||
filename behind the scenes. This can happen when a filesystem changes
|
||||
an all-uppercase name into lowercase, or when it decomposes UTF-8 behind
|
||||
your back.
|
||||
|
||||
<blockquote>
|
||||
|
||||
<p>An example of the latter can occur with HFS+ on Mac OS X: if you
|
||||
copy a directory with a file that has a UTF-8 character sequence in it,
|
||||
say a 2-byte umlaut-u (\0303\0274), the file will get that character
|
||||
stored by the filesystem using 3 bytes (\0165\0314\0210), and rsync will
|
||||
not know that these differing filenames are the same file (it will, in
|
||||
fact, remove a prior copy of the file if --delete is enabled, and then
|
||||
recreate it).
|
||||
|
||||
<p>You can avoid a charset problem by passing an appropriate --iconv
|
||||
option to rsync that tells it what character-set the source files are,
|
||||
and what character-set the destination files get stored in. For
|
||||
instance, the above Mac OS X problem would be dealt with by using
|
||||
--iconv=UTF-8,UTF8-MAC (UTF8-MAC is a pseudo-charset recognized by Mac
|
||||
OS X iconv in which all characters are decomposed).
|
||||
|
||||
</blockquote>
|
||||
|
||||
<p>If you think that rsync is copying too many files, look at the
|
||||
itemized output (-i) to see why rsync is doing the update (e.g. the 't'
|
||||
flag indicates that the time differs, or all pluses indicates that rsync
|
||||
thinks the file doesn't exist). You can also look at the stats produced
|
||||
with -v and see if rsync is really sending all the data. See also the
|
||||
--checksum (-c) option for one way to avoid the extra copying of files
|
||||
that don't have synchronized modified times (but keep in mind that the
|
||||
-c option eats lots of disk I/O, and can be rather slow).
|
||||
|
||||
<hr>
|
||||
<h3><a name=3>is your shell clean</a></h3>
|
||||
|
||||
<p>The "is your shell clean" message and the "protocol mismatch" message
|
||||
are usually caused by having some sort of program in your .cshrc, .profile,
|
||||
.bashrc or equivalent file that writes a message every time you connect
|
||||
using a remote-shell program (such as ssh or rsh). Data written in this
|
||||
way corrupts the rsync data stream. rsync detects this at startup and
|
||||
produces those error messages. However, if you are using rsync-daemon
|
||||
syntax (host::path or rsync://) without using a remote-shell program (no
|
||||
--rsh or -e option), there is not remote-shell program involved, and the
|
||||
problem is probably caused by an error on the daemon side (so check the
|
||||
daemon logs).
|
||||
|
||||
<p>A good way to test if your remote-shell connection is clean is to try
|
||||
something like this (use ssh or rsh, as appropriate):
|
||||
|
||||
<blockquote><pre>ssh remotesystem /bin/true > test.dat</pre></blockquote>
|
||||
|
||||
<p>That should create a file called test.dat with nothing in it. If
|
||||
test.dat is not of zero length then your shell is not clean. Look at the
|
||||
contents of test.dat to see what was sent. Look at all the startup files on
|
||||
remotesystem to try and find the problem.
|
||||
|
||||
<hr>
|
||||
<h3><a name=4>memory usage</a></h3>
|
||||
|
||||
<p>Rsync versions before 3.0.0 always build the entire list of files to be
|
||||
transferred at the beginning and hold it in memory for the entire run. Rsync
|
||||
needs about 100 bytes to store all the relevant information for one file,
|
||||
so (for example) a run with 800,000 files would consume about 80M of
|
||||
memory. -H and --delete increase the memory usage further.
|
||||
|
||||
<p>Version 3.0.0 slightly reduced the memory used per file by not storing fields
|
||||
not needed for a particular file. It also introduced an incremental recursion
|
||||
mode that builds the file list in chunks and holds each chunk in memory only as
|
||||
long as it is needed. This mode dramatically reduces memory usage, but it
|
||||
only works provided that both sides are 3.0.0 or newer and certain options that
|
||||
rsync currently can't handle in this mode are not being used.
|
||||
|
||||
<hr>
|
||||
<h3><a name=5>out of memory</a></h3>
|
||||
|
||||
<p>The usual reason for "out of memory" when running rsync is that you are
|
||||
transferring a _very_ large number of files. The size of the files doesn't
|
||||
matter, only the total number of files. If memory is a problem, first try to
|
||||
use the incremental recursion mode: upgrade both sides to rsync 3.0.0 or
|
||||
newer and avoid options that disable incremental recursion (e.g., use
|
||||
<tt>--delete-delay</tt> instead of <tt>--delete-after</tt>). If this is not
|
||||
possible, you can break the rsync run into smaller chunks operating on
|
||||
individual subdirectories using <tt>--relative</tt> and/or exclude rules.
|
||||
|
||||
<hr>
|
||||
<h3><a name=6>rsync through a firewall</a></h3>
|
||||
|
||||
<p>If you have a setup where there is no way to directly connect two
|
||||
systems for an rsync transfer, there are several ways to get a firewall
|
||||
system to act as an intermediary in the transfer. You'll find full details
|
||||
on the <a href="firewall.html">firewall methods</a> page.
|
||||
|
||||
<hr>
|
||||
<h3><a name=7>rsync and cron</a></h3>
|
||||
|
||||
<p>On some systems (notably SunOS4) cron supplies what looks like a socket
|
||||
to rsync, so rsync thinks that stdin is a socket. This means that if you
|
||||
start rsync with the --daemon switch from a cron job you end up rsync
|
||||
thinking it has been started from inetd. The fix is simple—just
|
||||
redirect stdin from /dev/null in your cron job.
|
||||
|
||||
<hr>
|
||||
<h3><a name=8>rsync: Command not found</a></h3>
|
||||
|
||||
<p>This error is produced when the remote shell is unable to locate the rsync
|
||||
binary in your path. There are 3 possible solutions:
|
||||
|
||||
<ol>
|
||||
|
||||
<li>install rsync in a "standard" location that is in your remote path.
|
||||
|
||||
<li>modify your .cshrc, .bashrc etc on the remote system to include the path
|
||||
that rsync is in
|
||||
|
||||
<li>use the --rsync-path option to explicitly specify the path on the
|
||||
remote system where rsync is installed
|
||||
|
||||
</ol>
|
||||
|
||||
<p>You may echo find the command:
|
||||
|
||||
<blockquote><pre>ssh host 'echo $PATH'</pre></blockquote>
|
||||
|
||||
<p>for determining what your remote path is.
|
||||
|
||||
<hr>
|
||||
<h3><a name=9>spaces in filenames</a></h3>
|
||||
|
||||
<p>Can rsync copy files with spaces in them?
|
||||
|
||||
<p>Short answer: Yes, rsync can handle filenames with spaces.
|
||||
|
||||
<p>Long answer:
|
||||
|
||||
<p>Rsync handles spaces just like any other unix command line application.
|
||||
Within the code spaces are treated just like any other character so a
|
||||
filename with a space is no different from a filename with any other
|
||||
character in it.
|
||||
|
||||
<p>The problem of spaces is in the argv processing done to interpret the
|
||||
command line. As with any other unix application you have to escape spaces
|
||||
in some way on the command line or they will be used to separate arguments.
|
||||
|
||||
<p>It is slightly trickier in rsync (and other remote-copy programs like
|
||||
scp) because rsync sends a command line to the remote system to launch the
|
||||
peer copy of rsync (this assumes that we're not talking about daemon mode,
|
||||
which is not affected by this problem because no remote shell is involved
|
||||
in the reception of the filenames). The command line is interpreted by the
|
||||
remote shell and thus the spaces need to arrive on the remote system
|
||||
escaped so that the shell doesn't split such filenames into multiple
|
||||
arguments.
|
||||
|
||||
<p>For example:
|
||||
|
||||
<blockquote><pre>rsync -av host:'a long filename' /tmp/</pre></blockquote>
|
||||
|
||||
<p>This is usually a request for rsync to copy 3 files from the remote
|
||||
system, "a", "long", and "filename" (the only exception to this is for a
|
||||
system running a shell that does not word-split arguments in its commands,
|
||||
and that is exceedingly rare). If you wanted to request a single file with
|
||||
spaces, you need to get some kind of space-quoting characters to the remote
|
||||
shell that is running the remote rsync command. The following commands
|
||||
should all work:
|
||||
|
||||
<blockquote><pre>rsync -av host:'"a long filename"' /tmp/
|
||||
rsync -av host:'a\ long\ filename' /tmp/
|
||||
rsync -av host:a\\\ long\\\ filename /tmp/</pre></blockquote>
|
||||
|
||||
<p>You might also like to use a '?' in place of a space as long as there
|
||||
are no other matching filenames than the one with spaces (since '?' matches
|
||||
any character):
|
||||
|
||||
<blockquote><pre>rsync -av host:a?long?filename /tmp/</pre></blockquote>
|
||||
|
||||
<p>As long as you know that the remote filenames on the command line
|
||||
are interpreted by the remote shell then it all works fine.
|
||||
|
||||
<hr>
|
||||
<h3><a name=10>ignore "vanished files" warning</a></h3>
|
||||
|
||||
<p>Some folks would like to ignore the "vanished files" warning, which
|
||||
manifests as an exit-code 24. The easiest way to do this is to create
|
||||
a shell script wrapper. For instance, name this something like
|
||||
"rsync-no24":
|
||||
|
||||
<blockquote><pre>#!/bin/sh
|
||||
rsync "$@"
|
||||
e=$?
|
||||
if test $e = 24; then
|
||||
exit 0
|
||||
fi
|
||||
exit $e</pre></blockquote>
|
||||
|
||||
<hr>
|
||||
<h3><a name=11>read-only file system</a></h3>
|
||||
|
||||
<p>If you get "Read-only file system" as an error when sending to a rsync
|
||||
daemon then you probably forgot to set "read only = no" for that module.
|
||||
|
||||
<hr>
|
||||
<h3><a name=12>multiplexing overflow 101:7104843</a></h3>
|
||||
|
||||
<p>This mysterious error, or the similar "invalid message 101:7104843", can
|
||||
happen if one of the rsync processes is killed for some reason and a message
|
||||
beginning with the four characters "Kill" gets inserted into the protocol
|
||||
stream as a result. To solve the problem, you'll need to figure out why rsync
|
||||
is being killed.
|
||||
|
||||
<hr>
|
||||
<h3><a name=13>inflate (token) returned -5</a></h3>
|
||||
|
||||
This error means that rsync failed to handle an expected error from the
|
||||
compression code for a file that happened to be transferred with a block size
|
||||
of 32816 bytes. You can avoid this issue for the affected file by transferring
|
||||
it with a manually-set block size (e.g. --block-size=33000), or by upgrading
|
||||
the receiving side to rsync 3.0.7.
|
||||
|
||||
<hr>
|
||||
|
||||
<!--#include virtual="footer.html" -->
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>rsync's license</TITLE>
|
||||
</HEAD>
|
||||
<!--#include virtual="header.html" -->
|
||||
|
||||
Beginning with 3.0.0, rsync is available under the <b>GNU General Public
|
||||
License version 3</b>. <i>(Older releases were available under the
|
||||
<a href="GPL2.html">GPL version 2</a>.)</i>
|
||||
|
||||
<pre><small>
|
||||
<!--#include virtual="COPYING.html" -->
|
||||
</small></pre>
|
||||
|
||||
<!--#include virtual="footer.html" -->
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>rsync's license</TITLE>
|
||||
</HEAD>
|
||||
<!--#include virtual="header.html" -->
|
||||
|
||||
Releases <b>prior</b> to 3.0.0 were released under the
|
||||
<b>GNU General Public License version 2</b>:
|
||||
|
||||
<pre><small>
|
||||
<!--#include virtual="COPYING2.html" -->
|
||||
</small></pre>
|
||||
|
||||
<!--#include virtual="footer.html" -->
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This script does personal backups to a rsync backup server. You will end up
|
||||
# with a 7 day rotating incremental backup. The incrementals will go
|
||||
# into subdirectories named after the day of the week, and the current
|
||||
# full backup goes into a directory called "current"
|
||||
# tridge@linuxcare.com
|
||||
|
||||
# directory to backup
|
||||
BDIR=/home/$USER
|
||||
|
||||
# excludes file - this contains a wildcard pattern per line of files to exclude
|
||||
EXCLUDES=$HOME/cron/excludes
|
||||
|
||||
# the name of the backup machine
|
||||
BSERVER=owl
|
||||
|
||||
# your password on the backup server
|
||||
export RSYNC_PASSWORD=XXXXXX
|
||||
|
||||
|
||||
########################################################################
|
||||
|
||||
BACKUPDIR=`date +%A`
|
||||
OPTS="--force --ignore-errors --delete-excluded --exclude-from=$EXCLUDES
|
||||
--delete --backup --backup-dir=/$BACKUPDIR -a"
|
||||
|
||||
export PATH=$PATH:/bin:/usr/bin:/usr/local/bin
|
||||
|
||||
# the following line clears the last weeks incremental directory
|
||||
[ -d $HOME/emptydir ] || mkdir $HOME/emptydir
|
||||
rsync --delete -a $HOME/emptydir/ $BSERVER::$USER/$BACKUPDIR/
|
||||
rmdir $HOME/emptydir
|
||||
|
||||
# now the actual transfer
|
||||
rsync $OPTS $BDIR $BSERVER::$USER/current
|
||||
Loaded 100 of 633 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user