latest

Bowmap · bowmap.git · Tickets

Branches > main > latest > files

Don't read/write bad values

Grahp, 2026-10-13

Commit: e81f338aeeca921a0901e9f5c328603475add5a0
Parent: af0a324

5 changed files:

src/net/bowmap/server/network/streams.clj modified (view file)

@@ -2,7 +2,7 @@   "Functions for reading and writing frames to Input and OutputStreams"   (:require    [net.bowmap.server.packet.frame :as frame]-   [net.bowmap.server.packet.readers :refer [max-varint-size]])+   [net.bowmap.server.packet.varint :as varint])   (:import    [java.io IOException InputStream OutputStream])) @@ -15,19 +15,17 @@       (throw (IOException. "EOF while reading from InputStream"))       x))) +(def max-frame-bytes 2097151)+ (defn read-varint!!   "Blocks and reads a varint from an InputStream.-   Throws if EOF is reached, or if the encoded varint is invalid."+   Throws if EOF is reached, if the encoded varint is invalid, or if it is+   not a valid frame length."   [input-stream]-  (loop [num-read 0-         result 0]-    (if (>= num-read max-varint-size)-      (throw (IOException. "VarInt too long"))-      (let [b (read-byte!! input-stream)-            result' (bit-or result (bit-shift-left (bit-and b 0x7F) (* 7 num-read)))]-        (if (zero? (bit-and b 0x80))-          result'-          (recur (inc num-read) result'))))))+  (let [n (varint/decode-varint read-byte!! input-stream)]+    (if (<= 0 n max-frame-bytes)+      n+      (throw (IOException. (str "Invalid frame length: " n))))))  (defn read-frame!!   "Blocks and reads a frame from an InputStream.@@ -46,20 +44,19 @@ (defn write-varint!!   "Blocks and writes a varint to an OutputStream."   [output-stream n]-  (loop [n n]-    (let [b (bit-and n 0x7F)-          nxt (unsigned-bit-shift-right n 7)]-      (if (zero? nxt)-        (OutputStream/.write output-stream b)-        (do (OutputStream/.write output-stream (bit-or b 0x80))-            (recur nxt))))))+  (varint/encode-varint (fn [output-stream b]+                          (doto output-stream+                            (^[int] OutputStream/.write b)))+                        output-stream n))  (defn write-frame!!   "Blocks and writes a frame to an OutputStream.    Throws if the frame cannot be written."   [frame output-stream]-  (let [array (frame/array frame)]-    (doto output-stream-      (write-varint!! (alength array))-      (OutputStream/.write array)-      (OutputStream/.flush))))+  (let [^bytes array (frame/array frame)]+    (if (> (alength array) max-frame-bytes)+      (throw (IOException. "Frame exceeds maximum length."))+      (doto output-stream+        (write-varint!! (alength array))+        (OutputStream/.write array)+        (OutputStream/.flush)))))

src/net/bowmap/server/packet/readers.clj modified (view file)

