Compare commits

...
Author SHA1 Message Date
Zen Dodd 89eb68add9 ci: bound workflow runtimes 2026-08-15 17:52:58 +10:00
Zen Dodd faeea05d28 ci: schedule expensive diagnostics 2026-08-15 17:21:38 +10:00
Zen Dodd b9f926818e ci: restrict workflow permissions 2026-08-15 17:15:49 +10:00
Zen Dodd ebfadde688 ci: consolidate Ubuntu workflows 2026-08-15 17:10:50 +10:00
Zen Dodd 4c029dd9d3 ci: stabilise Cygwin tests (#1057)
* ci: stabilise Cygwin tests
* ci: install Cygwin directly
* ci: wait for Cygwin setup
2026-08-15 17:03:31 +10:00
Zen Dodd 5351b53276 ci: run PR checks without labels (#1058) 2026-08-15 16:45:26 +10:00
Anja Lea Brinkmann ec52eed3ed Update COPYING.html (openssl & xxhash exceptions) (#1040) 2026-08-15 16:28:18 +10:00
Zen Dodd 195b4c6d30 testsuite: make basis xname oracle deterministic (#1051) 2026-08-15 16:19:15 +10:00
Zen Dodd bd48f0751b docs: clarify --files-from deletion scope (#1041) 2026-08-15 16:01:09 +10:00
Andrew Tridgell dcef974c7c packaging: fixed an error in release process 2026-08-13 10:42:24 +10:00
24 changed files with 301 additions and 310 deletions

No files matched your search

+6 -4
View File
@@ -1,5 +1,9 @@
name: Lint GitHub Actions workflows
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# 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.
@@ -14,7 +18,7 @@ on:
- '.github/actionlint.yaml'
- '.github/actionlint.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths:
- '.github/workflows/*.yml'
- '.github/actionlint.yaml'
@@ -25,10 +29,8 @@ permissions:
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
timeout-minutes: 15
name: actionlint
steps:
- uses: actions/checkout@v4
+9 -4
View File
@@ -1,5 +1,9 @@
name: Test rsync on AlmaLinux 8
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# Older-LTS coverage on the Fedora/RHEL family to help with backporting
# security fixes. AlmaLinux 8 is the RHEL 8 rebuild and is the oldest
# active LTS in this family (RHEL 8 full support runs to 2029).
@@ -13,19 +17,20 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/almalinux-8-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/almalinux-8-build.yml'
schedule:
- cron: '42 8 * * *'
permissions:
contents: read
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
timeout-minutes: 45
container:
image: almalinux:8
name: Test rsync on AlmaLinux 8
+9 -4
View File
@@ -1,5 +1,9 @@
name: Build static rsync for Android
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# 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
@@ -16,7 +20,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/android-static-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/android-static-build.yml'
@@ -24,6 +28,9 @@ on:
- cron: '42 8 * * 1'
workflow_dispatch:
permissions:
contents: read
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.
@@ -31,10 +38,8 @@ env:
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
timeout-minutes: 45
name: ${{ matrix.abi }}
strategy:
fail-fast: false
+9 -4
View File
@@ -1,5 +1,9 @@
name: rsync ASan+UBSan (clang)
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,7 +11,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/asan-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/asan-build.yml'
@@ -19,12 +23,13 @@ on:
- cron: '42 9 * * 1'
workflow_dispatch:
permissions:
contents: read
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
timeout-minutes: 45
name: rsync ASan+UBSan (clang)
env:
# rsync intentionally leaks small allocations at process exit, so leak
+10 -13
View File
@@ -1,26 +1,23 @@
name: Coverage (Ubuntu)
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# Coverage duplicates the Ubuntu suite to measure it rather than protect a
# distinct PR failure mode. Keep that cost scheduled and available on demand.
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:
permissions:
contents: read
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
timeout-minutes: 45
name: gcov coverage
steps:
- uses: actions/checkout@v4
+51 -8
View File
@@ -1,5 +1,9 @@
name: Test rsync on Cygwin
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,30 +11,69 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/cygwin-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/cygwin-build.yml'
schedule:
- cron: '42 8 * * *'
permissions:
contents: read
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
timeout-minutes: 60
name: Test rsync on Cygwin
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: cygwin
run: choco install -y --no-progress cygwin cyg-get
- name: prep
run: |
cyg-get make autoconf automake gcc-core attr libattr-devel python39 python39-pip libzstd-devel liblz4-devel libssl-devel libxxhash0 libxxhash-devel
echo "C:/tools/cygwin/bin" >>$Env:GITHUB_PATH
$setup = Join-Path $Env:RUNNER_TEMP 'setup-x86_64.exe'
$sums = Join-Path $Env:RUNNER_TEMP 'cygwin-sha512.sum'
$packages = 'make,autoconf,automake,gcc-core,attr,libattr-devel,python39,python39-pip,libzstd-devel,liblz4-devel,libssl-devel,libxxhash0,libxxhash-devel'
Invoke-WebRequest https://cygwin.com/setup-x86_64.exe -OutFile $setup
Invoke-WebRequest https://cygwin.com/sha512.sum -OutFile $sums
$sum = Select-String -LiteralPath $sums -Pattern '^[0-9a-fA-F]{128}\s+\*?setup-x86_64\.exe$' | Select-Object -First 1
if (-not $sum) {
throw 'setup-x86_64.exe is missing from Cygwin sha512.sum'
}
$expected = ($sum.Line -split '\s+')[0]
$actual = (Get-FileHash -LiteralPath $setup -Algorithm SHA512).Hash
if ($actual -ine $expected) {
throw 'Cygwin setup SHA-512 mismatch'
}
$arguments = @(
'--quiet-mode',
'--no-desktop',
'--no-startmenu',
'--no-shortcuts',
'--root', 'C:\tools\cygwin',
'--local-package-dir', (Join-Path $Env:RUNNER_TEMP 'cygwin-packages'),
'--site', 'https://mirrors.kernel.org/sourceware/cygwin/',
'--packages', $packages
)
$install = Start-Process -FilePath $setup -ArgumentList $arguments -Wait -PassThru -NoNewWindow
if ($install.ExitCode -ne 0) {
exit $install.ExitCode
}
$bash = 'C:\tools\cygwin\bin\bash.exe'
if (-not (Test-Path -LiteralPath $bash)) {
throw 'Cygwin setup did not install bash'
}
& $bash -lc 'command -v make aclocal gcc python3 >/dev/null'
if ($LASTEXITCODE -ne 0) {
throw 'Cygwin setup did not install all required build tools'
}
echo 'C:/tools/cygwin/bin' >>$Env:GITHUB_PATH
- name: commonmark
run: bash -c 'python3 -mpip install --user commonmark'
- name: configure
+9 -4
View File
@@ -1,5 +1,9 @@
name: Test fleettest harness
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# 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
@@ -16,7 +20,7 @@ on:
- 'testsuite/skiplist/**'
- 'testsuite/skiplist-spec_test.py'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths:
- 'testsuite/fleettest.py'
- '.github/workflows/fleettest.yml'
@@ -27,12 +31,13 @@ on:
schedule:
- cron: '17 7 * * 1'
permissions:
contents: read
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
timeout-minutes: 45
name: fleettest against localhost
steps:
- uses: actions/checkout@v4
+9 -4
View File
@@ -1,5 +1,9 @@
name: Test rsync on FreeBSD
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,19 +11,20 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/freebsd-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/freebsd-build.yml'
schedule:
- cron: '42 8 * * 1'
permissions:
contents: read
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
timeout-minutes: 45
name: Test rsync on FreeBSD
steps:
- uses: actions/checkout@v4
+9 -4
View File
@@ -1,5 +1,9 @@
name: Test rsync on macOS
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,19 +11,20 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/macos-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/macos-build.yml'
schedule:
- cron: '42 8 * * *'
permissions:
contents: read
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
timeout-minutes: 45
name: Test rsync on macOS
steps:
- uses: actions/checkout@v4
+9 -4
View File
@@ -1,5 +1,9 @@
name: Test rsync on NetBSD
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,19 +11,20 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/netbsd-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/netbsd-build.yml'
schedule:
- cron: '42 8 * * 1'
permissions:
contents: read
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
timeout-minutes: 45
name: Test rsync on NetBSD
steps:
- uses: actions/checkout@v4
+9 -4
View File
@@ -1,5 +1,9 @@
name: Test rsync on OpenBSD
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,19 +11,20 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/openbsd-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/openbsd-build.yml'
schedule:
- cron: '42 8 * * 1'
permissions:
contents: read
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
timeout-minutes: 45
name: Test rsync on OpenBSD
steps:
- uses: actions/checkout@v4
+10 -7
View File
@@ -1,5 +1,9 @@
name: rsync scan-build (clang analyzer)
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,12 +11,15 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/scan-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/scan-build.yml'
workflow_dispatch:
permissions:
contents: read
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
@@ -20,10 +27,8 @@ jobs:
# 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
timeout-minutes: 45
name: scan-build gate (clang-18, pinned)
steps:
- uses: actions/checkout@v4
@@ -64,10 +69,8 @@ jobs:
# 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
timeout-minutes: 45
name: scan-build (latest clang, informational)
continue-on-error: true
steps:
+9 -4
View File
@@ -1,5 +1,9 @@
name: Test rsync on Solaris
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,19 +11,20 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/solaris-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/solaris-build.yml'
schedule:
- cron: '42 8 * * 1'
permissions:
contents: read
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
timeout-minutes: 45
name: Test rsync on Solaris
steps:
- uses: actions/checkout@v4
-68
View File
@@ -1,68 +0,0 @@
name: Test rsync on Ubuntu 22.04
# Older-LTS coverage to help with backporting security fixes. ubuntu-22.04
# is currently the oldest GitHub Actions runner image (20.04 was retired
# in April 2025).
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-22.04-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-22.04-build.yml'
schedule:
- cron: '42 8 * * *'
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:
- 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
run: make
- name: install
run: sudo make install
- name: info
run: rsync --version
- name: check
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check
- name: check30
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check30
- name: 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
rsync-ssl
rsync.1
rsync-ssl.1
rsyncd.conf.5
rrsync.1
rrsync
+25 -7
View File
@@ -1,5 +1,9 @@
name: Test rsync on Ubuntu
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -7,20 +11,33 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-build.yml'
schedule:
- cron: '42 8 * * *'
permissions:
contents: read
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
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
name: Test rsync on Ubuntu latest
artifact: ubuntu-bin
nonroot: true
- runner: ubuntu-22.04
name: Test rsync on Ubuntu 22.04
artifact: ubuntu-22.04-bin
nonroot: false
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
name: ${{ matrix.name }}
steps:
- uses: actions/checkout@v4
with:
@@ -50,6 +67,7 @@ jobs:
# 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)
if: matrix.nonroot
# 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
@@ -71,7 +89,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: ubuntu-bin
name: ${{ matrix.artifact }}
path: |
rsync
rsync-ssl
+9 -4
View File
@@ -1,5 +1,9 @@
name: Test rsync version mixing on Ubuntu
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# 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
@@ -28,19 +32,20 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-version-mix.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-version-mix.yml'
schedule:
- cron: '52 8 * * 1'
permissions:
contents: read
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
timeout-minutes: 45
name: rsync version-mix
steps:
- uses: actions/checkout@v4
+10 -14
View File
@@ -1,27 +1,23 @@
name: Valgrind memcheck
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# ASan+UBSan remains the per-PR memory-safety gate. Run the four slower
# Valgrind combinations daily and on demand instead of occupying PR runners.
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:
permissions:
contents: read
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
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
+10 -1
View File
@@ -1,3 +1,12 @@
REGARDING OPENSSL AND XXHASH
In addition, as a special exception, the copyright holders give
permission to dynamically link rsync with the OpenSSL and xxhash
libraries when those libraries are being distributed in compliance
with their license terms, and to distribute a dynamically linked
combination of rsync and these libraries. This is also considered
to be covered under the GPL's System Libraries exception.
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
@@ -671,4 +680,4 @@ 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
&lt;<a href="https://www.gnu.org/philosophy/why-not-lgpl.html">https://www.gnu.org/philosophy/why-not-lgpl.html</a>&gt;.
&lt;<a href="https://www.gnu.org/licenses/why-not-lgpl.html">https://www.gnu.org/licenses/why-not-lgpl.html</a>&gt;.
+6
View File
@@ -2610,6 +2610,12 @@ sign) if you want the local shell to expand it.
options are parsed (e.g. [`-a`](#opt) works the same before or after
`--files-from`, as does `--no-R` and all other options).
Listing individual files with `--files-from` does not make unlisted
siblings eligible for deletion. The [`--delete`](#opt) option only removes
entries from directories whose complete contents are being synchronised,
so list the directory itself and enable recursion if that deletion scope is
intended.
The filenames that are read from the FILE are all relative to the source
directory: any leading slash is removed, and ".." components are resolved away so
an entry cannot rise above the source directory -- e.g. "../foo" is taken as "foo"
+8 -1
View File
@@ -57,7 +57,14 @@ def main():
for fn in files:
if args.prefix:
fn = args.prefix + fn
mtime = os.lstat(fn).st_mtime
try:
mtime = os.lstat(fn).st_mtime
except FileNotFoundError:
# Tracked in git but absent from the tree we are stamping.
# export-ignore in .gitattributes keeps rsync-web/ and the
# old_versions/ binaries out of "git archive", so they have
# no file here to give a commit time to.
continue
if args.list:
print_line(fn, mtime, commit_time)
elif mtime != commit_time:
+61 -137
View File
@@ -16,30 +16,16 @@
# *symlink* components. The fix sanitizes the wire xname itself (for basis
# types only, leaving the hard-link "=> target" xname alone).
#
# Test: build an instrumented daemon-sender (env-gated sender.c edit that, when
# Test: build an instrumented rsync (env-gated sender.c edit that, when
# RSYNC_MAL_XNAME is set, injects ITEM_XNAME_FOLLOWS|ITEM_BASIS_TYPE_FOLLOWS +
# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer),
# run it via RSYNC_CONNECT_PROG with the production rsync as the receiver pulling
# with --link-dest, and observe where the receiver opens the basis.
#
# Two FIFOs, each with a helper blocked in open(O_WRONLY) that drops a flag when
# some reader opens it, tell RED from GREEN without hanging the receiver (it
# reads EOF and finishes):
# * ESCAPE base/secret reached only by an unsanitized "../secret"
# * DECOY linkdest/secret where the SANITIZED "secret" lands
# Injected xname is "../secret":
# - vulnerable receiver opens ESCAPE -> escape flag -> FAIL (traversal)
# - fixed receiver sanitizes to "secret", opens DECOY -> decoy flag -> PASS
# (the decoy flag also proves the crafted xname actually crossed the wire,
# so a stale/failed injection build can't false-PASS as "confined")
# - neither flag -> the injection never took effect -> FAIL (vacuous)
# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer).
# An env-gated receiver.c edit records the exact basedir and relpath passed to
# secure_basis_open(). This observes the security decision directly without
# relying on timing-sensitive FIFO rendezvous behaviour across operating systems.
import os
import shlex
import subprocess
import time
from pathlib import Path
import sys
from rsyncfns import (
SCRATCHDIR, build_patched_rsync, forced_protocol, makepath, rmtree,
@@ -53,11 +39,7 @@ from rsyncfns import (
_proto = forced_protocol()
if _proto is not None and _proto < 29:
test_skipped("basis-xname-traversal: xname/item flags need protocol >= 29")
if not hasattr(os, 'mkfifo'):
test_skipped("basis-xname-traversal: os.mkfifo unavailable on this platform")
# -- Build the instrumented sender (shared helper: Cygwin skip, CCACHE_DISABLE,
# -- Build the instrumented peer (shared helper: Cygwin skip, CCACHE_DISABLE,
# forced rebuild of the patched unit) -------------------------------------
PATCH_OLD = ("\t\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\n"
"\t\twrite_sum_head(f_xfer, s);")
@@ -68,14 +50,32 @@ PATCH_NEW = ("\t\tif (getenv(\"RSYNC_MAL_XNAME\")) { /* basis-xname-traversal Po
"\t\t}\n"
"\t\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\n"
"\t\twrite_sum_head(f_xfer, s);")
mal_rsync = build_patched_rsync('mal-xname-rsync', [('sender.c', PATCH_OLD, PATCH_NEW)])
TRACE_OLD = ("static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n"
"{\n"
"\textern int am_daemon, am_chrooted;")
TRACE_NEW = ("static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n"
"{\n"
"\tconst char *trace_path = getenv(\"RSYNC_BASIS_TRACE\");\n"
"\tif (trace_path) {\n"
"\t\tFILE *trace = fopen(trace_path, \"a\");\n"
"\t\tif (trace) {\n"
"\t\t\tfprintf(trace, \"%s\\t%s\\n\", basedir ? basedir : \"\", relpath);\n"
"\t\t\tfclose(trace);\n"
"\t\t}\n"
"\t}\n"
"\textern int am_daemon, am_chrooted;")
mal_rsync = build_patched_rsync(
'mal-xname-rsync',
[('sender.c', PATCH_OLD, PATCH_NEW),
('receiver.c', TRACE_OLD, TRACE_NEW)],
)
# -- Workspace ----------------------------------------------------------------
# base/serversrc/file the file the instrumented daemon offers
# base/linkdest/ the client's --link-dest (basis_dir[0])
# base/linkdest/secret DECOY fifo -- where a sanitized "secret" resolves
# base/secret ESCAPE fifo -- where an unsanitized "../secret" lands
# base/linkdest/secret where a sanitized "secret" resolves
# base/secret where an unsanitized "../secret" resolves
# base/dest/ the client's destination
base = SCRATCHDIR / 'xname-race'
rmtree(base)
@@ -84,133 +84,57 @@ linkdest = base / 'linkdest'
dest = base / 'dest'
escape = base / 'secret' # linkdest/../secret
decoy = linkdest / 'secret' # linkdest/secret
esc_flag = base / 'escape.flag'
dec_flag = base / 'decoy.flag'
trace_file = base / 'basis.trace'
makepath(serversrc)
makepath(linkdest)
makepath(dest)
(serversrc / 'file').write_text("from the server\n")
escape.write_text("escaped basis\n")
decoy.write_text("confined basis\n")
# A helper that blocks in open(fifo, O_WRONLY) until some reader opens the FIFO,
# then records the flag. Terminated below if no reader ever appears.
WRITER = ("import os,sys\n"
"open(sys.argv[3],'w').close()\n" # ready: about to block in open()
"fd=os.open(sys.argv[1],os.O_WRONLY)\n"
"open(sys.argv[2],'w').close()\n"
"os.close(fd)\n")
def spawn(fifo, flag):
ready = Path(str(flag) + '.ready')
if ready.exists():
ready.unlink()
proc = subprocess.Popen(
[sys.executable, '-c', WRITER, str(fifo), str(flag), str(ready)])
# Wait until the helper is actually at its blocking open(). Starting the
# transfer before that lets the receiver come and go while nothing is
# watching the FIFO, and the run reports a vacuous result -- which is what
# made this test flaky on the slower fleet VMs.
deadline = time.time() + 30
while not ready.exists() and proc.poll() is None and time.time() < deadline:
time.sleep(0.02)
return proc
def settle(w):
"""Give a rendezvoused helper a bounded chance to record its flag; a still-
blocked one just times out. (Closes the terminate-before-flag race.)"""
try:
w.wait(timeout=15)
except subprocess.TimeoutExpired:
pass
def reap(w):
if w.poll() is None:
w.terminate()
try:
w.wait(timeout=10)
except subprocess.TimeoutExpired:
w.kill()
w.wait()
def attempt():
"""One injection run. Returns the receiver's CompletedProcess.
Re-creates the FIFOs and flags each time so a retry starts clean.
"""
for f in (escape, decoy, esc_flag, dec_flag):
if os.path.lexists(f):
os.unlink(f)
rmtree(dest)
makepath(dest)
os.mkfifo(escape)
os.mkfifo(decoy)
esc_w = spawn(escape, esc_flag)
dec_w = spawn(decoy, dec_flag)
proc = None
try:
conf = write_daemon_conf(
[('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})],
name='mal-xname-rsyncd.conf')
os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon'
os.environ['RSYNC_MAL_XNAME'] = '../secret' # from basis_dir[0] == linkdest
proc = subprocess.run(
rsync_argv('-a', f'--link-dest={linkdest}',
'rsync://localhost/m/file', str(dest) + '/'),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)
settle(esc_w)
settle(dec_w)
finally:
os.environ.pop('RSYNC_MAL_XNAME', None)
os.environ.pop('RSYNC_CONNECT_PROG', None)
reap(esc_w)
reap(dec_w)
for f in (escape, decoy):
if os.path.lexists(f):
os.unlink(f)
return proc
# A run where NEITHER fifo was opened proves nothing: the injection did not
# take effect, so there was no traversal attempt to confine. That is a setup
# failure, not a security signal, and on the slower fleet VMs it happens often
# enough to make the test unusable -- so retry it. An ESCAPE is never retried:
# the loop stops the moment the escape flag appears.
attempts = 0
for _try in range(6):
attempts += 1
proc = attempt()
if esc_flag.is_file() or dec_flag.is_file():
break
conf = write_daemon_conf(
[('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})],
name='mal-xname-rsyncd.conf')
os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon'
os.environ['RSYNC_MAL_XNAME'] = '../secret' # from basis_dir[0] == linkdest
os.environ['RSYNC_BASIS_TRACE'] = str(trace_file)
try:
argv = rsync_argv('-a', f'--link-dest={linkdest}',
'rsync://localhost/m/file', str(dest) + '/')
argv[0] = str(mal_rsync)
proc = subprocess.run(
argv,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)
finally:
os.environ.pop('RSYNC_BASIS_TRACE', None)
os.environ.pop('RSYNC_MAL_XNAME', None)
os.environ.pop('RSYNC_CONNECT_PROG', None)
# -- Oracle -------------------------------------------------------------------
out_tail = '\n'.join((proc.stdout if proc else '').splitlines()[-20:])
out_tail = '\n'.join(proc.stdout.splitlines()[-20:])
trace = trace_file.read_text().splitlines() if trace_file.is_file() else []
escaped = f'{linkdest}\t../secret'
confined = f'{linkdest}\tsecret'
if esc_flag.is_file():
if escaped in trace:
test_fail(
"malicious server traversed the client's filesystem via the alt-dest "
f"xname: the receiver opened {escape} (one level above the --link-dest "
f"xname: the receiver attempted {escape} (one level above the --link-dest "
"dir) as the delta basis. A server-supplied xname of '../secret' was "
"not sanitized on the client (sanitize_paths==0 off-daemon). Fix: "
"sanitize a basis-type xname in read_ndx_and_attrs(). Receiver output "
f"tail:\n{out_tail}")
# The decoy flag proves the crafted xname reached the receiver AND was confined
# to the basedir (sanitized "../secret" -> "secret" -> linkdest/secret). Its
# absence means the injection never took effect (e.g. a stale patched build),
# so a clear escape flag alone would be a vacuous pass.
if not dec_flag.is_file():
# The trace proves the crafted xname reached the receiver and was confined to
# the basedir (sanitized "../secret" -> "secret" -> linkdest/secret). Its
# absence means the injection never took effect (e.g. a stale patched build).
if confined not in trace:
test_fail(
"the crafted xname never reached the receiver's basis open (neither the "
"escape nor the decoy FIFO was opened) -- the instrumented-sender "
f"injection did not take effect, so this run is vacuous after "
f"{attempts} attempt(s). This is a harness failure, NOT a traversal: "
"an escape is reported separately and is never retried. Receiver rc="
f"{proc.returncode if proc else 'n/a'}. Output tail:\n{out_tail}")
"the crafted xname never reached the receiver's confined basis open; "
"the instrumented injection did not take effect, so this run is "
f"vacuous. Trace={trace!r}. Receiver rc={proc.returncode}. "
f"Output tail:\n{out_tail}")
if proc.returncode != 0:
test_fail(
+5 -5
View File
@@ -91,10 +91,10 @@
]
},
{
"_comment": "Ubuntu 18.04 older-LTS backport coverage on a root@ box; no 18.04 runner image exists so it mirrors the 22.04 workflow.",
"_comment": "Ubuntu 18.04 older-LTS backport coverage on a root@ box; no 18.04 runner image exists so it mirrors the 22.04 matrix lane.",
"name": "ubuntu-1804",
"ssh_host": "root@ubuntu-1804",
"workflow": "ubuntu-22.04-build.yml",
"workflow": "ubuntu-build.yml",
"_python_comment": "18.04's default python3 is 3.6, but runtests.py uses subprocess capture_output (3.7+); run the suite under the 3.10 that's also installed.",
"python": "python3.10",
"_configure_flags_comment": "18.04's libzstd 1.3.3 lacks ZSTD_minCLevel (configure aborts), so disable zstd; also disable lz4 so the default -z compressor is zlib -- with lz4 as the default, compress-options' --compress-level=9 check fails since lz4 has no levels. xxhash is fine.",
@@ -110,10 +110,10 @@
]
},
{
"_comment": "Ubuntu 20.04 older-LTS backport coverage on a root@ box; no 20.04 runner image exists so it mirrors the 22.04 workflow.",
"_comment": "Ubuntu 20.04 older-LTS backport coverage on a root@ box; no 20.04 runner image exists so it mirrors the 22.04 matrix lane.",
"name": "ubuntu-2004",
"ssh_host": "root@ubuntu-2004",
"workflow": "ubuntu-22.04-build.yml",
"workflow": "ubuntu-build.yml",
"configure_flags": [
"--with-rrsync"
],
@@ -126,7 +126,7 @@
"_comment": "Builds unprivileged (like a CI runner) and runs the suite via sudo; the nonroot pass reruns the privilege-sensitive tests as the ssh user. protocols: [30, 29] runs the check30/check29 passes on this NO-xattrat kernel: the CI's proto-29 step runs here, but the only other proto-29 fleet box (ubuntu-2604) is xattrat, so without this the (no-xattrat x proto-29) cell is uncovered.",
"name": "ubuntu-2204",
"ssh_host": "runner@ubuntu-2204",
"workflow": "ubuntu-22.04-build.yml",
"workflow": "ubuntu-build.yml",
"privilege": "sudo",
"nonroot": true,
"protocols": [
+5 -5
View File
@@ -6,10 +6,10 @@ Builds the committed HEAD of an rsync checkout on a fleet of remote machines
--use-tcp) in parallel, and prints one report of only the UNEXPECTED results --
a fast local pre-flight for the GitHub CI matrix.
Each target maps 1:1 to a .github/workflows/*.yml job: the per-target configure
flags mirror that workflow, and the pipe-run RSYNC_EXPECT_SKIPPED list is PARSED
from the workflow (not hardcoded). The --use-tcp run never sets an expected-skip
list (matching the workflows), so only test FAILs matter there.
Each target maps to a .github/workflows/*.yml job or matrix lane: the per-target
configure flags mirror that lane, and the pipe-run RSYNC_EXPECT_SKIPPED list is
PARSED from the workflow (not hardcoded). The --use-tcp run never sets an
expected-skip list (matching the workflows), so only test FAILs matter there.
The tcp pass runs only the tests that can reach the daemon transport, because it
follows a full pipe pass over the very same build: --use-tcp is observable only
@@ -167,7 +167,7 @@ PUSH_EXCLUDES = [
class Target:
name: str
ssh_host: str | None # null in JSON => run locally
workflow: str # filename under .github/workflows
workflow: str # workflow containing the matching job or matrix lane
configure_flags: list[str]
make: str = "make" # e.g. "gmake" on the BSDs/Solaris
env_prefix: str = "" # exported before configure AND make (e.g. PATH)
@@ -48,6 +48,10 @@ try:
os.utime(inside_file, (st.st_atime, st.st_mtime))
except FileNotFoundError:
pass
except PermissionError:
# Cygwin can report EACCES while the flipper swaps this path.
if not _CYGWIN:
raise
subprocess.run(
rsync_argv('-a', '--remove-source-files', f'{url}src/real/file', str(dest) + '/'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)