Skip to content
Closed
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
3 changes: 3 additions & 0 deletions algorithmic/problems/59/interactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ int main(int argc, char* argv[]) {
registerInteraction(argc, argv);

int n = inf.readInt(1, 30000, "n");
// Seed the generator from a per-test secret in the answer file so the
// hidden array is not a fixed function of the default rng seed (#208).
rnd.setSeed(ans.readLong());
cout << n << endl;
cout.flush();

Expand Down
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/1.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2718281828
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/10.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1912931182
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/2.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3141592653
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/3.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1618033988
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/4.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1414213562
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/5.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2236067977
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/6.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1732050807
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/7.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2645751311
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/8.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1259921049
1 change: 1 addition & 0 deletions algorithmic/problems/59/testdata/9.ans
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1442249570
103 changes: 103 additions & 0 deletions algorithmic/tests/test_problem59.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""The problem 59 interactor must not be solvable with zero queries (#208).

Before the fix the interactor built its hidden array with testlib's rng at the
fixed default seed and never reseeded, so a program replaying that stream scored
100 without asking a single comparison. The fix reads a per-test secret seed from
the (hidden) answer file. These tests reconstruct the array the interactor will
use from the committed secret seed and feed it back with no queries: the array
from the committed seed is accepted, while the array from the old default seed is
rejected on every non-trivial test, proving the exploit is defeated.
"""
import os
import re
import subprocess
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[2]
DATA = ROOT / "algorithmic/problems/59/testdata"
TESTS = list(range(1, 11))

# testlib random_t: java-style 48-bit LCG.
_MUL, _ADD, _MASK = 0x5DEECE66D, 0xB, (1 << 48) - 1
_LLMAX = 9223372036854775807
_DEFAULT_SEED = 3905348978240129619


class _Rng:
def __init__(self, secret=None):
# registerInteraction leaves the default seed; setSeed(s) xors with the multiplier.
self.s = _DEFAULT_SEED if secret is None else ((secret ^ _MUL) & _MASK)

def _bits(self, bits):
if bits <= 48:
self.s = (self.s * _MUL + _ADD) & _MASK
return self.s >> (48 - bits)
return (self._bits(31) << 32) ^ self._bits(32) # lowerBitCount = 32 (version = -1)

def _next(self, n):
limit = _LLMAX // n * n
while True:
b = self._bits(63)
if b < limit:
return b % n

def nrange(self, a, b):
return a + self._next(b - a + 1)


def _hidden_array(n, secret):
rng = _Rng(secret)
a = list(range(1, n + 1))
for i in range(n):
j = rng.nrange(i, min(n - 1, i + 2))
a[i], a[j] = a[j], a[i]
return a


@pytest.fixture(scope="module")
def interactor(tmp_path_factory):
exe = tmp_path_factory.mktemp("interactor59") / "interactor"
subprocess.run(
["g++", "-std=gnu++17", "-O2", "-I" + str(ROOT / "algorithmic/judge/include"),
str(ROOT / "algorithmic/problems/59/interactor.cpp"), "-o", str(exe)],
check=True,
)
return exe


def _play(interactor, t, array):
"""Send `array` as the final answer with no queries; return (returncode, stderr)."""
inf, ans = DATA / f"{t}.in", DATA / f"{t}.ans"
proc = subprocess.Popen(
[str(interactor), str(inf), os.devnull, str(ans)],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
)
n = int(proc.stdout.readline())
proc.stdin.write("! " + " ".join(map(str, array)) + "\n")
proc.stdin.flush()
proc.stdin.close()
err = proc.stderr.read()
proc.wait()
return proc.returncode, n, err


@pytest.mark.parametrize("t", TESTS)
def test_correct_array_is_accepted(interactor, t):
secret = int((DATA / f"{t}.ans").read_text().split()[0])
n = int((DATA / f"{t}.in").read_text().split()[0])
rc, got_n, err = _play(interactor, t, _hidden_array(n, secret))
assert got_n == n
assert rc == 7, err # testlib _points: a correct answer scores
assert "Ratio: 1.0" in err


@pytest.mark.parametrize("t", TESTS)
def test_default_seed_replay_is_rejected(interactor, t):
n = int((DATA / f"{t}.in").read_text().split()[0])
if n < 2:
pytest.skip("n=1 has a unique array, so no seed can distinguish it")
rc, _, err = _play(interactor, t, _hidden_array(n, None))
assert rc != 7, "zero-query default-seed replay must no longer score (#208)"
assert "Wrong final array" in err