roc: `Num.intCast` enables segfaults by blessing negative `Nat`s

This is the root cause of https://github.com/rtfeldman/roc/issues/3377.

Here is an example of Num.intCast enabling a negative list capacity, which the REPL accepts:

» List.withCapacity -100

── TYPE MISMATCH ───────────────────────────────────────────────────────────────

The 1st argument to withCapacity is not what I expect:

4│      List.withCapacity -100
                          ^^^^

This argument is a number of type:

    I8, I16, I32, I64, I128, F32, F64, or Dec

But withCapacity needs the 1st argument to be:

    Nat


» List.withCapacity (Num.intCast -100)

[] : List a

» List.withCapacity (Num.intCast -100) |> List.sum |> Num.toStr

"0" : Str

» 

Here is an example of a compiled app rejecting the same negative list capacity:

[jan@framey roc]$ cat withoutIntCast.roc 
app "test"
    packages { pf: "examples/hello-world/platform/main.roc" }
    imports []
    provides [main] to pf

main = List.withCapacity -100 |> List.sum |> Num.toStr
[jan@framey roc]$ 
[jan@framey roc]$ 
[jan@framey roc]$ cargo run withoutIntCast.roc 
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/roc withoutIntCast.roc`
🔨 Rebuilding host...

── TYPE MISMATCH ────────────────────────────────────────── withoutIntCast.roc ─

The 1st argument to withCapacity is not what I expect:

6│  main = List.withCapacity -100 |> List.sum |> Num.toStr
                             ^^^^

This argument is a number of type:

    I8, I16, I32, I64, I128, F32, F64, or Dec

But withCapacity needs the 1st argument to be:

    Nat

────────────────────────────────────────────────────────────────────────────────

1 error and 0 warnings found in 582 ms.

You can run the program anyway with: roc run withoutIntCast.roc
[jan@framey roc]$ 
[jan@framey roc]$ 
[jan@framey roc]$ cat withIntCast.roc 
app "test"
    packages { pf: "examples/hello-world/platform/main.roc" }
    imports []
    provides [main] to pf

main = List.withCapacity (Num.intCast -100) |> List.sum |> Num.toStr
[jan@framey roc]$ 
[jan@framey roc]$ 
[jan@framey roc]$ cargo run withIntCast.roc 
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/roc withIntCast.roc`
🔨 Rebuilding host...
Segmentation fault (core dumped)
[jan@framey roc]$ 

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 16 (5 by maintainers)

Commits related to this issue

Most upvoted comments

Can we split this into two issues?

One about Num.intCast being unintuitive to new users and one about allocating giant amounts of memory (and if roc should do anything about it)?

🤔 Actually, the problem also occurs when calling

app "helloWorld"
    packages { pf: "platform/main.roc" }
    imports []
    provides [main] to pf

main = List.withCapacity 18446744073709551614 |> Str.joinWith ","

Here, 18446744073709551614 is definitely a valid Nat. So this can be considered a separate problem from Num.intCast. But internally inside the implementation of List.withCapacity, this very large unsigned number it is turned back into (or probably more accurately ‘reinterpreted as’) a signed number.

I don’t see why any function that takes a Nat should have to confirm that it’s not negative. Shouldn’t static type checking be enough? Negative Nats should just be impossible.

🤔 What about adding a debug_assert? That way, we will be able to debug problems related to it more quickly when doing tests on a debug build, while not slowing down production code.