Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions scripts/ci/test-validate-regression-corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,47 @@ def _validate(self) -> dict[str, Any]:
codec_runner_path=self.codec_runner,
)

def test_binary_base_asset_does_not_prevent_corpus_validation(self) -> None:
asset = self.root / "assets/font.woff2"
asset.parent.mkdir()
data = b"wOF2\x00\xbc\xff\x80\r\n"
asset.write_bytes(data)
self._git("add", "assets/font.woff2")
self._git(
"-c", "user.name=Regression Corpus Test",
"-c", "user.email=regression-corpus@example.invalid",
"commit", "--quiet", "--message=binary-baseline",
)

result = self._validate()

self.assertFalse(result["counts"]["codec"]["related_change"])
self.assertEqual(VALIDATOR._ref_files(self.root, "HEAD")["assets/font.woff2"], data)

def test_unchanged_crlf_fixture_keeps_exact_base_bytes(self) -> None:
relative = "tests/fixtures/codec_regressions/base.json"
fixture = self.root / relative
data = fixture.read_bytes().replace(b"\n", b"\r\n")
fixture.write_bytes(data)
self._git("-c", "core.autocrlf=false", "add", relative)
self._git(
"-c", "user.name=Regression Corpus Test",
"-c", "user.email=regression-corpus@example.invalid",
"commit", "--quiet", "--message=crlf-baseline",
)

result = self._validate()

self.assertFalse(result["counts"]["codec"]["related_change"])
self.assertEqual(VALIDATOR._ref_files(self.root, "HEAD")[relative], data)

def test_symbolic_base_ref_is_bound_to_its_complete_commit_identity(self) -> None:
expected = VALIDATOR._run(["git", "rev-parse", "HEAD"], self.root).strip()

result = self._validate()

self.assertEqual(result["base_ref"], expected)

