You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by as...@apache.org on 2026/04/21 08:49:33 UTC
(tooling-agents) branch main updated: ASVS L1 audit: 2.3.1 (business_logic_voting)
This is an automated email from the ASF dual-hosted git repository.
asf-tooling pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-agents.git
The following commit(s) were added to refs/heads/main by this push:
new adfda98 ASVS L1 audit: 2.3.1 (business_logic_voting)
adfda98 is described below
commit adfda98bbebc2455f606c368dbfeaeefd70eab13
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Tue Apr 21 10:49:28 2026 +0200
ASVS L1 audit: 2.3.1 (business_logic_voting)
---
.../steve/d0aa7e9/business_logic_voting/2.3.1.md | 447 +++++++++++++++++++++
1 file changed, 447 insertions(+)
diff --git a/repos/apache/ASVS/reports/steve/d0aa7e9/business_logic_voting/2.3.1.md b/repos/apache/ASVS/reports/steve/d0aa7e9/business_logic_voting/2.3.1.md
new file mode 100644
index 0000000..7f05aed
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/d0aa7e9/business_logic_voting/2.3.1.md
@@ -0,0 +1,447 @@
+# ASVS 2.3.1 Security Audit Report
+## Apache STeVe — Business Logic Sequential Flow Enforcement
+
+---
+
+## Executive Summary
+
+- **Repository:** apache/steve
+- **Audit Date:** Apr 21, 2026
+- **Auditor:** Tooling Agents
+- **ASVS Requirement:** 2.3.1 — Verify that the application will only process business logic flows for the same user in the expected sequential step order and without skipping steps.
+ - **Level:** 1
+ - **Section:** Business Logic Security
+ - **Chapter:** Validation and Business Logic
+- **Files Analyzed:** 4 relevant out of 4 total, 0 skipped
+
+### Finding Summary
+
+| Severity | Count | Description |
+|----------|-------|-------------|
+| **Critical** | 2 | State enforcement bypasses and missing validation steps |
+| **High** | 0 | — |
+| **Medium** | 0 | — |
+| **Low** | 0 | — |
+| **Total** | 2 | Both findings enable complete bypass of sequential flow controls |
+
+---
+
+## Security Controls Inventory
+
+### State Transition Controls
+
+| Control | Defined In | Entry Points That Should Use It | Actually Called? | Enforcement Mechanism |
+|---------|-----------|-------------------------------|-----------------|----------------------|
+| `_all_metadata(required_state)` | `election.py:160-177` | All state-changing operations | Only 3 of 10 methods | `if/raise ElectionBadState` (reliable) |
+| `is_editable()` | `election.py:407-409` | `open()`, `add_issue()`, `edit_issue()`, `delete_issue()`, `add_voter()`, `delete()` | Yes, via `assert` | `assert` (unreliable – removed by `-O`) |
+| `is_open()` | `election.py:411-413` | `close()` | Yes, via `assert` | `assert` (unreliable – removed by `-O`) |
+| `is_closed()` | `election.py:415-417` | N/A (no methods assert closed) | N/A | `assert` (unreliable – removed by `-O`) |
+
+### Coverage Analysis – State Enforcement per Method
+
+| Method | Expected State | Mechanism | Reliable? | Gap Type |
+|--------|---------------|-----------|-----------|----------|
+| `add_vote()` (line 282) | `S_OPEN` | `_all_metadata(self.S_OPEN)` → raises `ElectionBadState` | ✅ Yes | — |
+| `tally_issue()` (line 300) | `S_CLOSED` | `_all_metadata(self.S_CLOSED)` → raises `ElectionBadState` | ✅ Yes | — |
+| `has_voted_upon()` (line 360) | `S_OPEN` | `_all_metadata(self.S_OPEN)` → raises `ElectionBadState` | ✅ Yes | — |
+| `open()` (line 68) | `S_EDITABLE` | `assert self.is_editable()` | ❌ No | **Type B** |
+| `close()` (line 120) | `S_OPEN` | `assert self.is_open()` | ❌ No | **Type B** |
+| `add_issue()` (line 206) | `S_EDITABLE` | `assert self.is_editable()` | ❌ No | **Type B** |
+| `edit_issue()` (line 225) | `S_EDITABLE` | `assert self.is_editable()` | ❌ No | **Type B** |
+| `delete_issue()` (line 238) | `S_EDITABLE` | `assert self.is_editable()` | ❌ No | **Type B** |
+| `add_voter()` (line 270) | `S_EDITABLE` | `assert self.is_editable()` | ❌ No | **Type B** |
+| `delete()` (line 46) | `S_EDITABLE` | `assert self.is_editable()` | ❌ No | **Type B** |
+
+---
+
+## Critical File Review
+
+### `v3/steve/election.py`
+
+| Line(s) | Function | Security Relevance | Status |
+|---------|----------|-------------------|--------|
+| 50 | `delete()` | `assert self.is_editable()` — state enforcement via removable assert | ❌ CRITICAL |
+| 70 | `open()` | `assert self.is_editable()` — state enforcement via removable assert | ❌ CRITICAL |
+| 123 | `close()` | `assert self.is_open()` — state enforcement via removable assert | ❌ CRITICAL |
+| 208 | `add_issue()` | `assert self.is_editable()` — state enforcement via removable assert | ❌ CRITICAL |
+| 228 | `edit_issue()` | `assert self.is_editable()` — state enforcement via removable assert | ❌ CRITICAL |
+| 241 | `delete_issue()` | `assert self.is_editable()` — state enforcement via removable assert | ❌ CRITICAL |
+| 273 | `add_voter()` | `assert self.is_editable()` — state enforcement via removable assert | ❌ CRITICAL |
+| 286 | `add_vote()` | `_all_metadata(self.S_OPEN)` — proper exception-based state check | ✅ Good |
+| 288 | `add_vote()` | `### validate VOTESTRING` — validation step entirely missing | ❌ CRITICAL |
+| 305 | `tally_issue()` | `_all_metadata(self.S_CLOSED)` — proper exception-based state check | ✅ Good |
+| 363 | `has_voted_upon()` | `_all_metadata(self.S_OPEN)` — proper exception-based state check | ✅ Good |
+
+### `v3/server/pages.py`
+
+| Line(s) | Endpoint | Security Relevance | Status |
+|---------|----------|-------------------|--------|
+| 383-424 | `do_vote_endpoint` | Delegates to `add_vote()` — state check reliable, vote validation absent | ⚠️ Affected by SEV-002 |
+| 447-463 | `do_open_endpoint` | Delegates to `election.open()` — relies on removable assert | ⚠️ Affected by SEV-001 |
+| 466-480 | `do_close_endpoint` | Delegates to `election.close()` — relies on removable assert | ⚠️ Affected by SEV-001 |
+| 483-507 | `do_add_issue_endpoint` | Delegates to `election.add_issue()` — relies on removable assert | ⚠️ Affected by SEV-001 |
+| 510-531 | `do_edit_issue_endpoint` | Delegates to `election.edit_issue()` — relies on removable assert | ⚠️ Affected by SEV-001 |
+| 534-551 | `do_delete_issue_endpoint` | Delegates to `election.delete_issue()` — relies on removable assert | ⚠️ Affected by SEV-001 |
+
+---
+
+## Findings
+
+### Critical Severity
+
+#### ASVS-231-CRT-001: Election Lifecycle State Enforcement Uses `assert` Statements — Control Exists But Is Removable (Type B Gap)
+
+**Severity:** CRITICAL
+
+**File:** `v3/steve/election.py`
+**Functions:** `open()` (line 70), `close()` (line 123), `add_issue()` (line 208), `edit_issue()` (line 228), `delete_issue()` (line 241), `add_voter()` (line 273), `delete()` (line 50)
+
+**Related Functions Checked:**
+- `_all_metadata()` (lines 160-177) — proper enforcement mechanism that exists but is not used
+- `is_editable()` (lines 407-409) — state check helper used via assert
+- `is_open()` (lines 411-413) — state check helper used via assert
+- `_compute_state()` (lines 424-437) — state derivation logic
+
+**Description:**
+
+Seven critical election lifecycle management methods rely exclusively on Python `assert` statements for state enforcement. When the application runs with the `-O` optimization flag (a standard production practice), all `assert` statements are removed from bytecode, completely eliminating state transition controls. This allows the election state machine (editable → open → closed) to be traversed in arbitrary order, including backwards transitions and step skipping.
+
+The codebase contains a proper enforcement mechanism (`_all_metadata(required_state)`) that raises `ElectionBadState` exceptions and is used correctly in voting-related methods (`add_vote()`, `tally_issue()`, `has_voted_upon()`), but this pattern was not applied to administrative lifecycle methods.
+
+**Vulnerable Code:**
+
+```python
+# election.py line 68-70
+def open(self, pdb):
+ # Double-check the Election is in the editing state.
+ assert self.is_editable() # <-- REMOVED by python -O
+
+# election.py line 120-123
+def close(self):
+ "Close an election."
+ # The Election should be open.
+ assert self.is_open() # <-- REMOVED by python -O
+
+# election.py line 206-208
+def add_issue(self, title, description, vtype, kv):
+ "Add a new issue with a generated unique IID."
+ assert self.is_editable() # <-- REMOVED by python -O
+
+# (same pattern for edit_issue, delete_issue, add_voter, delete)
+```
+
+**Contrasted with the proper pattern used elsewhere in the same file:**
+
+```python
+# election.py line 282-286 — CORRECT implementation
+def add_vote(self, pid: str, iid: str, votestring: str):
+ "Add VOTESTRING as the (latest) vote by PID for IID."
+ # The Election should be open.
+ md = self._all_metadata(self.S_OPEN) # <-- Raises ElectionBadState, NOT removable
+
+# election.py line 160-177 — The proper mechanism that EXISTS
+def _all_metadata(self, required_state=None):
+ md = self.q_metadata.first_row(self.eid)
+ if not md:
+ raise self._disappeared()
+ if required_state:
+ state = self._compute_state(md)
+ if state != required_state:
+ raise ElectionBadState(self.eid, state, required_state)
+ return md
+```
+
+**Data Flow:**
+
+- **Source:** Web endpoint (e.g., `do_open_endpoint` at `pages.py:447`) receives user request
+- **Sink:** State-changing method (e.g., `election.open()` at `election.py:68`) modifies election state
+- **Missing Control:** The `assert` statements on lines 50, 70, 123, 208, 228, 241, 273 are the ONLY barriers preventing out-of-order state transitions. Python's `-O` flag removes them entirely.
+
+**Proof of Concept:**
+
+When the application is run with `python -O` (a common production optimization):
+
+**1. Re-open an already-open election (destroys all existing votes):**
+```http
+GET /do-open/a1b2c3d4e5 HTTP/1.1
+Cookie: [valid committer session]
+```
+
+`election.open(pdb)` runs without hitting the removed `assert self.is_editable()`. This calls `add_salts()` which regenerates ALL mayvote salts, then generates a new `opened_key`. All previously cast votes become permanently undecryptable because their vote tokens were derived from the old salts.
+
+**2. Add issues to an open election (corrupts election integrity):**
+```http
+POST /do-add-issue/a1b2c3d4e5 HTTP/1.1
+Cookie: [valid committer session]
+Content-Type: application/x-www-form-urlencoded
+
+title=Malicious+Issue&description=injected
+```
+
+`election.add_issue()` runs without hitting the removed `assert self.is_editable()`. The issue is added to an active election. No mayvote entries with salts exist for it, and the `opened_key` tamper detection is retroactively triggered.
+
+**3. Delete issues from an open election (destroys active voting options):**
+```http
+POST /do-delete-issue/a1b2c3d4e5/f6g7h8i9j0 HTTP/1.1
+Cookie: [valid committer session]
+```
+
+**Impact:**
+
+- **Election integrity destruction:** Re-opening an active election regenerates cryptographic salts, making all previously cast votes permanently undecryptable. Voters' choices are silently and irreversibly destroyed.
+- **State machine bypass:** The election lifecycle (editable → open → closed) can be traversed in any order, including backwards (open → editable operations) or skipping (editable → closed).
+- **False confidence:** The `assert` statements create an appearance of state enforcement in code review while providing zero protection in optimized deployments. The proper mechanism (`_all_metadata(required_state)`) exists in the same file, proving the correct pattern was known but inconsistently applied.
+
+**Remediation:**
+
+Replace all `assert` statements used for state enforcement with the existing `_all_metadata(required_state)` mechanism:
+
+```python
+def open(self, pdb):
+ # Verify the Election is in the editing state (raises ElectionBadState if not)
+ self._all_metadata(required_state=self.S_EDITABLE)
+
+ self.add_salts()
+ edata = self.gather_election_data(pdb)
+ salt = crypto.gen_salt()
+ opened_key = crypto.gen_opened_key(edata, salt)
+ self.c_open.perform(salt, opened_key, self.eid)
+
+def close(self):
+ "Close an election."
+ # Verify the Election is open (raises ElectionBadState if not)
+ self._all_metadata(required_state=self.S_OPEN)
+ self.c_close.perform(self.eid)
+
+def add_issue(self, title, description, vtype, kv):
+ "Add a new issue with a generated unique IID."
+ self._all_metadata(required_state=self.S_EDITABLE)
+ if vtype not in vtypes.TYPES:
+ raise ValueError(f'Invalid vote type: {vtype}')
+ # ... rest of method
+
+def delete(self):
+ "Delete this Election and its Issues and Person/Issue pairs."
+ self._all_metadata(required_state=self.S_EDITABLE)
+ # ... rest of method
+
+# Apply same pattern to: edit_issue(), delete_issue(), add_voter(), add_salts()
+```
+
+---
+
+#### ASVS-231-CRT-002: Vote Content Validation Step Entirely Absent in Vote Submission Flow (Type A Gap)
+
+**Severity:** CRITICAL
+
+**File:** `v3/steve/election.py`, function `add_vote()`, lines 282-298
+**Called from:** `v3/server/pages.py`, function `do_vote_endpoint()`, lines 383-424
+
+**Related Functions Checked:**
+- `crypto.create_vote()` — encrypts votestring without validation
+- `crypto.gen_vote_token()` — generates encryption key
+- `q_get_mayvote.first_row()` — voter eligibility check (present and working)
+- `vtypes.vtype_module()` — vote type system exists but no validation interface
+
+**Description:**
+
+The vote submission flow skips the critical business logic step of validating vote content against the issue's vote type (YNA, STV, etc.) before encryption. The expected sequential steps are: (1) authenticate user, (2) verify election is open, (3) verify voter eligibility, (4) **validate vote content**, (5) encrypt and store vote. Step 4 is completely absent, acknowledged by a TODO comment that was never implemented.
+
+Raw user input travels directly from HTTP form fields to encrypted storage in the database. Invalid, malformed, or malicious vote strings are accepted and stored, with corruption only discovered during tallying when votes are decrypted and processed.
+
+**Expected Sequential Steps for Vote Submission:**
+
+1. ✅ Authenticate user → `@asfquart.auth.require({R.committer})`
+2. ✅ Verify election is open → `_all_metadata(self.S_OPEN)`
+3. ✅ Verify voter eligibility → `q_get_mayvote.first_row(pid, iid)`
+4. ❌ **Validate vote content matches issue type** → COMPLETELY MISSING
+5. ✅ Encrypt and store vote → `crypto.create_vote()`
+
+Step 4 is skipped entirely. The `votestring` travels directly from user input to encrypted storage without any domain validation.
+
+**Vulnerable Code:**
+
+```python
+# election.py lines 282-298
+def add_vote(self, pid: str, iid: str, votestring: str):
+ "Add VOTESTRING as the (latest) vote by PID for IID."
+
+ # The Election should be open.
+ md = self._all_metadata(self.S_OPEN)
+
+ ### validate VOTESTRING for ISSUE.TYPE voting # <-- TODO: validation NEVER implemented
+
+ mayvote = self.q_get_mayvote.first_row(pid, iid)
+ vote_token = crypto.gen_vote_token(md.opened_key, pid, iid, mayvote.salt)
+
+ # Pass MAYVOTE.SALT for PBKDF.
+ ciphertext = crypto.create_vote(vote_token, mayvote.salt, votestring)
+
+ self.c_add_vote.perform(vote_token, ciphertext)
+```
+
+```python
+# pages.py lines 397-415 — caller also performs no validation
+for iid, votestring in votes.items():
+ if iid not in issue_dict:
+ await flash_danger(f'Invalid issue ID: {iid}')
+ return quart.redirect(f'/vote-on/{election.eid}', code=303)
+
+ try:
+ election.add_vote(result.uid, iid, votestring) # votestring is raw user input
+```
+
+**Data Flow:**
+
+- **Source:** HTTP form field `vote-<iid>` submitted by user (`pages.py:393-395`)
+- **Sink:** `crypto.create_vote()` encrypts and stores the raw string (`election.py:293-295`)
+- **Missing Control:** No validation exists between receiving the vote string and encrypting it. The expected business logic step (validate vote against issue type) is acknowledged as missing via the TODO comment but never implemented.
+
+**Proof of Concept:**
+
+**For a YNA (Yes/No/Abstain) issue, submit an invalid vote:**
+```http
+POST /do-vote/a1b2c3d4e5 HTTP/1.1
+Cookie: [valid committer session with mayvote entry]
+Content-Type: application/x-www-form-urlencoded
+
+vote-f6g7h8i9j0=DEFINITELY_NOT_A_VALID_YNA_VALUE
+```
+
+**For an STV (Single Transferable Vote) issue, submit a corrupted ranking:**
+```http
+POST /do-vote/a1b2c3d4e5 HTTP/1.1
+Cookie: [valid committer session with mayvote entry]
+Content-Type: application/x-www-form-urlencoded
+
+vote-f6g7h8i9j0=zzz,zzz,zzz,zzz,zzz
+```
+
+Both requests succeed with "Votes submitted successfully!" The invalid votestring is encrypted and stored in the `vote` table.
+
+**Impact:**
+
+- **Election result corruption:** Invalid votes are encrypted and stored. During `tally_issue()`, the decrypted votestrings are passed directly to `vtypes.vtype_module(issue.type).tally(votes, kv)`. Depending on tally implementation, malformed votes could be silently miscounted, crash the tally process, or produce incorrect results.
+- **Irreversible data corruption:** Once encrypted and stored, there is no mechanism to distinguish valid from invalid votes without decrypting all of them. The damage is only discovered at tally time.
+- **Per-election scope:** A single malicious voter can corrupt results for every issue they're eligible to vote on in an election.
+
+**Remediation:**
+
+Implement the missing validation step using the existing `vtypes` module infrastructure:
+
+```python
+# In election.py, add_vote()
+def add_vote(self, pid: str, iid: str, votestring: str):
+ "Add VOTESTRING as the (latest) vote by PID for IID."
+
+ # The Election should be open.
+ md = self._all_metadata(self.S_OPEN)
+
+ # Fetch issue to determine vote type
+ issue = self.q_get_issue.first_row(iid)
+ if not issue:
+ raise IssueNotFound(iid)
+
+ # Validate votestring for the issue's vote type
+ vtype_mod = vtypes.vtype_module(issue.type)
+ if not vtype_mod.validate(votestring, self.json2kv(issue.kv)):
+ raise InvalidVoteString(iid, issue.type, votestring)
+
+ mayvote = self.q_get_mayvote.first_row(pid, iid)
+ vote_token = crypto.gen_vote_token(md.opened_key, pid, iid, mayvote.salt)
+ ciphertext = crypto.create_vote(vote_token, mayvote.salt, votestring)
+ self.c_add_vote.perform(vote_token, ciphertext)
+```
+
+```python
+# Each vtype module should implement validate():
+# vtypes/yna.py
+VALID_VOTES = {'yes', 'no', 'abstain'}
+def validate(votestring, kv):
+ return votestring.lower().strip() in VALID_VOTES
+
+# vtypes/stv.py
+def validate(votestring, kv):
+ candidates = set(kv.get('labelmap', {}).keys())
+ rankings = votestring.split(',')
+ return all(r.strip() in candidates for r in rankings) and len(rankings) <= len(candidates)
+```
+
+---
+
+## Positive Security Patterns
+
+| Pattern | Location | Assessment |
+|---------|----------|------------|
+| Proper state check via `_all_metadata(required_state)` | `election.py:160-177`, used in `add_vote()`, `tally_issue()`, `has_voted_upon()` | ✅ Exception-based, not removable by `-O` |
+| Election state derivation from database fields | `_compute_state()` at `election.py:424-437` | ✅ Derives state from actual column values rather than cached flags |
+| Cryptographic ID generation | `crypto.create_id()` used in `add_issue()`, `create()` | ✅ Prevents enumeration |
+| Integrity loop on ID collision | `while True / try / IntegrityError` at lines 209-214 and 453-458 | ✅ Handles concurrent ID creation safely |
+| Transactional operations for multi-step modifications | `BEGIN TRANSACTION / COMMIT` in `delete()` and `add_salts()` | ✅ Prevents partial state corruption |
+| Tamper detection via `opened_key` | `is_tampered()` at `election.py:387-405` | ✅ Detects post-opening modifications to election data |
+| Voter eligibility enforcement via `mayvote` table | `q_get_mayvote.first_row(pid, iid)` in `add_vote()` | ✅ Database-level eligibility check |
+| Re-voting support via `MAX(vid)` ordering | `vote` table with AUTOINCREMENT `vid` | ✅ Latest vote counted, old votes preserved for audit |
+
+---
+
+## Architecture Observations
+
+1. **Inconsistent enforcement patterns:** The codebase contains both reliable (`_all_metadata(required_state)`) and unreliable (`assert`) state enforcement mechanisms. The reliable mechanism was applied to the voting-specific methods but not to the election lifecycle management methods. This suggests the voting flow was hardened separately from the administrative flow.
+
+2. **Web layer delegates entirely to domain layer:** The `pages.py` handlers perform no independent state validation before calling `election.py` methods. This means the domain layer is the sole point of business logic enforcement. When that enforcement uses `assert`, there is no defense-in-depth.
+
+3. **Election lifecycle is well-modeled:** The three-state model (editable → open → closed) with clear database indicators (salt/opened_key presence for open, closed=1 for closed) is well-designed. The issue is purely in how transitions are guarded, not in the model itself.
+
+4. **Vote type system exists but incomplete:** The `vtypes` module provides an extensible framework for different voting methods, with proper tally implementations. However, the validation interface was never completed, leaving a critical gap in the vote submission pipeline.
+
+---
+
+## Recommendations Summary
+
+### Immediate (Critical Priority)
+
+| Priority | Finding | Effort | Impact |
+|----------|---------|--------|--------|
+| **1** | ASVS-231-CRT-001: Replace all 7 `assert` statements with `_all_metadata(required_state)` calls | Low — the replacement mechanism already exists and is proven in `add_vote()`/`tally_issue()` | Eliminates complete state bypass under `-O` flag |
+| **2** | ASVS-231-CRT-002: Implement vote content validation in `add_vote()` using per-vtype `validate()` functions | Medium — requires implementing validation logic for each vote type (yna, stv) | Prevents invalid votes from corrupting election results |
+
+### Short-term
+
+- **Add integration tests for `-O` mode:** Create test suite that runs with optimization flags enabled to catch similar issues
+- **Implement defense-in-depth in web layer:** Add state checks in `pages.py` endpoints before delegating to `election.py` methods
+- **Add pre-tally vote validation:** Implement a validation pass during `tally_issue()` that reports invalid votes rather than silently failing
+
+### Long-term
+
+- **Establish coding standards prohibiting `assert` for business logic:** Document that `assert` must only be used for developer-facing invariants, never for security or business rule enforcement
+- **Implement comprehensive state transition logging:** Add audit trail for all election state changes with before/after snapshots
+- **Add vote content preview/confirmation:** Allow voters to see their parsed vote before final submission to catch client-side corruption
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand file list (4 files)</summary>
+
+1. **`v3/docs/schema.md`** (relevance: 10)
+ - Documentation of database schema
+ - Election state model definition
+ - Vote storage structure
+
+2. **`v3/schema.sql`** (relevance: 10)
+ - Database table definitions
+ - State indicator columns (salt, opened_key, closed)
+ - Vote and mayvote table structure
+
+3. **`v3/server/pages.py`** (relevance: 10)
+ - Web endpoint handlers
+ - Entry points for state-changing operations
+ - Vote submission endpoint (`do_vote_endpoint`)
+
+4. **`v3/steve/election.py`** (relevance: 10)
+ - Core election business logic
+ - State transition methods
+ - Vote processing and validation (incomplete)
+
+</details>
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org