@@ -7,7 +7,8 @@   (:require    [clojure.spec.alpha :as s]    [medley.core :refer [assoc-some]]-   [net.bowmap.server.packet.identifier :as identifier])+   [net.bowmap.server.packet.identifier :as identifier]+   [net.bowmap.server.packet.varint :as varint])   (:import    [io.netty.buffer ByteBuf]    [java.nio.charset StandardCharsets]@@ -41,15 +42,6 @@                 (read-key acc k reader buf))               {} pairs)))) -(def byte< ByteBuf/.readByte)-(def ubyte< ByteBuf/.readUnsignedByte)-(def short< ByteBuf/.readShort)-(def ushort< ByteBuf/.readUnsignedShort)-(def int< ByteBuf/.readInt)-(def long< ByteBuf/.readLong)-(def float< ByteBuf/.readFloat)-(def double< ByteBuf/.readDouble)- (defn apply<   "Returns a reader that calls f with the result of reading with each reader."   ([f reader]@@ -68,25 +60,39 @@   (fn [buf]     ((f (reader buf)) buf))) -(def max-varint-size 5)+(def byte< ByteBuf/.readByte)+(def ubyte< ByteBuf/.readUnsignedByte)+(def short< ByteBuf/.readShort)+(def ushort< ByteBuf/.readUnsignedShort)+(def int< ByteBuf/.readInt)+(def long< ByteBuf/.readLong)++(defn- finite+  "Returns its argument, or throws if it is not a finite floating point number."+  [x]+  (if (and (float? x) (Double/isFinite x))+    x+    (throw (ex-info "Value is not a valid float (cannot read NaN/Infinity)"+                    {:value x}))))++(def float< (apply< finite ByteBuf/.readFloat))+(def double< (apply< finite ByteBuf/.readDouble))  (defn varint< [buf]-  (loop [num-read 0-         result 0]-    (if (>= num-read max-varint-size)-      (throw (Exception. "VarInt too long"))-      (let [b (byte< buf)-            result' (bit-or result (bit-shift-left (bit-and b 0x7F) (* 7 num-read)))]-        (if (zero? (bit-and b 0x80))-          result'-          (recur (inc num-read) result'))))))+  (varint/decode-varint ubyte< buf))++(defn varlong< [buf]+  (varint/decode-varlong ubyte< buf))  (defn array<   "Returns a readers that 'length' number of values into a vector from    a buffer via 'reader'."   [reader length]-  (fn [buf]-    (into [] (repeatedly length #(reader buf)))))+  (if (nat-int? length)+    (fn [buf]+      (into [] (repeatedly length #(reader buf))))+    (throw (ex-info "Reading array length must be a non-negative integer."+                    {:length length}))))  (defn list<   "Returns a reader that reads a varint length, then reads that many values@@ -97,8 +103,36 @@   ([k v & kvs]    (list< (apply map< k v kvs)))) -(defn string< [buf]-  (ByteBuf/.readCharSequence buf (varint< buf) StandardCharsets/UTF_8))+(defn- within-valid-length?+  "Returns whether 'byte-count' number of UTF-8 bytes *may* be within+   'max-length' UTF-16 code points (or string characters) when encoded into a+   string. This can be used to avoid constructing strings that would be+   certainly too long."+  [max-length byte-count]+  (<= 0 byte-count (* 3 max-length)))++(def maximum-string-characters 32767)++(defn string<*+  "Returns a reader that reads a string as a varint byte length followed by its+   UTF-8 bytes. Throws when the string exceeds 'max-length' characters."+  [max-length]+  {:pre [(int? max-length)+         (<= 0 max-length maximum-string-characters)]}

Show all 137 lines

src/net/bowmap/server/packet/serialize.clj modified (view file)

@@ -23,7 +23,8 @@  (defn decode-frame   "Decodes a packet from a frame.-   Can throw if the frame does not contain a valid packet."+   Can throw if the frame does not contain a valid packet.+   Does not throw when the frame has leftover bytes."   [{:keys! [::phase/phase]} frame]   (let [buf (frame/->ByteBuf frame)         numeric-id (readers/varint< buf)

src/net/bowmap/server/packet/varint.clj added (view file)

@@ -0,0 +1,43 @@+(ns net.bowmap.server.packet.varint)++(defn encode-varlong+  "Calls 'rf' with the accumulator and each of the bytes of a varlong.+   When encoding a varint, turn the number into an unsigned long."+  [rf init n]+  (loop [n n+         acc init]+    (let [b (bit-and n 0x7F)+          nxt (unsigned-bit-shift-right n 7)]+      (if (zero? nxt)+        (rf acc b)+        (recur nxt (rf acc (bit-or b 0x80)))))))++(defn encode-varint+  [rf init n]+  (encode-varlong rf init (Integer/toUnsignedLong n)))++(defn- decode-varint*+  [read-byte src max-bytes]+  (loop [num-read 0+         result 0]+    (if (>= num-read max-bytes)+      (throw (ex-info "Cannot decode varint, too many bytes."+                      {:max-bytes max-bytes}))+      (let [b (read-byte src)]+        (if (and (= num-read 9) (> b 0x01))+          (throw (ex-info "Varint exceeds 64 bits while decoding"+                          {:last-byte b}))+          (let [result' (bit-or result (bit-shift-left (bit-and b 0x7F) (* 7 num-read)))]+            (if (zero? (bit-and b 0x80))+              result'+              (recur (inc num-read) result'))))))))++(defn decode-varint [read-byte src]+  (let [n (decode-varint* read-byte src 5)]+    (if (<= n 0xFFFFFFFF)+      (long (unchecked-int n))+      (throw (ex-info "VarInt exceeds 32 bits."+                      {:value n})))))++(defn decode-varlong [read-byte src]+  (decode-varint* read-byte src 10))

src/net/bowmap/server/packet/writers.clj modified (view file)

@@ -12,7 +12,9 @@    [net.bowmap.server.game.text :as text]    [net.bowmap.server.packet.identifier :as identifier]    [net.bowmap.server.packet.nbt :as nbt]-   [net.bowmap.server.packet.schema :as p])+   [net.bowmap.server.packet.readers :as readers]+   [net.bowmap.server.packet.schema :as p]+   [net.bowmap.server.packet.varint :as varint])   (:import    [io.netty.buffer ByteBuf ByteBufOutputStream]    [java.nio.charset StandardCharsets]@@ -59,14 +61,34 @@       (catch Exception e         (throw (ex-info "Failed to call writer with applied function" {:f f :writer writer} e)))))) -(def byte> ByteBuf/.writeByte)-(def ubyte> ByteBuf/.writeByte)-(def short> ByteBuf/.writeShort)-(def ushort> ByteBuf/.writeShort)-(def int> ByteBuf/.writeInt)-(def long> ByteBuf/.writeLong)-(def float> (apply> (comp Float/floatToRawIntBits float) int>))-(def double> (apply> Double/doubleToRawLongBits long>))+(defn- check-int+  "Returns a function that throws when its argument is not an integer within 'min' to 'max'."+  [min max]+  (fn [x]+    (if (and (int? x) (<= min x max))+      x+      (throw (ex-info "Integer out of size range."+                      {:min min+                       :max max+                       :value x})))))++(def byte>   (apply> (check-int -128 127) ByteBuf/.writeByte))+(def ubyte>  (apply> (check-int 0 255) ByteBuf/.writeByte))+(def short>  (apply> (check-int -32768 32767) ByteBuf/.writeShort))+(def ushort> (apply> (check-int 0 65535) ByteBuf/.writeShort))+(def int>    (apply> (check-int Integer/MIN_VALUE Integer/MAX_VALUE) ByteBuf/.writeInt))+(def long>   (apply> (check-int Long/MIN_VALUE Long/MAX_VALUE) ByteBuf/.writeLong))++(defn- finite+  "Returns its argument, or throws if it is not a finite floating point number."+  [x]+  (if (and (number? x) (Double/isFinite x))+    x+    (throw (ex-info "Value is not a valid float (cannot write NaN/Infinity)"+                    {:value x}))))++(def float>  (apply> (comp Float/floatToRawIntBits float finite) int>))+(def double> (apply> (comp Double/doubleToRawLongBits finite) long>))  (defn then>   "Returns a writer that writes (f x) to the buffer, then (g x)."@@ -83,15 +105,11 @@     (byte> buf 1)     (byte> buf 0))) -(defn varint> [buf n]-  (loop [buf buf-         n n]-    (let [b (bit-and n 0x7F)-          nxt (unsigned-bit-shift-right n 7)]-      (if (zero? nxt)-        (byte> buf b)-        (recur (byte> buf (bit-or b 0x80))-               nxt)))))+(def varlong> (apply> (check-int Long/MIN_VALUE Long/MAX_VALUE)+                      (partial varint/encode-varlong ubyte>)))++(def varint> (apply> (check-int Integer/MIN_VALUE Integer/MAX_VALUE)+                     (partial varint/encode-varint ubyte>)))  (defn array>   "Returns a writer that writes every value in a coll via 'writer'."@@ -112,26 +130,25 @@    (list> (apply map> k v kvs))))  (defn- write-string [buf s max-length]-  (let [bytes (String/.getBytes s StandardCharsets/UTF_8)]-    (if (> (count bytes) max-length)-      (throw (ex-info "String exceeds maximum length for writing."-                      {:max-length max-length-                       :actual-count (count bytes)-                       :string s}))+  (if (> (count s) max-length)+    (throw (ex-info "String exceeds maximum length for writing."+                    {:max-length max-length+                     :actual-length (count s)+                     :string s}))+    (let [bytes (String/.getBytes s StandardCharsets/UTF_8)]       (-> buf           (varint> (count bytes))           (ByteBuf/.writeBytes bytes)))))  (defn string>   "Writes a string as a varint byte length followed by its UTF-8 bytes.--   When passed 1 arg, returns a string writer that throws when the string-   exceeds the supplied maximum number of UTF-8 bytes."

Show all 194 lines