def _add_avro_golden_to_base(
self,
malformed_wire: str | None = None,
Expand Down
23 changes: 17 additions & 6 deletions scripts/ci/validate-regression-corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,11 +1080,18 @@ def _tracked_worktree_files(root: Path) -> dict[str, bytes]:

def _ref_files(root: Path, ref: str) -> dict[str, bytes]:
paths = _run(["git", "ls-tree", "-r", "--name-only", "-z", ref], root).split("\0")
return {
path: _run(["git", "show", f"{ref}:{path}"], root).encode()
for path in paths
if path
}
files = {}
for path in filter(None, paths):
# Assets may be binary, and immutable fixtures retain their exact line endings.
result = subprocess.run(
["git", "show", "--end-of-options", f"{ref}:{path}"],
cwd=root, capture_output=True, check=False,
)
if result.returncode != 0:
detail = result.stderr.decode("utf-8", errors="replace").strip()
raise CorpusError(f"git show {ref}:{path} failed: {detail}")
files[path] = result.stdout
return files


def _matches(path: str, pattern: str) -> bool:
Expand Down Expand Up @@ -1415,7 +1422,11 @@ def validate(
base_files: dict[str, bytes] = {}
base_evidence: list[Evidence] = []
if base_ref and not ZERO_COMMIT.fullmatch(base_ref):
_run(["git", "rev-parse", "--verify", f"{base_ref}^{{commit}}"], root)
base_ref = _run(
["git", "rev-parse", "--verify", "--end-of-options", f"{base_ref}^{{commit}}"], root,
).strip()
if not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", base_ref):
raise CorpusError("base ref must resolve to a complete Git commit ID")
changed, added_paths = _changed_paths(root, base_ref)
base_files = _ref_files(root, base_ref)
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ No #184 executor, acquisition source or blocked review is reused here.
Use a dedicated observer process, with one connection and no retry, for each
before/after SQL pair. The proposed driver family is PyMySQL 1.2.3, source commit
`c990e86083c26493f3285b1e047902994c4b1811`. Its package archive, complete runtime
import closure, authentication/TLS dependencies and installed input are **not
selected or acquired**. Public release/source identity is only an input to this
import closure, actual authentication/TLS inputs and installed input are **not
bound or acquired**. Public release/source identity is only an input to this
proposal; it does not establish a supported or qualified historical tuple.

The first pure source slice now lives in `tests/worker_startup_raw.py`.
Expand All @@ -37,22 +37,56 @@ failure, and checks the same monotonic deadline before and after each callback.
Synthetic callbacks exercise these rules without a driver or socket. The
wrapper cannot interrupt a blocked callback. It is not wired to the pinned
driver's private method and proves no actual handshake or receive coverage.
No driver, terminator validation, process owner, framing or cleanup is
implemented by these slices. The collector vectors below remain unexecuted,
and all operating inputs remain unselected.

`tests/worker_startup_buffered.py` places the same reservation beneath a stock
`io.BufferedReader`. Its raw input must be supplied before the first protocol
read. A logical request is bounded before buffered allocation; every raw
request, including read-ahead, is reserved before reading. Cached bytes are not
charged twice, and replacing a stream retains the same phase budget/deadline.
The inert predecessor control demonstrates that a one-byte logical callback
can otherwise fetch 8192 uncharged bytes through stock buffering. Successor
controls cover that reservation, cached reads, shared streams, oversize/expiry
refusals, short EOF and close without draining. They use fixture raw streams,
not a socket or the proposed driver.

The proposed `ProtocolFileBinding` mixin intercepts `_rfile` assignment. The
bound connection source assigns a fresh `sock.makefile("rb")` in `connect`
(line 698) and a fresh `self._sock.makefile("rb")` after TLS wrapping (line 953).
Place this mixin before the pinned `Connection` in the future fixture class's
method resolution order, initialize its budget before the deferred driver
constructor, and retain the driver's packet parser and `_read_bytes` method.
The setter immediately detaches that fresh buffer and installs the budgeted raw
boundary before its first read. It shares the budget across replacements,
refuses to discard unread prefetched bytes, and closes a rejected newly owned
stream. `None` assignment closes without a read or a fresh deadline allowance.
Freshness of the incoming buffer is a requirement of these two exact source
sites; the helper does not certify an arbitrary buffer supplied after use.

Five additional inert controls exercise those assignment/replacement/close
boundaries with stock in-memory I/O. No PyMySQL class has been imported or
composed. Actual class composition and driver lifecycle qualification, TLS
handshake deadlines, authentication, result terminators, process ownership,
framing and cleanup remain unqualified. The collector vectors below remain
unexecuted, and all actual operating inputs remain unbound.

The process receives only the fixture identity and dedicated SELECT-only
fixture connection parameters through the future owner's private input channel.
Never obtain production endpoints, ambient credentials, config files, plugin
credentials or a user's home defaults. Network membership, peer identity,
authentication, grants and TLS trust must be fixed in the later input packet.
credentials or a user's home defaults. The [source/input decision](https://github.com/durable-workflow/sdk-python/issues/123#issuecomment-6673104109)
selects a dedicated SELECT-only fixture identity using `caching_sha2_password`
over required verified TLS, with peer hostname `mysql` and port 3306. This is
new reconstruction input, not recovered original authentication provenance.
Actual network/peer, CA, hostname-valid certificate, grants, backend/platform
and full installed closure remain unbound. No account/certificate/key creation
is selected by the source decision.
No opportunistic RSA package installation, plaintext fallback, server-key fetch,
reconnect, discovery or endpoint substitution is permitted.
For the proposed TCP path, supply an explicit SSLContext with the selected CA,
certificate verification and hostname verification. This pinned driver makes
explicit SSL configuration required, while its default preferred mode can fall
back to plaintext. Default TLS options are therefore inadmissible. The exact
context, peer and authentication closure still require selection and controls.
context, peer and authentication closure still require concrete binding and
controls. Ambient CA defaults and authentication plugin switches are refused.

The driver source has consequential defaults:

Expand All @@ -63,7 +97,7 @@ The driver source has consequential defaults:
to show autocommit before either SELECT; refuse an inherited false state
without changing it. No transaction or time-zone change is proposed.
- Socket read/write timeouts bound socket operations, not the complete pair.
Buffered row reading and multipart packets have no proposed receive bound.
Stock buffered row reading and multipart packets have no such receive bound.
`max_allowed_packet` is not a receive-size limit.
- Default converters produce Python values including datetimes. The proposed
raw path disables Unicode and type-code decoders, retaining only the driver's
Expand All @@ -72,7 +106,8 @@ The driver source has consequential defaults:

These facts are bound to [connection source](https://github.com/PyMySQL/PyMySQL/blob/c990e86083c26493f3285b1e047902994c4b1811/pymysql/connections.py),
[cursor source](https://github.com/PyMySQL/PyMySQL/blob/c990e86083c26493f3285b1e047902994c4b1811/pymysql/cursors.py)
and [converter source](https://github.com/PyMySQL/PyMySQL/blob/c990e86083c26493f3285b1e047902994c4b1811/pymysql/converters.py).
[converter source](https://github.com/PyMySQL/PyMySQL/blob/c990e86083c26493f3285b1e047902994c4b1811/pymysql/converters.py)
and [authentication source](https://github.com/PyMySQL/PyMySQL/blob/c990e86083c26493f3285b1e047902994c4b1811/pymysql/_auth.py).
The future adapter is still missing; stock driver use alone satisfies none of
the complete-read, byte-limit, absolute-deadline or cleanup obligations below.

Expand All @@ -88,14 +123,20 @@ SQL, quote them locally, log `mogrify`, or allow arbitrary SQL/token replacement
The driver's parameter API is documented [here](https://pymysql.readthedocs.io/en/latest/modules/cursors.html#pymysql.cursors.Cursor.execute).
It performs driver escaping, not server-side prepared execution.

The proposed raw reader checks each requested byte count **before** calling the
pinned driver's `_read_bytes`. It refuses a request above 32 KiB or cumulative
receive reservation above 64 KiB for the entire connection/pair, including
handshake, setup, descriptions, rows and terminators. This also bounds multipart
assembly: header reservations and every component body consume the same budget.
The proposed reader bounds the driver's logical requests and reserves raw
protocol reads, including buffered read-ahead, **before** their read/allocation
boundary. It refuses a request above 32 KiB or cumulative raw reservation above
64 KiB for the entire connection/pair, including greeting, auth/setup,
descriptions, rows and terminators. Multipart headers and every component body
consume the same protocol budget; cached bytes are not counted a second time.
These are MySQL protocol reservations, not a total TLS ciphertext, certificate
record or OpenSSL allocation cap. TLS negotiation, buffering and blocked I/O
still share the original absolute phase/case deadline and resource envelope.
They receive no separate allowance, and no custom TLS engine is proposed.
A failed read does not refund that reservation or permit a retry. The adapter
must be source-reviewed against this exact private method before adoption; it
has not been implemented or exercised here.
must bind both initial and post-TLS raw readers against the pinned connection
source before adoption. The mixin above is proposed source; its actual driver
composition and lifecycle have not been exercised here.
Construct with `defer_connect=True`, initialize the deadline/receive guard before
any handshake, and connect only after fixture identity/input validation. The
complete process lifetime and its output are also bounded by the owner. Send
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
"blob": "e1afd4f8e5171aa139bc6ec3c41c9a38a7252ba8",
"bytes": 1492,
"sha256": "0c3fbc0b53bdaad91e4036c511662d282e7a858192a37e952c0a822a8bf8f7ca"
},
{
"path": "pymysql/_auth.py",
"blob": "77123d731334783e31a82ff827d0eb469fbfb8c0",
"bytes": 7636,
"sha256": "0ac2af5973379f156136aefdac9da0c296e3246860999d3446c46325f337a9dc"
}
],
"status": "public-source-only-unexecuted-driver-proposal",
Expand Down Expand Up @@ -93,5 +99,6 @@
"bytes": 675,
"sha256": "025db37ab5311650d694f139941e206c2ae085d9b862426efa409a53ddd2ae13"
}
]
],
"source_input_decision": "https://github.com/durable-workflow/sdk-python/issues/123#issuecomment-6673104109"
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,19 @@
"defer_connect": true,
"guard_initialized_before_connect": true,
"driver_stdout_stderr": "discard; fixed-code framed output only",
"tcp_tls": "explicit SSLContext with selected CA, certificate and hostname verification; default preferred mode refused"
"tcp_tls": "explicit SSLContext with selected CA, certificate and hostname verification; default preferred mode refused",
"observer_authentication_proposal": {
"identity": "dedicated SELECT-only fixture observer",
"plugin": "caching_sha2_password",
"transport": "required verified TLS",
"host_identity": "mysql",
"port": 3306,
"ca_certificate_hostname_and_grants": null,
"original_historical_authentication_provenance": false,
"ambient_ca_or_credentials": false,
"plugin_switch_or_server_key_fetch": false,
"account_or_certificate_creation_selected": false
}
},
"proposed_bounds": {
"case_seconds": 30,
Expand All @@ -73,7 +85,8 @@
"observer_within_original_runner_cpu": 1,
"observer_within_original_runner_memory_mib": 512,
"independent_observer_container": false,
"additional_resources_selected": false
"additional_resources_selected": false,
"receive_budget_domain": "MySQL protocol bytes, including all raw read-ahead; not total TLS ciphertext or OpenSSL allocation"
},
"deadlines": {
"case": "case_started_monotonic + 30",
Expand Down Expand Up @@ -121,7 +134,7 @@
},
"implementation_steps": [
"closed SQL-token adapter and raw tuple normalizer with pure byte fixtures",
"pinned private-reader receive guard with inert fragmented-packet controls",
"bind initial and post-TLS protocol readers before their first read; retain inert read-ahead controls",
"absolute-budget and framed-output owner integration with inert blocked/trickling child controls",
"exact-source/input/owner packet and independent cleanup controls before historical execution"
],
Expand All @@ -145,5 +158,23 @@
"containers_started": 0,
"execution_admitted": false,
"qualification_result": null
},
"source_input_decision": "https://github.com/durable-workflow/sdk-python/issues/123#issuecomment-6673104109",
"protocol_reader_source": {
"primitive": "tests/worker_startup_buffered.py",
"buffer": "stock io.BufferedReader over a budgeted raw stream",
"logical_request_bound_before_buffer_allocation": true,
"raw_reservation_before_read": true,
"actual_driver_initial_and_post_tls_assignment_binding": null,
"tls_handshake_deadline_enforcement": null,
"actual_driver_imported": false,
"replacement_streams_share_supplied_phase_budget": true,
"proposed_assignment_mixin": "ProtocolFileBinding before pinned Connection; initialize budget before deferred constructor",
"pinned_fresh_buffer_assignment_lines": [
698,
953
],
"replacement_with_unread_protocol_data": "refuse",
"actual_driver_composition_and_lifecycle_qualification": null
}
}
Loading
Loading