Author: jpeck
Date: Thu Jul 9 22:24:20 2026
New Revision: 1936032
Log:
Add the dav-mirror-autocheck.sh regression tests promised in r1936010-r1936031.
The mirror harness now runs everything against URI-encoded master and
slave locations ("master loc"/"slave loc", deliberately of different
lengths) and a slash-terminated SVNMasterURI, exercising the encoding
fixes.
New coverage:
- Log messages and revision properties containing the slave URL
round-trip verbatim (create-txn-with-props POST bodies).
- The full lock lifecycle (a URL-bearing lock comment stored on the
master, locked commit, release-on-commit, and explicit UNLOCK)
- Raw HTTP reads of open-transaction resources through the slave
(file content verbatim, PROPFIND hrefs translated, XML-mime-typed
content guarded).
- The one remaining SVN-3445 residual: dead-property values rewritten
in a proxied multistatus, reported as XFAIL
The last 2 tests require curl for the transaction probes, no svn client
can hold a transaction open or address !svn/txr URIs
* subversion/tests/cmdline/dav-mirror-autocheck.sh: As above and fix a
stray ">" in the generated master vhost's ServerName.
* subversion/mod_dav_svn/mirror.c
(proxy_request_fixup_destination, response_is_xml): Correct some stale
comments
Modified:
subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
Modified:
subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
==============================================================================
--- subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
Thu Jul 9 20:40:47 2026 (r1936031)
+++ subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
Thu Jul 9 22:24:20 2026 (r1936032)
@@ -35,10 +35,9 @@
/* If the request carries a Destination header (as COPY and MOVE do), rewrite
it to target the master server instead of this slave. MASTER_URI is the
- SVNMasterURI configuration value (not URI-encoded). This is the
+ SVNMasterURI configuration value (canonical and URI-encoded). This is the
request-side counterpart to the Location-header rewrite performed by
- dav_svn__location_header_filter() (the two are conceptual inverses, though
- they differ in how they juggle the encoded/decoded root). */
+ dav_svn__location_header_filter(); the two are conceptual inverses. */
static void
proxy_request_fixup_destination(request_rec *r, const char *master_uri)
{
@@ -329,10 +328,11 @@ apr_status_t dav_svn__location_header_fi
return ap_pass_brigade(f->next, bb);
}
-/* Only protocol XML (a multistatus, activity set, etc.) carries hrefs the
- proxy must translate. Anything else including a body of unidentified
- type is versioned payload and must pass through untouched, else we
- potentially corrupt it (issue #3445). */
+/* Only protocol XML (a multistatus, merge-response, etc.) carries hrefs
+ the proxy must translate. Since the body filter is attached only for
+ MERGE and PROPFIND, anything non-XML reaching it is an error body
+ (an httpd or mod_proxy error page, whose fixed Content-Length the
+ rewrite would invalidate) and must pass through untouched. */
static svn_boolean_t
response_is_xml(const request_rec *r)
{
Modified:
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
==============================================================================
---
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
Thu Jul 9 20:40:47 2026 (r1936031)
+++
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
Thu Jul 9 22:24:20 2026 (r1936032)
@@ -29,11 +29,12 @@
# loopback network (127.0.0.1, 127.0.0.2) for slave and
# master, respectively.
#
-# The set of changes sent through the system is currently
-# just the test case for issue 2939, using svnmucc
-# https://issues.apache.org/jira/browse/SVN-2939
-# But of course, any svn traffic liable to break over
-# mirroring would be a good addition.
+# The changes sent through the system started as the reproduction
+# recipe for issue 2939 (https://issues.apache.org/jira/browse/SVN-2939,
+# using svnmucc) and have grown to cover URI-encoded locations, the
+# SVN-3445 payload-corruption regressions, proxied reads of transaction
+# resources, and locks. Any svn traffic liable to break over
+# mirroring remains a good addition.
#
# Most of the httpd setup was lifted from davautocheck.sh.
# The common boilerplate snippets to setup/start/stop httpd
@@ -181,7 +182,8 @@ CustomLog "${HTTPD_ROOT}/ops"
<Location "/${SLAVE_LOCATION}">
DAV svn
SVNPath "${SLAVE_REPOS}"
- SVNMasterURI "${MASTER_URL}"
+ # Deliberate trailing slash: SVNMasterURI must be canonicalized at parse
time.
+ SVNMasterURI "${MASTER_URL}/"
AuthType Basic
AuthName "Subversion Repository"
AuthUserFile ${HTTPD_ROOT}/users
@@ -200,7 +202,7 @@ CustomLog "${HTTPD_ROOT}/ops"
# master
<VirtualHost ${MASTER_HOST}>
- ServerName ${MASTER_HOST}>
+ ServerName ${MASTER_HOST}
CustomLog "${HTTPD_ROOT}/master_access_log" common
ErrorLog "${HTTPD_ROOT}/master_error_log"
<Location "/${MASTER_LOCATION}">
@@ -292,6 +294,8 @@ SVNLOOK=$ABS_BUILDDIR/subversion/svnlook
[ -x $SVNLOOK ] || fail "SVNLOOK $SVNLOOK not built"
[ -x $SVNMUCC ] || fail "SVNMUCC $SVNMUCC not built"
+CURL=$(which curl) || fail "curl not found - required for proxied-read tests"
+
say HTTPD: $HTTPD
say SVN: $SVN
say SVNADMIN: $SVNADMIN
@@ -366,16 +370,22 @@ SLAVE_HOST=127.0.0.1
#TEST_PORT=11111
TEST_PORT=$(($RANDOM+1024))
-# Location directive elements for master, slave, and sync. Master and slave
-# deliberately use different locations so the proxy actually has to translate
-# paths (slave root_dir <-> master URI) on the way through. With matching
-# locations some of the rewrite logic is not exercised.
-MASTER_LOCATION="master"
-SLAVE_LOCATION="slave"
+# Location directive elements for master, slave, and sync. Master and slave
+# deliberately use DIFFERENT locations so the proxy actually has to translate
+# paths (slave root_dir <-> master URI), locations of DIFFERENT LENGTHS so a
+# byte-level rewrite of versioned payload corrupts the svndiff framing loudly
+# instead of passing silently (SVN-3445), and locations containing a
+# URI-escapable character (a space) so every test exercises the encoding of
+# both location paths end to end. The *_LOCATION_URI forms are encoded (they
go
+# into URLs and comparisons against wire data).
+MASTER_LOCATION="master loc"
+MASTER_LOCATION_URI="master%20loc"
+SLAVE_LOCATION="slave loc"
+SLAVE_LOCATION_URI="slave%20loc"
SYNC_LOCATION="sync"
-MASTER_URL="http://${MASTER_HOST}:${TEST_PORT}/${MASTER_LOCATION}"
-SLAVE_URL="http://${SLAVE_HOST}:${TEST_PORT}/${SLAVE_LOCATION}"
+MASTER_URL="http://${MASTER_HOST}:${TEST_PORT}/${MASTER_LOCATION_URI}"
+SLAVE_URL="http://${SLAVE_HOST}:${TEST_PORT}/${SLAVE_LOCATION_URI}"
SYNC_URL="http://${SLAVE_HOST}:${TEST_PORT}/${SYNC_LOCATION}"
BASE_URL="$SLAVE_URL"
@@ -496,50 +506,217 @@ $SVNLOOK tree --full-paths "$MASTER_REPO
say "PASS: committing a path which has a space in it passes"
-# Regression coverage for SVN-3445: when the master and slave locations differ,
-# the proxy's request-body filter (dav_svn__location_in_filter) does a blind,
-# byte-level search-and-replace of the slave location path with the master
-# location path over the entire commit body, including versioned file content
-# carried in the svndiff stream. A file whose text contains the slave's path
-# is therefore mangled, whereas a file containing the master's path passes
-# through untouched.
+# Regression coverage for SVN-3445. When the master and slave locations
+# differ, the proxy must translate the location prefix in *protocol* URLs
+# (hrefs in MERGE/CHECKOUT bodies) but must NOT touch *user payload*:
+# svndiff-encoded file content in PUT bodies, property values in
+# PROPPATCH bodies, and log-message/revprop skels in POST bodies.
say "Test case for versioned content munging (SVN-3445)"
-# A file embedding the master URL must commit and round-trip verbatim: the
-# request-body filter only looks for the slave path, which this content does
-# not contain.
+# File content must round-trip verbatim whether it embeds the master URL
+# or the slave URL (used to be mangled/rejected).
echo "$MASTER_URL" > "$HTTPD_ROOT/master-url.txt"
+echo "$SLAVE_URL" > "$HTTPD_ROOT/slave-url.txt"
$svnmucc put "$HTTPD_ROOT/master-url.txt" "$BASE_URL/master-url.txt" \
- || fail "committing a file containing the master URL failed"
+ put "$HTTPD_ROOT/slave-url.txt" "$BASE_URL/slave-url.txt" \
+ || fail "committing URL-bearing files failed (SVN-3445: PUT body rewritten?)"
+
master_file_content=$($SVNLOOK cat "$SLAVE_REPOS" master-url.txt)
[ "$master_file_content" = "$MASTER_URL" ] \
- || fail "content embedding the master URL was munged: committed
'$MASTER_URL', slave stores '$master_file_content'"
-say "PASS: content embedding the master URL is preserved verbatim"
-
-# A file embedding the slave URL is the SVN-3445 hazard. The filter rewrites
-# the "/${SLAVE_LOCATION}" byte sequence to "/${MASTER_LOCATION}" inside the
-# svndiff-encoded file data. When those path segments differ in length (as
-# here) the rewrite corrupts the svndiff framing and the master rejects the
-# commit (SVN_ERR_SVNDIFF_UNEXPECTED_END); when they are the same length it
-# instead silently stores content with the slave hostname but the master path.
-# Either outcome is a known bug, reported as XFAIL so it does not abort the
-# suite. Once SVN-3445 is fixed, tighten this into a hard assertion that the
-# file both commits and round-trips verbatim.
-echo "$SLAVE_URL" > "$HTTPD_ROOT/slave-url.txt"
-slave_url_munged="http://${SLAVE_HOST}:${TEST_PORT}/${MASTER_LOCATION}"
-if ($svnmucc put "$HTTPD_ROOT/slave-url.txt" "$BASE_URL/slave-url.txt" 2>&1);
then
- slave_file_content=$($SVNLOOK cat "$SLAVE_REPOS" slave-url.txt)
- if [ "$slave_file_content" = "$SLAVE_URL" ]; then
- say "XPASS: SVN-3445 appears fixed: 'slave-url.txt' committed and survived
verbatim."
- say " Please replace this XFAIL block with a hard assertion."
- else
- say "XFAIL (SVN-3445): 'slave-url.txt' content was altered unexpectedly:"
- say " committed '$SLAVE_URL', slave stores '$slave_file_content'."
- fi
+ || fail "file content embedding the master URL was munged: committed
'$MASTER_URL', slave stores '$master_file_content'"
+slave_file_content=$($SVNLOOK cat "$SLAVE_REPOS" slave-url.txt)
+[ "$slave_file_content" = "$SLAVE_URL" ] \
+ || fail "file content embedding the slave URL was munged: committed
'$SLAVE_URL', slave stores '$slave_file_content'"
+say "PASS: file content is preserved verbatim regardless of embedded URL"
+
+# Property values must likewise round-trip verbatim: a PROPPATCH value that
+# contains the slave URL used to be silently rewritten.
+$svnmucc propset svn-3445-prop "$SLAVE_URL" "$BASE_URL/slave-url.txt" \
+ || fail "propset of a value containing the slave URL failed"
+prop_value=$($SVNLOOK propget "$SLAVE_REPOS" svn-3445-prop slave-url.txt)
+[ "$prop_value" = "$SLAVE_URL" ] \
+ || fail "property value embedding the slave URL was munged: set
'$SLAVE_URL', slave stores '$prop_value'"
+say "PASS: property value is preserved verbatim regardless of embedded URL"
+
+# Commit log messages and revision properties travel inside the
+# create-txn-with-props POST body (HTTPv2), a length-prefixed skel
+# carrying only user data, never protocol URLs. Rewriting that body
+# corrupts the values, and when the location paths differ in length it
+# breaks the skel framing outright. Both must round-trip verbatim.
+# (The $svnmucc wrapper bakes in -mm, so invoke $SVNMUCC directly.)
+log_msg="log mentioning the slave URL: $SLAVE_URL"
+$SVNMUCC --non-interactive --username jrandom --password rayjandom \
+ -m "$log_msg" --with-revprop "svn-3445-revprop=$SLAVE_URL" \
+ mkdir "$BASE_URL/log-url-dir" \
+ || fail "commit with a log message containing the slave URL failed (POST
body rewritten?)"
+rev=$($SVNLOOK youngest "$SLAVE_REPOS")
+stored_log=$($SVNLOOK propget --revprop -r "$rev" "$SLAVE_REPOS" svn:log)
+[ "$stored_log" = "$log_msg" ] \
+ || fail "log message was munged: committed '$log_msg', slave stores
'$stored_log'"
+stored_revprop=$($SVNLOOK propget --revprop -r "$rev" "$SLAVE_REPOS"
svn-3445-revprop)
+[ "$stored_revprop" = "$SLAVE_URL" ] \
+ || fail "revprop value was munged: set '$SLAVE_URL', slave stores
'$stored_revprop'"
+say "PASS: log message and revprop values are preserved verbatim"
+
+# The response side (mirror.c attaches the body-rewrite filter only for
+# MERGE and PROPFIND, with the response_is_xml() gate as a backstop) is
+# covered by the proxied-txn-reads test below. The residual block further
+# down exercises the one remaining SVN-3445 residual.
+
+# Response-side SVN-3445 coverage: reads of transaction resources are
+# proxied to the master; the proxy must rewrite hrefs in protocol XML
+# (PROPFIND multistatus) but leave file content untouched.
+#
+# These probes must be raw HTTP (curl), not svn commands, at both ends:
+# no svn client operation creates a commit transaction and leaves it open
+# (svn/svnmucc create, commit and abort their txns within one operation),
+# and no svn command can address the private !svn/txr/ URIs. An
+# 'svn cat' of the public URL would be served locally by the slave
+# without ever traversing the proxy or the response filter under test.
+say "Test case for proxied reads of txn resources (SVN-3445 response side)"
+
+curl_auth="$CURL --silent --show-error --user jrandom:rayjandom"
+
+# Commit an XML-mime-typed file for probe 3 below (before the txn is
+# opened, so the txn tree contains it).
+printf '<?xml version="1.0"?>\n<note><!-- %s --></note>\n' "$MASTER_URL" \
+ > "$HTTPD_ROOT/xml-payload.xml"
+$svnmucc put "$HTTPD_ROOT/xml-payload.xml" "$BASE_URL/xml-payload.xml" \
+ propset svn:mime-type text/xml "$BASE_URL/xml-payload.xml" \
+ || fail "committing the XML-typed payload file failed"
+
+# Open a transaction directly on the master (raw HTTPv2 create-txn POST)
+# and harvest its name from the SVN-Txn-Name response header.
+txn_name=$($curl_auth --request POST \
+ --header "Content-Type: application/vnd.svn-skel" --data "( create-txn )" \
+ --dump-header - --output /dev/null "$MASTER_URL/!svn/me" \
+ | sed -ne 's/^SVN-Txn-Name: *//p' | tr -d '\r')
+[ -n "$txn_name" ] || fail "could not create a txn on the master"
+
+# 1. Non-XML payload: GET the file through the SLAVE. The response is
+# proxied from the master and must arrive verbatim, the slave URL
+# embedded in the content must NOT have been rewritten.
+proxied_get=$($curl_auth "$SLAVE_URL/!svn/txr/$txn_name/slave-url.txt")
+[ "$proxied_get" = "$SLAVE_URL" ] \
+ || fail "proxied GET of txn file content was munged: expected '$SLAVE_URL',
got '$proxied_get'"
+
+# 2. Protocol XML: PROPFIND on the txn file through the SLAVE. The
+# multistatus hrefs come from the master and MUST be rewritten to the
+# slave location.
+proxied_propfind=$($curl_auth --request PROPFIND --header "Depth: 0" \
+ "$SLAVE_URL/!svn/txr/$txn_name/slave-url.txt")
+echo "$proxied_propfind" | grep -qF "/${SLAVE_LOCATION_URI}/" \
+ || fail "proxied PROPFIND multistatus hrefs were not rewritten to the slave
location: $proxied_propfind"
+echo "$proxied_propfind" | grep -qF "/${MASTER_LOCATION_URI}/" \
+ && fail "proxied PROPFIND multistatus still contains master-location hrefs:
$proxied_propfind"
+
+# 3. Regression guard: a versioned file whose svn:mime-type is XML must
+# never have its content rewritten on a proxied read. Doubly protected:
+# GET responses do not receive the ReposRewrite body filter at all
+# (proxy_request_fixup() attaches it only for MERGE and PROPFIND), and
+# mod_dav_svn happens to emit no Content-Type for !svn/txr file GETs.
+proxied_xml_get=$($curl_auth "$SLAVE_URL/!svn/txr/$txn_name/xml-payload.xml")
+echo "$proxied_xml_get" | grep -qF "$MASTER_URL" \
+ || fail "XML-typed txn file content did not round-trip a proxied read
verbatim: got '$proxied_xml_get'"
+say "PASS: XML-typed txn file content survives a proxied read verbatim"
+
+# Clean up the open txn so later consistency checks aren't confused.
+$curl_auth --request DELETE --output /dev/null \
+ "$MASTER_URL/!svn/txn/$txn_name" \
+ || say "WARNING: could not delete test txn $txn_name (continuing)"
+
+say "PASS: proxied txn reads: content verbatim, protocol hrefs rewritten"
+
+# The one remaining SVN-3445 residual: a dead-property value rewritten in
+# a proxied PROPFIND multistatus. Needs a proxied read of in-transaction
+# data (reads of committed data are served locally by the slave and never
+# traverse the response filter), so it reuses the curl txn-probe technique
+# from the test above.
+say "Test case for the SVN-3445 response-side residual (XFAIL expected)"
+
+# The munged fingerprint: a master-URL value rewritten by the response
+# filter keeps the master host but gains the slave location path.
+master_url_munged="http://${MASTER_HOST}:${TEST_PORT}/${SLAVE_LOCATION_URI}"
+
+# The residual: a dead-property VALUE containing the master URL, returned
+# inside a proxied PROPFIND multistatus, is blindly rewritten along with
+# the genuine hrefs (see the "FIXME (SVN-3445, residual)" comment in
+# mirror.c's dav_svn__location_body_filter).
+$svnmucc propset svn-3445-residual-prop "$MASTER_URL"
"$BASE_URL/master-url.txt" \
+ || fail "propset of a master-URL value failed"
+
+# Open a fresh txn on the master (after the propset above, so its tree
+# contains the property).
+txn_name=$($curl_auth --request POST \
+ --header "Content-Type: application/vnd.svn-skel" --data "( create-txn )" \
+ --dump-header - --output /dev/null "$MASTER_URL/!svn/me" \
+ | sed -ne 's/^SVN-Txn-Name: *//p' | tr -d '\r')
+[ -n "$txn_name" ] || fail "could not create a txn on the master (residual
tests)"
+
+# Residual probe: PROPFIND (allprop) on the file through the SLAVE.
+residual_propfind=$($curl_auth --request PROPFIND --header "Depth: 0" \
+ "$SLAVE_URL/!svn/txr/$txn_name/master-url.txt")
+if echo "$residual_propfind" | grep -qF "$MASTER_URL"; then
+ say "XPASS: SVN-3445 residual appears fixed: dead-property value survived a"
+ say " proxied PROPFIND verbatim."
+elif echo "$residual_propfind" | grep -qF "$master_url_munged"; then
+ say "XFAIL (SVN-3445 residual): dead-property value was rewritten in the"
+ say " proxied multistatus: '$MASTER_URL' -> '$master_url_munged'."
else
- say "XFAIL (SVN-3445): committing 'slave-url.txt' was rejected"
+ fail "residual PROPFIND contained neither the original nor the munged value:
$residual_propfind"
fi
+# Clean up the open txn.
+$curl_auth --request DELETE --output /dev/null \
+ "$MASTER_URL/!svn/txn/$txn_name" \
+ || say "WARNING: could not delete residual-test txn $txn_name (continuing)"
+
+# LOCK/UNLOCK are proxied methods, and locked-file commits push lock tokens
+# through the proxy. The lock comment travels as the DAV owner element in
+# the LOCK body, assert the comment avoids the filters.
+say "Test case for locks through the write-through proxy"
+
+svncmd="$SVN --non-interactive --username=jrandom --password=rayjandom"
+$svncmd checkout -q "$BASE_URL" "$HTTPD_ROOT/wc-lock" \
+ || fail "checkout for lock test failed"
+$svncmd lock -m "locked via slave: $SLAVE_URL"
"$HTTPD_ROOT/wc-lock/slave-url.txt" \
+ || fail "svn lock through the proxy failed"
+
+# The lock must exist on the MASTER (locks are not versioned; svnsync does
+# not carry them, the proxied LOCK must have created it there).
+$SVNLOOK lock "$MASTER_REPOS" slave-url.txt | grep -q "Owner: jrandom" \
+ || fail "lock did not reach the master repository"
+
+# The client's own view of the comment is just the cached copy in its
+# working-copy lock table, so inspect what the master actually stored.
+lock_comment=$($SVNLOOK lock "$MASTER_REPOS" slave-url.txt)
+echo "$lock_comment" | grep -qF "$SLAVE_URL" \
+ || fail "stored lock comment was munged by the proxy: expected it to contain
'$SLAVE_URL', master stores: $lock_comment"
+say "PASS: lock comment survives the proxy verbatim"
+
+# Committing a change to the locked file sends the lock token with the
+# proxied request (inside the MERGE body's lock-token-list), the commit
+# must succeed and (by default) release the lock.
+echo "locked change" >> "$HTTPD_ROOT/wc-lock/slave-url.txt"
+$svncmd commit -m "commit to locked file via slave" "$HTTPD_ROOT/wc-lock" \
+ || fail "commit to a locked file through the proxy failed"
+[ -z "$($SVNLOOK lock "$MASTER_REPOS" slave-url.txt)" ] \
+ || fail "lock was not released by the commit on the master"
+
+# Explicit unlock is a separate proxied method, exercise it too.
+$svncmd update -q "$HTTPD_ROOT/wc-lock" \
+ || fail "update of the lock working copy failed"
+$svncmd lock -m "re-locked for explicit unlock"
"$HTTPD_ROOT/wc-lock/slave-url.txt" \
+ || fail "re-locking through the proxy failed"
+$SVNLOOK lock "$MASTER_REPOS" slave-url.txt | grep -q "Owner: jrandom" \
+ || fail "re-acquired lock did not reach the master repository"
+$svncmd unlock "$HTTPD_ROOT/wc-lock/slave-url.txt" \
+ || fail "explicit svn unlock through the proxy failed"
+[ -z "$($SVNLOOK lock "$MASTER_REPOS" slave-url.txt)" ] \
+ || fail "explicit unlock did not release the lock on the master"
+
+say "PASS: lock, locked commit, commit release, and explicit unlock work
through the proxy"
+
# Test case for commit to out-dated(though target path is up to date) slave.
# See issue #3860 for details.
say "Test case for out-dated slave commit"