Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

redirect-generics etc. allow abusing method implementations #4935

Open
LiberalArtist opened this issue Feb 17, 2024 · 0 comments
Open

redirect-generics etc. allow abusing method implementations #4935

LiberalArtist opened this issue Feb 17, 2024 · 0 comments

Comments

@LiberalArtist
Copy link
Contributor

Using redirect-generics, chaperone-generics, impersonate-generics, etc., a client module can exfiltrate a server module's implementations of generic methods and apply them to arguments that break the server module's invariants. For example, consider the following program, where the definition of list-stream comes from the gen:stream documentation:

#lang racket
(module list-stream racket
  (struct list-stream (v)
    #:methods gen:stream
    [(define (stream-empty? stream)
       (empty? (list-stream-v stream)))
     (define (stream-first stream)
       (first (list-stream-v stream)))
     (define (stream-rest stream)
       (list-stream (rest (list-stream-v stream))))])
  (provide (contract-out
            [list-stream (-> list? list-stream?)])))
(require 'list-stream
         racket/generic)
(define list-stream-first
  (let/ec return
    (stream-first ; otherwise, `return` wouldn't be called
     (chaperone-generics gen:stream (list-stream '(1))
       [stream-first return]))
    (error "won't get here")))
(list-stream-first 'not-a-list-stream)
#|
list-stream-v: contract violation
  expected: list-stream?
  given: 'not-a-list-stream
|#

This seems like an instance of the problem from @capfredf and @samth's "Type Checking Extracted Methods" (related to TR's Self; see also the guide section). (Less directly, it also reminded me of the issue that inspired struct-guard/c.)

I think the argument to a method-proc-expr (i.e., in the above example, the version of stream-first passed to return) should be chaperoned to check that it is applied to a suitable argument. The contract for Self requires an argument eq? to the value to which the property accessor procedure was applied: in this case, it seems like it might be adequate to check that the values have the same generic method table, but @capfredf or @samth would have better insight into the nuances.

Some tangential observations:

  1. As the comment next to stream-first in the above example mentions, a method-proc-expr isn't called when the chaperone is created: instead, it seems to be called each time the corresponding method is called. This surprised me, and it doesn't seem to be required by the docs, which just say, "The impersonator applies the results of the method-proc-exprs to the structure’s implementation of the corresponding method-ids, and replaces the method implementation with the result."
  2. This expression produces an error without ever getting to the specific implementation of stream-first:
    (let/ec return
      (stream-first
       (chaperone-generics gen:stream (list-stream '())
         [stream-first return])))
    #|
    stream-first: contract violation
      expected: (and/c stream? (not/c stream-empty?))
      given: #<list-stream>
    |#
    That seems like a useful feature, and it also seems non-trivial to implement for a user-defined generic interface.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant