From 4fb2546ae805bed1b906849ad041a6f54efbe546 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Fri, 17 Dec 2021 13:44:01 -0500
Subject: [PATCH] Completely disallow non-None `new_length` in the mutable
 protocol

---
 src/_zkapauthorizer/_storage_server.py        | 18 ++++++
 src/_zkapauthorizer/tests/strategies.py       | 14 ++---
 .../tests/test_storage_server.py              | 60 ++++++++++++++++++-
 3 files changed, 83 insertions(+), 9 deletions(-)

diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py
index b847499..6599cc5 100644
--- a/src/_zkapauthorizer/_storage_server.py
+++ b/src/_zkapauthorizer/_storage_server.py
@@ -84,6 +84,17 @@ SLOT_HEADER_SIZE = 468
 LEASE_TRAILER_SIZE = 4
 
 
+class NewLengthRejected(Exception):
+    """
+    A non-None value for ``new_length`` was given to
+    ``slot_testv_and_readv_and_writev``.
+
+    This is disallowed by ZKAPAuthorizer because of the undesirable
+    interactions with the current spending protocol and because there are no
+    known real-world use-cases for this usage.
+    """
+
+
 @attr.s
 class _ValidationResult(object):
     """
@@ -438,6 +449,13 @@ class ZKAPAuthorizerStorageServer(Referenceable):
         # part of this call.
         now = self._clock.seconds()
 
+        # We're not exactly sure what to do with mutable container truncations
+        # and the official client doesn't ever use that feature so just
+        # disable it by rejecting all attempts here.
+        for (testv, writev, new_length) in tw_vectors.values():
+            if new_length is not None:
+                raise NewLengthRejected(new_length)
+
         # Check passes for cryptographic validity.
         validation = _ValidationResult.validate_passes(
             slot_testv_and_readv_and_writev_message(storage_index),
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index c439874..61b4019 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -917,13 +917,13 @@ def slot_test_and_write_vectors():
         TestAndWriteVectors,
         slot_test_vectors(),
         slot_data_vectors(),
-        one_of(
-            # The new length might be omitted completely.
-            just(None),
-            # Or it might be given as an integer.  Allow a zero size which
-            # means "delete this share" in this context.
-            sizes(),
-        ),
+        # The underlying Tahoe-LAFS storage protocol allows None or an integer
+        # here (new_length) to set the size of the mutable container.  The
+        # real Tahoe-LAFS storage client never uses this feature and always
+        # passes None.  There are undesirable interactions between non-None
+        # values for new_length and our spending protocol.  Therefore, we
+        # disable non-None values.  So don't generate any here.
+        just(None),
     )
 
 
diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py
index ba28632..fadd948 100644
--- a/src/_zkapauthorizer/tests/test_storage_server.py
+++ b/src/_zkapauthorizer/tests/test_storage_server.py
@@ -24,14 +24,14 @@ from time import time
 from allmydata.storage.mutable import MutableShareFile
 from challenge_bypass_ristretto import random_signing_key
 from foolscap.referenceable import LocalReferenceable
-from hypothesis import given, note
+from hypothesis import given, note, assume
 from hypothesis.strategies import integers, just, lists, one_of, tuples
 from testtools import TestCase
 from testtools.matchers import AfterPreprocessing, Equals, MatchesAll
 from twisted.internet.task import Clock
 from twisted.python.runtime import platform
 
-from .._storage_server import _ValidationResult
+from .._storage_server import _ValidationResult, NewLengthRejected
 from ..api import MorePassesRequired, ZKAPAuthorizerStorageServer
 from ..storage_common import (
     add_lease_message,
@@ -428,6 +428,62 @@ class PassValidationTests(TestCase):
             ),
         )
 
+    @given(
+        storage_index=storage_indexes(),
+        secrets=tuples(
+            write_enabler_secrets(),
+            lease_renew_secrets(),
+            lease_cancel_secrets(),
+        ),
+        sharenums=sharenum_sets(),
+        test_and_write_vectors_for_shares=slot_test_and_write_vectors_for_shares(),
+        new_length=integers(),
+    )
+    def test_mutable_new_length_rejected(
+        self, storage_index, secrets, sharenums, test_and_write_vectors_for_shares, new_length,
+    ):
+        """
+        If ``new_length`` is not ``None`` then ``slot_testv_and_readv_and_writev``
+        rejects the operation.
+        """
+        tw_vectors = {
+            k: v.for_call() for (k, v) in test_and_write_vectors_for_shares.items()
+        }
+        # Change some tw_vector to have a non-None new_length.
+        sharenum, (testv, writev, ignored) = tw_vectors.popitem()
+        tw_vectors[sharenum] = (testv, writev, new_length)
+
+        required_pass_count = get_required_new_passes_for_mutable_write(
+            self.pass_value,
+            dict.fromkeys(tw_vectors.keys(), 0),
+            tw_vectors,
+        )
+        valid_passes = get_passes(
+            slot_testv_and_readv_and_writev_message(storage_index),
+            required_pass_count,
+            self.signing_key,
+        )
+
+        # Try to do a write with the non-None new_length and expect it to be
+        # rejected.
+        try:
+            result = self.storage_server.doRemoteCall(
+                "slot_testv_and_readv_and_writev",
+                (),
+                dict(
+                    passes=_encode_passes(valid_passes),
+                    storage_index=storage_index,
+                    secrets=secrets,
+                    tw_vectors=tw_vectors,
+                    r_vector=[],
+                ),
+            )
+        except NewLengthRejected:
+            pass
+        else:
+            self.fail("expected a failure but got {!r}".format(result))
+
+
     @given(
         storage_index=storage_indexes(),
         secrets=tuples(
-- 
GitLab