Skip to content

Inside task { }, sprintf "%A %s" tuple x returns a closure typed as string (type-unsafe IL; .Length reads garbage) #20675

Description

@ludwigvonmises2

Repro (.NET SDK 10.0.112 / F# 10, net10.0 console, Debug and Release):

module Program

let typeName (value: obj) = if isNull value then "null" else value.GetType().FullName

let insideTask () = task {
    let pair = ("a", "b")
    let other = "c"
    let formatted : string = sprintf "%A %s" pair other
    printfn "task, tuple first, 2 args -> %s" (typeName (box formatted))
    printfn "task, formatted.Length    -> %d" formatted.Length }

let outsideTask () =
    let pair = ("a", "b")
    printfn "sync, tuple first, 2 args -> %s" (typeName (box (sprintf "%A %s" pair "c")))

[<EntryPoint>]
let main _ =
    insideTask().Wait()
    outsideTask ()
    0

Actual output:

task, tuple first, 2 args -> Program+formatted@10
task, formatted.Length    -> 1614856976      (a different garbage value on every build)
sync, tuple first, 2 args -> System.String

Expected: System.String and formatted.Length = 9 (("a", "b") c).

What we observed:

  • The value bound to formatted : string is a printf continuation closure, not a string. .Length does not throw. It reads arbitrary memory: the emitted IL passes an object where string is declared.
  • Triggers: sprintf or failwithf, with %A or %O, two or more arguments, and a reference tuple as the first argument, inside task { } or backgroundTask { }.
  • Does not trigger:
    • the tuple in a later position, or a single argument;
    • an enum, a union with fields, a record, an option, DateOnly or a struct tuple;
    • async { }, or the same code outside a computation expression;
    • interpolation ($"%A{pair} %s{other}").
  • Impact we hit: a failing Assert.True(false, sprintf "... %A ..." tuple ...) inside a task test made xUnit 2.6.2 crash its test host with ArrayTypeMismatchException, and dotnet test reported a partial run as successful. xUnit 2.9.3 counts the failure but loses the message.
  • Workaround: use string interpolation.
  • Searched existing issues: closest are Preserve resumable state-machine lowering in Debug #20469 (Debug-only resumable lowering) and Unexpected warning '[FS3511] This state machine is not statically compilable' #12839 (FS3511); neither matches. This one reproduces in Release too.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions