Skip to content

Dependent function application can leak a scoped capability #17517

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

Closed
Linyxus opened this issue May 15, 2023 · 1 comment · Fixed by #17524
Closed

Dependent function application can leak a scoped capability #17517

Linyxus opened this issue May 15, 2023 · 1 comment · Fixed by #17524
Labels
cc-experiment Intended to be merged with cc-experiment branch on origin itype:bug
Milestone

Comments

@Linyxus
Copy link
Contributor

Linyxus commented May 15, 2023

Compiler version

main branch

Minimized code

import language.experimental.captureChecking
import java.io.*

object Test:
  class Logger(f: OutputStream^):
    def log(msg: String): Unit = ???

  def usingFile[sealed T](name: String, op: OutputStream^ => T): T =
    val f = new FileOutputStream(name)
    val result = op(f)
    f.close()
    result

  def usingLogger[sealed T](f: OutputStream^)(op: Logger^{f} => T): T = ???

  usingFile(
    "foo",
    file => {
      usingLogger(file)(l => () => l.log("test"))  // leaking the local logger
    }
  )  // should be error but it compiles

Output

It compiles, but the last lines should be rejected since the scoped capability is leaked in the result.

Expectation

There should be an error on the last lines.

@Linyxus Linyxus added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label cc-experiment Intended to be merged with cc-experiment branch on origin and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels May 15, 2023
@Linyxus
Copy link
Contributor Author

Linyxus commented May 15, 2023

This is a soundness problem related to capture set inference. To better see the problem we first inspect the typed tree of the usingFile application:

  usingFile[() ->?1 Unit](
    "foo",
    file => {
      usingLogger[() ->?2 Unit](file)(l => () => l.log("test"))  // leaking the local logger
    }
  )  // should be error but it compiles

The problem comes down to the inference of the capture set variables ?1 and ?2. What happens here? Let's inspect the flow of captured reference from the innermost of the expression:

  • First, the capture-checked type of l => () => l.log("test") will be (l : Logger^{file}) -> () ->{file} Unit.
  • After checking the conformance between the expected type and the actual type, the local parameter reference l is propagated to ?2.
  • Afterwards, l is propagated from ?2 to ?1.
  • When "healing" the captured references in ?1, we see that l is a TermParamRef and we widen it to its context capture set, which is {file}.
  • At this point, we should have further widen file to *, which would correct generate the error, but file here is a TermRef. Although it is a TermRef to a parameter bound by a later lambda, the current "healing" logic is not aware of it and does not widen it.
  • The code passes compilation, and we get a soundness problem.

odersky added a commit that referenced this issue May 22, 2023
- It changes `isParamDependent` to consider `CaptureDependency` as well,
so that when applying a capture dependent function the capture sets in
the types of the remaining parameters will be correctly substituted. For
example:
```scala
def foo[X](x: Foo[X]^, op: () ->{x} Unit): Unit = ???
foo(a, useA)
```
After rechecking `a` in the argument list, the expected type of the
second argument, `useA`, should become `() -> {a} Unit`. This example
previously does not work.

- It fixes #17517. It fixes the healing of capture sets, which happens
at the end of the capture checking phase, in which we traverse the
capture checked tree and try to heal the ill-formed capture sets in type
arguments. Here we say a capture set `C` in a type argument is
ill-formed if it contains `TermParamRef` bound by other lambdas. For
example:
```scala
def usingLogger[sealed T](f: OutputStream^)(op: Logger^{f} => T): T = ???
usingLogger[() ->?1 Unit](file)(l => () => l.log("test"))
```
`l` will be propagated to `?1` as a result of capture checking, which
makes `?1` ill-formed and we should widen `l` to `file`. The problem is
that we heal the capture sets one-after-another but the healing of a
later capture set may propagate more `TermParamRef`s to a healed one.
For example:
```
usingFile[() ->?2 Unit](  // should be error
  "foo",
  file => {
    usingLogger[() ->?1 Unit](file)(l => () => l.log("test"))
  }
)
```
After capture checking both `?1` and `?2` will be `{l}`. When traversing
the tree we first heal `?2`, wherein we widen `l` to `file`, but `file`
is a `TermRef` here and we do nothing. Then, only later when we heal
`?1`, we also widen `l` to `file` but this will end up propagating a
`TermParamRef` of `file` to `?2`, which we should have widened as well.
But `?2` is already healed and is not inspected again.

This PR solves this issue by installing a handler when healing the
capture sets which will try to heal the new elements propagated later in
the healing process.
@Kordyjan Kordyjan added this to the 3.3.1 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cc-experiment Intended to be merged with cc-experiment branch on origin itype:bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants