]> mop.ddnsfree.com - git repositories - brisk.git/commitdiff
add the probe for the short write to the bench
authorMatteo Nastasi <nastasi@alternativeoutput.it>
Sun, 13 Sep 2026 13:55:56 +0000 (15:55 +0200)
committerMatteo Nastasi <nastasi@alternativeoutput.it>
Sun, 13 Sep 2026 13:55:56 +0000 (15:55 +0200)
partial.py was written while chasing the truncated chunk but never
committed: it stayed in the scratch directory inside the container, so
the one tool that exercises the resumption after a short write against
the running daemon was not in the tree.

It opens a daemon socket directly, the way nginx does in direct http
mode, with an SO_RCVBUF small enough that the daemon cannot place the
whole first payload, waits without reading, then walks the chunked
framing and pairs the @BEGIN@/@END@ blocks.

It does not trigger the defect on its own: the room bootstrap is far
below the socket buffer, so the daemon never writes short. It is here to
check the framing against the real daemon, next to the socket pair
reproduction that did trigger it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

test/load/README
test/load/partial.py [new file with mode: 0644]

index 6f9bb68273ad65ffd683e05e6da5dde5fca93282..9b282a72dfb240af5efa8231d6f389dec4538125 100644 (file)
@@ -95,6 +95,13 @@ slowprobe.py    slow client: it connects and stops reading while a burst
                 arrives, to see whether the message queue overflows.
 burst.py        burst of simultaneous connections on the unix sockets: it
                 measures how many the backlog queues before refusing.
+partial.py      forces a short write on the daemon and checks what comes out
+                of it: it talks to a daemon socket directly with a tiny
+                SO_RCVBUF and stops reading, then validates the chunked
+                framing and the @BEGIN@/@END@ blocks. A chunk shorter than
+                its declared length, or one without its terminator, is the
+                resumption after the short write going wrong.
+                  partial.py [user] [socket] [rcvbuf] [pause]
 
 
 At the end of a session
diff --git a/test/load/partial.py b/test/load/partial.py
new file mode 100644 (file)
index 0000000..d210c7a
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+"""Forces a partial write on the daemon and checks what comes out.
+
+It logs in through nginx, then talks to one of the daemon unix sockets
+directly (which is what nginx itself does in direct http mode) with a tiny
+SO_RCVBUF and without reading for a while: the daemon's fwrite() cannot place
+the whole initial payload and returns short.
+
+Then it reads everything and validates the HTTP chunked framing plus the
+@BEGIN@...@END@ blocks the javascript client looks for. A truncated chunk or
+a block without its terminator means the resumption after the partial write
+is corrupt.
+"""
+import hashlib
+import re
+import socket
+import ssl
+import sys
+import time
+
+HOST, PORT, PFX = "127.0.0.1", 8444, "/brisk/"
+USER = sys.argv[1] if len(sys.argv) > 1 else "load390"
+SOCK = sys.argv[2] if len(sys.argv) > 2 else "/home/brisk/priv/brisk0.sock"
+RCVBUF = int(sys.argv[3]) if len(sys.argv) > 3 else 2048
+PAUSA = float(sys.argv[4]) if len(sys.argv) > 4 else 6.0
+
+
+def https(path):
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE
+    c = ctx.wrap_socket(socket.create_connection((HOST, PORT), 10), server_hostname=HOST)
+    c.sendall(("GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n"
+               % (path, HOST)).encode())
+    buf = b""
+    while True:
+        d = c.recv(65536)
+        if not d:
+            break
+        buf += d
+    c.close()
+    return buf
+
+
+chal = https("%sindex_wr.php?mesg=getchallenge&cli_name=%s" % (PFX, USER))
+tok = chal.rsplit(b"|", 1)[-1].strip().decode()
+priv = hashlib.md5((tok + hashlib.md5(USER.encode()).hexdigest()).encode()).hexdigest()
+m = re.search(rb'sess = "([0-9a-f]+)"',
+              https("%sindex.php?name=%s&pass_private=%s" % (PFX, USER, priv)))
+if not m:
+    print("login failed")
+    sys.exit(1)
+sess = m.group(1).decode()
+print("  session: %s" % sess)
+
+u = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+u.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, RCVBUF)
+u.connect(SOCK)
+req = ("GET %sindex_rd.php?sess=%s&stat=&subst=&step=-1&from=index_php&transp=xhr"
+       " HTTP/1.1\r\nHost: %s\r\nCookie: sess=%s\r\n\r\n" % (PFX, sess, HOST, sess))
+u.sendall(req.encode())
+print("  request sent, not reading for %.0f s (SO_RCVBUF=%d)" % (PAUSA, RCVBUF))
+time.sleep(PAUSA)
+
+u.settimeout(8)
+buf = b""
+t0 = time.monotonic()
+while time.monotonic() - t0 < 20:
+    try:
+        d = u.recv(4096)
+    except socket.timeout:
+        break
+    if not d:
+        break
+    buf += d
+    time.sleep(0.05)          # slow reader: keeps the pressure on
+u.close()
+print("  received: %d bytes" % len(buf))
+
+head, _, body = buf.partition(b"\r\n\r\n")
+print("  status: %s" % head.split(b"\r\n")[0].decode(errors="replace"))
+print("  declares chunked: %s" % (b"chunked" in head))
+
+# walk the chunked framing
+pos, chunks, errore = 0, 0, None
+while pos < len(body):
+    fine = body.find(b"\r\n", pos)
+    if fine < 0:
+        errore = "chunk header truncated at %d" % pos
+        break
+    try:
+        ln = int(body[pos:fine].split(b";")[0], 16)
+    except ValueError:
+        errore = "chunk length unreadable at %d: %r" % (pos, body[pos:fine][:40])
+        break
+    if ln == 0:
+        chunks += 1
+        break
+    if fine + 2 + ln + 2 > len(body):
+        errore = ("chunk of %d bytes declared but only %d available (truncated)"
+                  % (ln, len(body) - fine - 2))
+        break
+    pos = fine + 2 + ln + 2
+    chunks += 1
+
+print("  well formed chunks: %d" % chunks)
+print("  FRAMING: %s" % ("BROKEN -> " + errore if errore else "consistent"))
+
+aperti = body.count(b"@BEGIN@")
+chiusi = body.count(b"@END@")
+print("  @BEGIN@ %d / @END@ %d -> %s"
+      % (aperti, chiusi, "paired" if aperti == chiusi else "UNPAIRED"))