Skip to content

Pipe operator (|>) bypasses the untracked-call check; @allow_tracked_call placement hardening #1268

@mbouaziz

Description

@mbouaziz

1. Pipe operator bypasses the tracking check (pre-existing soundness hole)

check_tracking ("Cannot call an untracked function from a tracked context") has a single call site, in tfun_call. The N.Pipe case in expr_ (skiplang/compiler/src/skipTyping.sk, | N.Pipe(e1, e2) ->) builds the TAst.Call node directly: it joins the callee's type against an expected Tfun whose modifiers are the laxest possible ({Fmutable, Funtracked}) and never goes through tfun_call, so the tracking check never runs.

Repro — the direct call is rejected, the pipe is accepted:

untracked fun sideEffect(x: String): String {
  x
}

fun main(): void {
  print_raw(sideEffect("direct"))   // error: Cannot call an untracked function from a tracked context
}
untracked fun sideEffect(x: String): String {
  x
}

fun main(): void {
  print_raw("piped" |> sideEffect)  // accepted; compiles and runs
}

Verified against both the current main-based compiler and the #1161 branch: x |> f lets tracked code call any untracked function with no diagnostic. This predates #1161, but it undermines the same guarantee that PR is careful to preserve (its escape hatch, @allow_tracked_call / Unsafe.unsafe_untracked_call, is explicit and opt-in; the pipe bypass is silent).

Suggested fix: in the N.Pipe case, derive the callee's actual Tfun_modifiers (from ty2 after the join) and call check_tracking with them — or route pipe application through tfun_call so it gets the same treatment as ordinary calls (including @allow_tracked_call support). An invalid test (tracked context piping into an untracked function) should pin the fix.

2. Hardening: @allow_tracked_call is silently inert on methods

The bypass added in #1161 only matches direct calls of top-level functions (TAst.Fun callees). Placing @allow_tracked_call on a method parses fine and does nothing — the tracking check still fires. This is intentional and pinned by the allow_tracked_call_method invalid test, but a diagnostic at naming/typing time ("@allow_tracked_call has no effect on methods") would fail fast instead of surprising the author at the call site.

3. Hardening: @allow_tracked_call on a tracked function is a silent no-op

The annotation is only consulted when the callee is untracked and the context is tracked. Annotating a tracked function does nothing; a warning would catch annotations that are stale (e.g. after removing untracked from the function) or misplaced.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions