Files
rsync/.github/workflows/valgrind.yml
ᴄʜʀɪsᴛᴏᴘʜᴇʀ ᴍᴇɴɢ f0177d82a8 Add IDN support (#986)
* Add IDN support

rsync can now connect to IDN (internationalized domain name) hosts, and
IDN names are recognized in a daemon's hosts allow/deny.

* idn: convert host names label by label

The IDNA mapping folds some non-ASCII characters onto ASCII ones, so
running a whole hosts allow/deny token through idn2_to_ascii_8z() could
hand back a pattern the admin never wrote: a "*" (U+FF0A FULLWIDTH
ASTERISK) entry came back as "*" and let every host in.

Convert label by label instead, keeping an ASCII label byte for byte and
using a converted label only when it comes back as a bare A-label. An
ASCII-only config now behaves as it did before there was IDN support, and
a token that cannot be converted is left alone and so matches nothing. The
client side shares the same helper, and neither side truncates a name at
its 1024-byte buffer any more. strlower() folds only ASCII now, since its
one caller is the hosts allow/deny list, which can hold UTF-8.

Adds testsuite/daemon-access-idn and extends testsuite/idn to cover
Unicode, punycode, mixed-case and invalid input on both sides.

* idn: hand libidn2 the same flags on both sides

The client path called idn2_lookup_ul() without IDN2_NFC_INPUT while the
daemon path passed it to idn2_to_ascii_8z(). Both normalize either way --
idn2_lookup_ul() ors the flag in itself, and TR46 normalizes as it maps --
but there is no reason for the two calls to read differently, so pass one set
of flags from one place. The flag asks libidn2 to normalize the label rather
than promising that it already is: it gates the u32_normalize() call, and
without it a decomposed label comes back IDN2_NOT_NFC.

Adds composed/decomposed cases to testsuite/idn, which sees the exact host
name rsync hands out, and a decomposed hosts allow token to
testsuite/daemon-access-idn.
2026-08-20 18:51:23 +10:00

101 lines
3.6 KiB
YAML

name: Valgrind memcheck
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/valgrind.yml'
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/valgrind.yml'
schedule:
- cron: '17 4 * * *'
workflow_dispatch:
jobs:
memcheck:
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 libidn2-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