Skip to content

Commit ca0afd4

Browse files
committedFeb 10, 2025
Fixes from fuzzing testing
This slows down simple-array fixnum reading on sbcl because we need to protect against malicious data. It's still very very fast. If you need faster we can SIMD a check or something.
1 parent 3cbd661 commit ca0afd4

14 files changed

+126
-49
lines changed
 

‎benchmarks.lisp

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
(with-open-file (str "blarg.bin" :if-exists :supersede :if-does-not-exist :create
6262
:direction :output :element-type '(unsigned-byte 8))
6363
(cl-binary-store:store str data))))))
64+
;;(assert (equalp (cl-binary-store:restore store) data))
6465
(when read
6566
(timed (" READ :" repeats output-size-MB)
6667
(dotimes (x repeats) (cl-binary-store:restore store)))

‎src/array.lisp

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,35 @@
11
(in-package :cl-binary-store)
22

33
(defun restore-array (storage restore-object)
4-
(declare (type function restore-object))
4+
(declare (type function restore-object) (optimize (speed 3) (safety 1)))
55
(let* ((has-fill-pointer (funcall restore-object))
66
(fill-pointer (when has-fill-pointer (restore-tagged-unsigned-fixnum storage)))
77
(adjustable (funcall restore-object))
88
(array-rank (the (unsigned-byte 8) (restore-ub8 storage)))
99
(dimensions (loop repeat array-rank
1010
collect (restore-tagged-unsigned-fixnum storage)))
11-
(displaced (funcall restore-object)))
12-
(check-if-too-much-data (read-storage-max-to-read storage) (reduce #'* dimensions))
11+
(displaced (funcall restore-object))
12+
(array-total-size (reduce #'* dimensions)))
13+
(unless (and (typep array-total-size 'fixnum) (>= array-total-size 0))
14+
(unexpected-data "Array total size is too large"))
15+
(check-if-too-much-data (read-storage-max-to-read storage) array-total-size)
1316
(labels ((check-fill-pointer (dimensions)
1417
(when has-fill-pointer
1518
(unless (= array-rank 1)
1619
(unexpected-data "found fill-pointer for a non-vector"))
17-
(unless (<= fill-pointer (length (first dimensions)))
18-
(unexpected-data (format nil "fill-pointer ~A > dimensions ~A"
19-
fill-pointer dimensions))))))
20+
(unless (<= fill-pointer (first dimensions))
21+
(unexpected-data "fill-pointer > vector length")))
22+
(values)))
2023
(if displaced
2124
(let ((element-type (funcall restore-object))
2225
(offset (restore-tagged-unsigned-fixnum storage))
2326
(displaced-to (funcall restore-object)))
2427
(unless (typep displaced-to 'array)
25-
(unexpected-data "array" displaced-to))
28+
(unexpected-data "displaced to a non array?!"))
2629
(unless (typep (array-element-type displaced-to) element-type)
27-
(unexpected-data (format nil "array with element-type ~A" element-type)
28-
displaced-to))
30+
(unexpected-data "array displaced to array of different element-type"))
2931
(unless (< offset (array-total-size displaced-to))
30-
(unexpected-data (format nil "array of total size > ~A" offset)
31-
displaced-to))
32+
(unexpected-data "array displaced to too small array"))
3233
(when has-fill-pointer (check-fill-pointer dimensions))
3334
(make-array dimensions :element-type element-type :adjustable adjustable
3435
:fill-pointer fill-pointer :displaced-to displaced-to
@@ -42,7 +43,7 @@
4243
;; We need to make our array first in case any of the array elements refer to it!
4344
;; If we are ever referred to, then there will already be a fixup in place for
4445
;; our array handled by `restore-new-reference-indicator'.
45-
(loop for idx fixnum from 0 below (array-total-size array)
46+
(loop for idx fixnum from 0 below array-total-size
4647
do (restore-object-to (row-major-aref array idx) restore-object))
4748
array))))))
4849

‎src/basic-codespace.lisp

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"This is the basic codespace of cl-binary-store.")
1818

1919
;; Enable debug to get source code saved to a file so the debugger does the right thing
20-
(define-codespace ("basic codespace" +basic-codespace+ :debug nil)
20+
(define-codespace ("basic codespace" +basic-codespace+ :debug t)
2121
(register-references num-eq-refs (make-hash-table :test #'eq :size *num-eq-refs-table-size*))
2222
(register-references
2323
double-float-refs (make-hash-table :test #+sbcl #'double-float-= #-sbcl #'eql

‎src/cl-binary-store.lisp

+8-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,14 @@
214214
())
215215

216216
(defun unexpected-data (expected &optional (data nil data-provided-p))
217-
(error 'invalid-input-data :format-control "Expected ~A~A" :format-arguments (list expected (if data-provided-p (format nil ", found ~A" data) ""))))
217+
(error 'invalid-input-data
218+
:format-control "Expected ~A~A"
219+
:format-arguments (list expected
220+
(if data-provided-p
221+
;; be careful not to provide anything
222+
;; that cannot be printed trivially here!
223+
(format nil ", found ~A" data)
224+
""))))
218225

219226
(define-condition maybe-expected-error (invalid-input-data)
220227
()

‎src/hash-table.lisp

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
#-sbcl (declare (ignore synchronized weakness))
3232
;; weakness works as far as I can discern
3333
;; because of how we do reference restoration
34+
(unless (typep rehash-size '(or (integer 1 *) (float (1.0) *)))
35+
(unexpected-data "rehash-size is not correct"))
36+
(unless (< size (ash most-positive-fixnum -4))
37+
(unexpected-data "hash table too large"))
3438
(check-if-too-much-data (read-storage-max-to-read storage)
3539
(* 16 size)) ;; an estimate
3640
(make-hash-table :test test :size size

‎src/numbers.lisp

+11-11
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
(let* ((offset (read-storage-offset storage))
105105
(fixnum (signed-sap-ref-64 (read-storage-sap storage) offset)))
106106
(unless (typep fixnum 'fixnum)
107-
(unexpected-data "fixnum" fixnum))
107+
(unexpected-data "expected fixnum" fixnum))
108108
(setf (read-storage-offset storage) (truly-the fixnum (+ offset 8)))
109109
(truly-the fixnum fixnum)))
110110

@@ -232,17 +232,17 @@
232232

233233
(declaim (inline ensure-integer))
234234
(defun ensure-integer (x)
235-
(unless (integerp x)
236-
(unexpected-data "integer" x))
237-
x)
235+
(if (integerp x)
236+
x
237+
(progn (unexpected-data "expected an integer") 0)))
238238

239239
(defun restore-ratio (restore-object)
240240
(declare (optimize (speed 3) (safety 1)) (type function restore-object))
241241
(let ((a (ensure-integer (funcall restore-object)))
242242
(b (ensure-integer (funcall restore-object))))
243243
(declare (type integer a b))
244-
(unless (> b 0)
245-
(unexpected-data "integer > 0" b))
244+
(when (= b 0)
245+
(unexpected-data "ratio denominator is 0"))
246246
(/ (the integer a) (the integer b))))
247247

248248
(defun store-ratio (ratio storage num-eq-refs assign-new-reference-id)
@@ -261,9 +261,9 @@
261261

262262
(declaim (inline ensure-real))
263263
(defun ensure-real (x)
264-
(unless (typep x 'real)
265-
(unexpected-data "real" x))
266-
x)
264+
(if (typep x 'real)
265+
x
266+
(progn (unexpected-data "real") 0)))
267267

268268
(defun restore-complex (restore-object)
269269
(declare (type function restore-object))
@@ -328,8 +328,8 @@
328328
(#.+ub32-code+ (restore-ub32 storage))
329329
(#.+fixnum-code+
330330
(let ((fixnum (restore-fixnum storage)))
331-
(unless (>= fixnum 0)
332-
(unexpected-data "unsigned fixnum" fixnum))
331+
(unless (<= 0 fixnum (- most-positive-fixnum +interior-coded-max-integer+ 1))
332+
(unexpected-data "unsigned fixnum/interior" fixnum))
333333
(truly-the fixnum fixnum)))
334334
(otherwise (unexpected-data "tag for unsigned fixnum" tag)))
335335
+interior-coded-max-integer+ 1)))))

‎src/objects.lisp

+5-3
Original file line numberDiff line numberDiff line change
@@ -291,12 +291,14 @@
291291
(if (< num-slots 0) ; it's a reference id, look it up in our implicit tracking table
292292
(gethash num-slots implicit-eql-refs)
293293
(progn
294-
(check-if-too-much-data (read-storage-max-to-read storage) (* 8 num-slots))
294+
(if (> num-slots (ash most-positive-fixnum -3))
295+
(unexpected-data "too many slots in object-info" num-slots)
296+
(check-if-too-much-data (read-storage-max-to-read storage) (* 8 num-slots)))
295297
(let ((slot-name-vector (make-array num-slots))
296298
(type (funcall restore-object))
297299
(ref-id (- (the fixnum (incf (the fixnum (car implicit-ref-id)))))))
298300
(unless (symbolp type)
299-
(unexpected-data "symbol" type))
301+
(unexpected-data "expected a symbol"))
300302
;; No circularity possible below as these are symbols
301303
(loop for idx fixnum from 0 below num-slots
302304
do (setf (svref slot-name-vector idx) (funcall restore-object)))
@@ -385,7 +387,7 @@
385387
(declare (type function restore-object) (ignorable storage) (optimize speed safety))
386388
(let ((object-info (funcall restore-object)))
387389
(unless (object-info-p object-info)
388-
(unexpected-data "object-info" object-info))
390+
(unexpected-data "expected an object-info"))
389391
(let* ((specialized-deserializer (object-info-specialized-deserializer object-info))
390392
(constructor (object-info-specialized-constructor object-info)))
391393
(cond

‎src/pathname.lisp

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
:name (funcall restore-object)
2525
:type (funcall restore-object)
2626
:version (funcall restore-object))
27-
(error (e) (unexpected-data "pathname" e))))
27+
(error () (unexpected-data "pathname malformed"))))

‎src/reference-coding.lisp

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
(+ (- +minimum-untagged-signed-integer+)
4545
(truly-the fixnum number)
4646
1 +reference-two-byte-max-ref-id+))
47-
(unexpected-data "fixnum" number)))
47+
(unexpected-data "reference tag not valid")))
4848

4949
(declaim (inline encode-reference-direct))
5050
(defun encode-reference-direct (ref-index)

‎src/reference-count.lisp

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
(defmethod action ((code (eql +set-reference-action-code+)) storage references restore-object)
2020
(let ((num-refs (restore-tagged-unsigned-fixnum storage)))
2121
#+info-cbs(format t "This file has ~A references~%" num-refs)
22+
(unless (<= 0 num-refs (ash most-positive-fixnum -3))
23+
(unexpected-data "num-refs stored in file invalid"))
2224
(check-if-too-much-data (read-storage-max-to-read storage) (* 8 num-refs))
2325
(values (setf (references-vector references) (make-array num-refs :initial-element nil))
2426
:ignore)))

‎src/simple-array.lisp

+58-17
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
(18 (values '(unsigned-byte 62) 64))
8686
(19 (values '(unsigned-byte 64) 64))
8787
(otherwise
88-
(unexpected-data "encoded element type" encoded-element-type))))
88+
(unexpected-data "encoded element type"))))
8989

9090
(defconstant +first-direct-unsigned-integer+ 20)
9191
(defconstant +max-direct-encoded-unsigned-integer+ (- 255 +first-direct-unsigned-integer+))
@@ -95,10 +95,9 @@
9595
"Returns (values new-array array-bytes)"
9696
(declare (optimize (speed 3) (safety 1)) (type (unsigned-byte 8) encoded-element-type)
9797
(type fixnum num-elts))
98-
(unless (<= num-elts (ash most-positive-fixnum -3))
99-
(unexpected-data (format nil "num-elts smaller than ~A" (ash most-positive-fixnum -3)) num-elts))
98+
(unless (<= num-elts (ash most-positive-fixnum -7))
99+
(unexpected-data "num-elts too big"))
100100
(let ((num-elts num-elts))
101-
(declare (type (unsigned-byte 59) num-elts))
102101
(multiple-value-bind (type actual-bits)
103102
(encoded-element-type-to-type/packing encoded-element-type)
104103
(let ((total-bytes (ceiling (the fixnum
@@ -199,8 +198,8 @@
199198
(defun restore-simple-base-string (storage)
200199
(declare (optimize (speed 3) (safety 1)))
201200
(let ((num-bytes (restore-tagged-unsigned-fixnum/interior storage)))
202-
(unless (and (typep num-bytes 'fixnum) (>= num-bytes 0))
203-
(unexpected-data "unsigned fixnum" num-bytes))
201+
(unless (>= num-bytes 0)
202+
(unexpected-data "unexpected negative length for simple-base-string"))
204203
(check-if-too-much-data (read-storage-max-to-read storage) num-bytes)
205204
(let ((string (make-string num-bytes :element-type 'base-char)))
206205
(chunked/read storage num-bytes
@@ -417,6 +416,23 @@
417416
,@body
418417
(setf (read-storage-offset ,storage) (+ ,original-offset ,reserve-bytes)))))))
419418

419+
(defmacro sap-ref-fixnum (sap offset)
420+
"This is hideous. On SBCL we have stored the data fixnum encoded, which means shifted up
421+
by one so we shift it back to generate an 'unencoded' fixnum. We would normally just have
422+
blitted things back into the array without telling sbcl what we are doing, but in the
423+
presence of potentially malicious data, we have to make sure that our numbers are fixnums,
424+
so we shift them back down which makes it impossible for malicious actor to put bogus dat
425+
in the array sap. On non sbcl we just check to make sure things are fixnums because we
426+
did not blit the data out"
427+
#+sbcl
428+
`(ash (signed-sap-ref-64 ,sap ,offset) -1)
429+
#-sbcl
430+
(let ((a (gensym)))
431+
`(let ((,a (signed-sap-ref-64 ,sap ,offset)))
432+
(if (typep ,a 'fixnum)
433+
,a
434+
(unexpected-data "non fixnum in fixnum array")))))
435+
420436
(defun restore-simple-specialized-vector (storage)
421437
(declare (optimize (speed 3) (safety 1)))
422438
(let ((num-elts (restore-tagged-unsigned-fixnum/interior storage)))
@@ -449,18 +465,32 @@
449465
(32 (reader 32 t))
450466
(64 (reader 64 t))))))
451467
(t
452-
(ecase type
468+
(case type
453469
(bit (reader 1 nil))
454470
(single-float (reader 32 nil sap-ref-single (simple-array single-float (*))))
455471
(double-float (reader 64 nil sap-ref-double (simple-array double-float (*))))
456-
(fixnum (reader 64 t signed-sap-ref-64 (simple-array fixnum (*))))))))
472+
(fixnum (reader 64 t sap-ref-fixnum (simple-array fixnum (*))))
473+
(otherwise (unexpected-data "array of unexpected type"))))))
457474
#+sbcl
458475
(with-pinned-objects (sv)
459-
(let ((target-sap (vector-sap sv)))
460-
(chunked/read
461-
storage num-bytes
462-
(lambda (source-sap source-sap-offset data-start/bytes data-end-bytes)
463-
(copy-sap target-sap data-start/bytes source-sap source-sap-offset (the fixnum (- data-end-bytes data-start/bytes)))))))
476+
(multiple-value-bind (type bits-per-elt)
477+
(encoded-element-type-to-type/packing encoded-element-info)
478+
(declare (ignorable bits-per-elt))
479+
(cond
480+
((eq type 'fixnum)
481+
;; We have to be careful here, as just blitting potentially malicious 64
482+
;; bit data into a simple-array fixnum (*) can cause issues.
483+
;; Now the problem is that we already "fixnum encoded" the data if it is
484+
;; correct, which means that it needs to be un-fixnum-ized
485+
(reader 64 t sap-ref-fixnum (simple-array fixnum (*))))
486+
(t
487+
(let ((target-sap (vector-sap sv)))
488+
;; OK, so we have a problem here --- if the fixnums aren't actually fixnums
489+
;; then we end up screwing things up.
490+
(chunked/read
491+
storage num-bytes
492+
(lambda (source-sap source-sap-offset data-start/bytes data-end-bytes)
493+
(copy-sap target-sap data-start/bytes source-sap source-sap-offset (the fixnum (- data-end-bytes data-start/bytes))))))))))
464494
sv))))
465495

466496
;; for ccl ccl::array-data-and-offset would be fine... it's been a stable interface
@@ -511,8 +541,19 @@
511541
(type-of sa) num-bytes array-dimensions encoded-element-info)
512542
(with-pinned-objects (sa)
513543
(let ((target-sap (array-sap sa)))
514-
(chunked/read
515-
storage num-bytes
516-
(lambda (source-sap source-sap-offset data-start/bytes data-end-bytes)
517-
(copy-sap target-sap data-start/bytes source-sap source-sap-offset (- data-end-bytes data-start/bytes))))))
544+
(multiple-value-bind (type bits-per-elt)
545+
(encoded-element-type-to-type/packing encoded-element-info)
546+
(declare (ignorable bits-per-elt))
547+
(cond
548+
((eq type 'fixnum)
549+
;; We have to be careful here, as just blitting potentially malicious 64
550+
;; bit data into a simple-array fixnum (*) can cause issues.
551+
(sb-kernel:with-array-data ((sv sa) (start) (end))
552+
(declare (ignore start end))
553+
(reader 64 t sap-ref-fixnum (simple-array fixnum (*)))))
554+
(t
555+
(chunked/read
556+
storage num-bytes
557+
(lambda (source-sap source-sap-offset data-start/bytes data-end-bytes)
558+
(copy-sap target-sap data-start/bytes source-sap source-sap-offset (- data-end-bytes data-start/bytes)))))))))
518559
sa)))

‎src/simple-vector.lisp

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
(defun restore-simple-vector (storage restore-object)
1313
(declare (optimize speed safety))
1414
(let* ((num-elts (restore-tagged-unsigned-fixnum/interior storage)))
15+
(unless (< num-elts (ash most-positive-fixnum -3))
16+
(unexpected-data "simple vector too long"))
1517
(check-if-too-much-data (read-storage-max-to-read storage) (* num-elts 8))
1618
(let ((sv (make-array num-elts)))
1719
;; It's possible that we can refer to an

‎src/symbols.lisp

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
(defun ensure-string (maybe-string)
8080
(if (stringp maybe-string)
8181
maybe-string
82-
(progn (unexpected-data "string" maybe-string) "")))
82+
(progn (unexpected-data "expected a string, it was not") "")))
8383

8484
(declaim (inline restore-symbol))
8585
(defun restore-symbol (storage restore-object)

‎test/cl-binary-store-tests.lisp

+18-1
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,8 @@
592592
(define-test test-bignum
593593
(is '= (expt 2 64) (restore (store nil (expt 2 64))))
594594
(is '= 12345678901234567890 (restore (store nil 12345678901234567890)))
595-
(is '= -12345678901234567890 (restore (store nil -12345678901234567890))))
595+
(is '= -12345678901234567890 (restore (store nil -12345678901234567890)))
596+
(is '= (expt 2 123456) (restore (store nil (expt 2 123456)))))
596597

597598
(define-test test-write-into-extant-vector
598599
(loop for length in '(100 #-abcl 50000)
@@ -678,7 +679,23 @@
678679
(let ((a (make-array (file-length str))))
679680
(read-sequence a str)
680681
a))))
682+
(loop repeat 100 ;;10000
683+
with input = (make-array (random 100) :element-type '(unsigned-byte 8))
684+
do (loop for i fixnum below (length input) do (setf (aref input i) (random 256)))
685+
do (try input))
681686
(loop repeat 10
682687
with input = (make-array (random 1000000) :element-type '(unsigned-byte 8))
683688
do (loop for i fixnum below (length input) do (setf (aref input i) (random 256)))
684689
do (try input))))
690+
691+
(define-test simple-array-fixnum-malicious
692+
;; The below is a non-fixnum claiming to be in a fixnum array
693+
(finish
694+
(handler-case
695+
(cl-binary-store::restore #(21 5 3 127 127 127 127 127 127 127 127))
696+
(invalid-input-data ())))
697+
(finish
698+
(handler-case
699+
(cl-binary-store::restore #(21 5 3 127 127 127 127 127 127 127 127))
700+
(invalid-input-data ()))))
701+

0 commit comments

Comments
 (0)