Memory safety is a property of a programming language that helps to prevent bugs in programs written in that language. This article describes Chapel’s memory safety features and how these features support Chapel’s goals of productivity and performance.

Chapel is designed to balance productivity, performance and scalability. As a result, its memory safety features are not as comprehensive as Python’s (where performance is not as important) or Rust’s (which has a design that focuses primarily on safety).

This table shows how we see Chapel as comparing to the other technologies studied here:

C/C++ Rust Python MPI OpenSHMEM Chapel
Productivity ✔️
Performance
Scalability
Safety ✔️

Key: ➕: great; ✔️: good; ➖: drawback

Since this article is focused on the safety aspect, we’ll consider how Chapel compares to Rust, C, C++, and Python when it comes to common memory-safety programming errors. The following table shows the errors we will discuss and summarizes how each language does:

Error C C++ Rust Python Chapel
Variable Not Initialized
Mishandling Strings ⚠️
Use-After-Free ⚠️ ⚠️ ⚠️
Out-of-Bounds Array Access ⚠️

Here are the meanings of ❌, ⚠️ , and ✅ for the purposes of this article:

This article also evaluates out-of-bounds array accesses in the context of communication in distributed-memory programming with MPI, OpenSHMEM, and Chapel. This table summarizes the result:

Error MPI OpenSHMEM Chapel
Out-of-Bounds in Communication ⚠️

Variable Not Initialized

Many programming languages provide a way to declare a variable without initializing it. When using such a language, it’s a common error to forget to initialize a variable. What happens if you make that error? We’ll demonstrate it in this section with a program that declares a local variable but doesn’t initialize it.

C and C++

In C and C++, it’s easy to declare a variable without initializing it, as this example shows:

1
2
3
4
5
6
7
#include <stdio.h>
int main() {
  int x;                  // OOPS! x is not initialized
  
  printf("x is %i\n", x);
  return 0;
}

Unfortunately, programs like this in C and C++ print out stack trash, that is, whatever memory happened to be stored in the memory used for the variable:

$ gcc unset-variable.c
$ ./a.out
x is 32764

This can lead to hard-to-find bugs and, in the context of software security, reveal information about a program to an attacker. The situation is a little better in C++ because, for many types that didn’t exist in C, variables using that type are automatically initialized. That applies to types like std::vector but not to the int used in this example, because int is a type that C++ inherited from C, and for which it needs to maintain compatibility.

Rust

Rust checks at compile-time that a variable is initialized before it is used. As a result, the program below won’t compile:

1
2
3
4
5
fn main() {
    let mut x: i64; // OOPS! forgot to initialize x
    let y = x;
    println!("y is {}", y);
}
$ rustc unset-variable.rs

error[E0381]: used binding `x` isn't initialized
 --> unset-variable.rs:3:13
  |
2 |     let mut x: i64; // OOPS! forgot to initialize x
  |         ----- binding declared here but left uninitialized
3 |     let y = x;
  |             ^ `x` used here but it isn't initialized
  |
help: consider assigning a value
  |
2 |     let mut x: i64 = 42; // OOPS! forgot to initialize x
  |                    ++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0381`.

Note that Rust checks that local variables are initialized even in unsafe blocks.

Python

In Python, it’s just not possible to declare a variable; instead variables are created the first time they are assigned to. So, this type of error just isn’t possible.

Chapel

In Chapel, variables are initialized to a default value if the type supports it. Some variables can’t be initialized to a default value, and in those cases, the Chapel compiler will emit an error if the variable is used before it is initialized.

For example, an int variable will be initialized to 0:

1
2
3
4
proc main() {
  var x: int;  // integer variables are set to 0 if not initialized
  writeln(x);
}
$ chpl unset-int-variable.chpl
$ ./unset-int-variable
0
(What if the variable can’t be initialized to a default?)

A variable can’t be initialized to a default if it is declared without a type, or if its type has no default value.

Chapel allows variables to be declared without a type. In this case the variable’s type will be inferred when it is initialized. That won’t work if it’s used before it is initialized, so that case results in an error:

proc main() {
  var x;
  writeln(x);
}
unset-untyped-variable.chpl:1: In function 'main':
unset-untyped-variable.chpl:2: error: 'x' is not initialized and has no type
unset-untyped-variable.chpl:2: note: cannot find initialization point to split-init this variable
unset-untyped-variable.chpl:3: note: 'x' is used here before it is initialized

See https://chapel-lang.org/docs/language/spec/variables.html#split-initialization for more details on this feature.

A class type like owned C is an example of a Chapel type that has no default value. Class types in Chapel can be nilable or non-nilable; meaning they can store nil or not. The type owned C is non-nilable — that is, a variable of that type can’t store nil. Since nil is the reasonable default value for classes and owned C can’t be nil, the variable
var x: owned C; can’t be initialized with a default. As a result, the compiler will give an error.

class C { }
proc main() {
  var x: owned C;
  writeln(x);
}
$ chpl unset-owned-variable.chpl
unset-owned-variable.chpl:2: In function 'main':
unset-owned-variable.chpl:3: error: cannot default-initialize x: owned C
unset-owned-variable.chpl:4: error: use here prevents split-init
note: non-nilable class type 'borrowed C' does not support default initialization
note: Consider using the type owned C? instead

We’ll discuss Chapel’s classes and memory management further in the Use-After-Free section.

Summary

How well do each of these programming languages protect against uninitialized memory?

Mishandling Strings

General-purpose languages need to provide ways to manipulate strings, as strings are a very common data type. To demonstrate, we’ll create a little program in each language that creates a string storing a greeting:

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>

#define MAX_GREETING 16

int main(int argc, char** argv) {
  char greeting[MAX_GREETING]; // C doesn't really have string support;
                               // here we allocate an array to store the
                               // greeting

                               // OOPS! allocated array might not be big enough

  strcpy(greeting, "Hello ");  // copy "Hello " into 'greeting'
  strcat(greeting, argv[1]);   // append the passed name to the greeting
  printf("%s\n", greeting);
  return 0;
}

It’s easy to cause a stack overflow for this program by providing a name longer than 16 characters:

$ gcc string-greeting.c
$ ./a.out abcdefghijklmnopqrstuv
Hello abcdefghijklmnopqrstuv
*** stack smashing detected ***: terminated
Aborted (core dumped)

That’s disastrous from a security perspective, and it could be exploitable. In practice, C programmers should know not to write code like this. A better program would count the sizes of the strings to be concatenated, allocate a new string on the heap, and use stpncpy and strlcat instead of strcpy and strcat.

C++, Python, Rust, and Chapel

These newer languages have improved on the situation in C and include a standard string type that avoids many of the error-prone patterns of string manipulation in C. In particular, appending to a string will resize it appropriately.

Since C programs can be valid C++ programs, it’s possible to write an unsafe program like the above in C++ too.

Here are the equivalent programs using the standard string type in these other languages:

string-greeting.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <string>
#include <iostream>

int main(int argc, char** argv) {
  std::string greeting = "Hello ";
  greeting += argv[1];                // append the passed name to the greeting
  std::cout << greeting << std::endl; // print the greeting
  
  return 0;
}
string-greeting.py
1
2
3
4
5
6
7
8
9
import sys

def main(args):
    greeting = "Hello "
    greeting += args[1]
    print(greeting)

if __name__ == "__main__":
    main(sys.argv)
string-greeting.rs
1
2
3
4
5
6
7
8
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut greeting = String::from("Hello ");
    greeting += &args[1];
    println!("{}", greeting);
}
string-greeting.chpl
1
2
3
4
5
config const who = "";  // enable command-line options like --who=world

var greeting = "Hello ";
greeting += who;
writeln(greeting);
Summary

C is uniquely bad at string manipulation, but the other languages provide mechanisms to avoid the most common issues.

Use-After-Free

When allocating memory dynamically, a potential problem is reading or writing memory that has already been freed.

C

C and C++ have a lot of flexibility with pointers. As a result, it’s very easy to read or write to memory that has been freed. This kind of error can cause all manner of problems, since the writes could overwrite other values in memory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {

  int* buf = malloc(sizeof(int));
  free(buf);

  buf[0] = 42; // OOPS: uses 'buf' after the memory is freed

  // do other heap operations to make heap corruption more visible
  {
    int* other_buf = malloc(sizeof(int));
    other_buf[0] = 120;
    free(other_buf);
  }

  printf("buf[0] is %i\n", buf[0]);
  return 0;
}

What happens when you compile and run such a program? If you are lucky, it will crash. If you are unlucky, the error will cause a difficult-to-detect data corruption issue.

$ clang use-after-free.c
$ ./a.out
a.out(38065,0x1fae94f40) malloc: Heap corruption detected, free list is damaged at 0x6000007bc020
*** Incorrect guard value: 96606699388929
a.out(38065,0x1fae94f40) malloc: *** set a breakpoint in malloc_error_break to debug
C++

C++ provides std::unique_ptr and std::shared_ptr to reduce the chances of a use-after-free because the free calls are automatically added. However, use-after-free is still possible.

For example, this program compiles, but it has a use-after-free:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// compile with
//   g++ use-after-free.cpp --std=c++14       

#include <iostream>
#include <memory>

int main(int argc, char** argv) {
  // allocate an integer on the heap
  auto buf = std::make_unique<int>(0);

  // create a reference that refers to the value on the heap
  int& ref_to_val = *buf;

  {
    // Replace the buffer with something else.
    // This causes the old buffer to be freed.
    buf = std::make_unique<int>(1);
  }

  ref_to_val = 42; // OOPS: uses 'buf' after the memory is freed

  // do other heap operations to make heap corruption more visible
  {
    buf = std::make_unique<int>(2);
    buf = std::make_unique<int>(3);
  }

  printf("buf[0] is %i\n", ref_to_val);
  return 0;
}

As with the similar C program, if you are lucky, the program will crash:

$ clang++ use-after-free.cpp --std=c++14     
$ ./a.out
a.out(47219,0x1fae94f40) malloc: Heap corruption detected, free list is damaged at 0x60000315c020
*** Incorrect guard value: 71734543777834
a.out(47219,0x1fae94f40) malloc: *** set a breakpoint in malloc_error_break to debug
Python

Python is a garbage-collected language, and so isn’t susceptible to use-after-free errors. It keeps memory allocated as long as it can be referred to.

Rust

The Rust compiler issues an error in this case to prevent a use-after-free.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn main() {
    // Allocate an integer value on the heap
    let mut buf: Box<i32> = Box::new(1);

    // Create a reference to the value
    let ref_to_val: &mut i32 = &mut *buf;

    *ref_to_val = 10; // modify the value through the reference
                      
    {
        buf = Box::new(2); // replace the pointer

        drop(buf);         // explicitly drop 'buf' to avoid compiler error
    }
    
    println!("value: {}", ref_to_val);
}
$ rustc use-after-free.rs
error[E0506]: cannot assign to `buf` because it is borrowed
  --> use-after-free.rs:11:9
   |
6  |     let ref_to_val: &mut i32 = &mut *buf;
   |                                --------- `buf` is borrowed here
...
11 |         buf = Box::new(2); // replace the pointer
   |         ^^^ `buf` is assigned to here but it was already borrowed
...
16 |     println!("value: {}", ref_to_val);
   |                           ---------- borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0506`.

Note that here, the borrow in the error message refers to the concept of having a pointer to something without having any concern about when that thing will be deallocated.

(The situation is different for unsafe code)

However, code in unsafe blocks is not protected against use-after-free and can produce undefined behavior:

fn main() {
    // Allocate a an integer value on the heap
    let mut buf: Box<i32> = Box::new(1);

    // Create a pointer to the value
    let ptr: *mut i32 = &mut *buf as *mut i32;

    unsafe {
        *ptr = 10;         // modify the value through the pointer
                           //
        buf = Box::new(2); // free the pointer
        println!("value: {}", *ptr); // OOPS: use-after free, prints garbage

        drop(buf);         // explicitly drop 'buf' to avoid compiler error
    }
}
$ rustc use-after-free-unsafe.rs
$ ./use-after-free-unsafe  
value: -1495007200
Chapel

Chapel provides automatic memory management for its types (arrays, strings, …) and owned and shared for class types to automatically manage freeing classes.

Chapel includes compile-time lifetime checking that catches common errors, but it is not exhaustive. It is designed to help programmers find problems in their programs without requiring a lot of programmer effort.

Here is an example of a program containing a use-after-free that is detected by Chapel’s lifetime checker. This program does not compile as a result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class C { var x: int; }

proc main() {
  // create a reference to a 'C' instance on the heap
  var b: borrowed C? = nil;

  {
    var instance = new owned C(0);
    b = instance.borrow();
  }

  b!.x = 42;

  // do other heap operations to make heap corruption more visible
  {
    var x = new owned C(2);
    var y = new owned C(3);
  }

  writeln(b);
}
$ chpl use-after-free-scoped.chpl
use-after-free-scoped.chpl:3: In function 'main':
use-after-free-scoped.chpl:12: error: applying postfix-! to dead value
use-after-free-scoped.chpl:12: note: 'b' refers to 'instance'
use-after-free-scoped.chpl:9: note: 'instance' is dead due to deinitialization here
use-after-free-scoped.chpl:9: error: Scoped variable b would outlive the value it is set to
use-after-free-scoped.chpl:8: note: consider scope of instance

In addition to owned, shared, and borrowed classes, Chapel supports unmanaged classes, which require the user to be responsible for freeing such memory when necessary, similar to classic C++. While this can be an important feature in some applications for generality and/or performance, its use is generally discouraged since it can potentially result in memory safety errors.

What are some errors that Chapel’s lifetime checker doesn’t detect?

As mentioned, Chapel’s lifetime checker takes a hands-off approach to unmanaged. Using unmanaged is inherently unsafe but it is sometimes necessary. Here’s an example of a use-after-free that is not detected at compile-time because the use of unmanaged opts out of the checking:

class C { var x: int; }

{
  var x = new unmanaged C(42);
  delete x;
  writeln(x);
}

Here is a case that has a use-after-free due to aliasing. This case goes beyond what we expect the Chapel compiler to handle.

class C { var x: int; }

{
  var x = new C(42);
  var b = x.borrow();  // b refers to the same class instance as x
  {
    x = new C(41);     // now x refers to a new class instance;
                       // the old class instance is deleted
  }
  writeln(b);          // use-after-free: b refers to a deleted instance
}
Summary

The languages vary greatly in the extent to which use-after-free is an issue:

Out-of-Bounds Array Access

Did you notice the bounds-checking errors in most of the string-greeting programs above? For example, the C program uses argv[1] but does not check that there was an argument passed. What does an out-of-bounds array access do in these languages?

C and C++

There is no array bounds checking in C or C++. In fact, it’s quite hard for a C compiler to provide array bounds checking, because, in practice, C code uses pointers as arrays, and there is not a consistent way for the compiler to know where the array length is stored.

For example, consider this program that creates a 1-element array and then accesses the ithi^{th} element based on a command-line argument:

out-of-bounds.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
  int idx = atoi(argv[1]); // convert the first argument into an int


  int array[1] = {0};      // allocate an array with space for 1 element
  int x = array[idx];      // access the array at 'idx'
                           // OOPS! no bounds checking

  printf("array at index %i is %i\n", idx, x);

  return 0;
}

If the command-line argument is not 0, it will lead to an out-of-bounds array access. At best, you get a program crash. At worst, you get a hard-to-find memory corruption bug.

$ gcc out-of-bounds.c
$ ./a.out 123456789
zsh: segmentation fault  ./a.out 123456789

The story with a C++ vector is similar:

out-of-bounds.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <vector>
#include <iostream>

int main(int argc, char** argv) {
  int idx = atoi(argv[1]);   // convert the first argument into an int

  std::vector<int> array(1); // create a vector with space for one element

  int x = array[idx];
  std::cout << "array at index " << idx << " is " << x << std::endl;

  return 0;
}
$ g++ out-of-bounds.cpp
$ ./a.out 123456789
zsh: segmentation fault  ./a.out 123456789

C and C++ developers use address sanitizers or similar tools to find this class of error. Additionally, the C++ standard library in use might have a way to activate bounds checking for some types, such as -D_GLIBCXX_DEBUG.

Python

Since Python includes array bounds checking, a similar program with an out-of-bounds array access will cause an IndexError: list index out of range error to be raised.

out-of-bounds.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import sys

def main(args):
    idx = int(args[1])
    array = [0]
    x = array[idx]
    print("array at index", idx, " is ", x) 

if __name__ == "__main__":
    main(sys.argv)
$ python3 out-of-bounds.py 123456789
Traceback (most recent call last):
  File "/Users/mferguson/chapel-blog/content/posts/memory-safety/code/out-of-bounds.py", line 10, in <module>
    main(sys.argv)
    ~~~~^^^^^^^^^^
  File "/Users/mferguson/chapel-blog/content/posts/memory-safety/code/out-of-bounds.py", line 6, in main
    x = array[idx]
        ~~~~~^^^^^
IndexError: list index out of range
Rust

Rust includes bounds checking, and failing a bounds check will cause the program to panic (print out a message and halt).

out-of-bounds.rs
1
2
3
4
5
6
7
8
9
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let idx = args[1].parse::<usize>().expect("need a number");
    let array: [i32; 1] = [0; 1];
    let x = array[idx];
    println!("array at index {} is {}", idx, x);
}
$ rustc out-of-bounds.rs
$ ./out-of-bounds 123456789
thread 'main' panicked at out-of-bounds.rs:7:13:
index out of bounds: the len is 1 but the index is 123456789
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Rust’s bounds checking is active even in unsafe blocks.

Chapel

For Chapel, out-of-bounds array accesses are checked by default, but disabled when the program is compiled with --fast or --no-checks.

When the program is compiled with bounds checks, the behavior is similar to the Rust program. The program halts and prints an error about the out-of-bounds access. For example, this program allocates a 10-element array and accesses an index provided on the command line:

out-of-bounds.chpl
1
2
3
4
5
6
7
config const idx = 1;    // enable command-line options like --idx=2

var array:[0..#10] int;  // allocate an array with space for 10 elements

var x = array[idx];

writeln("array at index ", idx, " is ", x);
$ chpl out-of-bounds.chpl
$ ./out-of-bounds --idx=123456789
out-of-bounds.chpl:5: error: halt reached - array index out of bounds
note: index was 123456789 but array bounds are 0..9

However, if the program is compiled with checks disabled, as with --fast, the out-of-bounds access causes undefined behavior. The program might crash, or it might print out garbage values.

$ chpl --fast out-of-bounds.chpl
$ ./out-of-bounds --idx=123456789
zsh: segmentation fault  ./out-of-bounds --idx=123456789

Chapel’s array bounds checks are disabled with --fast in order to achieve maximum performance. The expectation is that such program errors will be found and resolved during development and testing where bounds checking will be enabled.

Summary

Bounds checking in these languages varies from opt-in to opt-out to always on:

Out-of-Bounds Array Access in Distributed Memory

Chapel is a language designed for distributed-memory parallel computing, so we’ll compare Chapel with MPI and OpenSHMEM, which are distributed-memory parallel computing frameworks usable from C and C++.

What will happen with an out-of-bounds array access in the context of a distributed-memory parallel program?

C/C++ with MPI and OpenSHMEM

MPI and OpenSHMEM have a C interface that precludes bounds checking.

Here is a C and MPI example using MPI_Gather that provides a count too large that overflows the local buffer:

out-of-bounds-mpi.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <mpi.h>

int main(int argc, char** argv) {
  MPI_Init(&argc, &argv);
  int count = atoi(argv[1]); // convert the first argument into an int

  int myRank = 0;
  int numRanks = 0;
  MPI_Comm_rank (MPI_COMM_WORLD, &myRank);
  MPI_Comm_size (MPI_COMM_WORLD, &numRanks);

  size_t nPerRank = 1000;
  
  int* array = malloc(nPerRank*sizeof(int));
  int* gathered = malloc(numRanks*nPerRank*numRanks*sizeof(int));

  for (int i = 0; i < nPerRank; i++) {
    array[i] = myRank;
  }

  // Gather the first value from each array onto rank 0
  MPI_Gather(/* sendbuf */ array,
             /* sendcount */ 1,
             /* sendtype */ MPI_INT,
             /* recvbuf */ gathered,
             /* recvcount */ 1,
             /* recvtype */ MPI_INT,
             /* root */ 0,
             /* comm */ MPI_COMM_WORLD);

  if (myRank == 0) {
    printf("Correct gather:\n");
    for (int i = 0; i < numRanks; i++) {
      printf("  %i\n", gathered[i]);
    }
  }

  // What if there is an error in MPI_Gather?
  // If 'count' is larger than nPerRank, this version
  // will gather too much data and refer to invalid memory.
  MPI_Gather(/* sendbuf */ array,
             /* sendcount */ count,
             /* sendtype */ MPI_INT,
             /* recvbuf */ gathered,
             /* recvcount */ count,
             /* recvtype */ MPI_INT,
             /* root */ 0,
             /* comm */ MPI_COMM_WORLD);

  if (myRank == 0) {
    printf("Potentially bad gather:\n");
    for (int i = 0; i < count*numRanks; i++) {
      printf("  %i\n", gathered[i]);
    }
  }

  MPI_Finalize();
  return 0;
}

First, here’s what it looks like to compile and run it when there is no out-of-bounds access. In this case, any index less than 1000 will not lead to a memory safety violation.

$ mpicc out-of-bounds-mpi.c
$ mpirun -n 3 ./a.out 2
Correct gather:
  0
  1
  2
Potentially bad gather:
  0
  0
  1
  1
  2
  2

Providing a count beyond the size of the array leads to incorrect results or core dumps:

$ mpirun -n 3 ./a.out 123456789

Correct gather:
  0
  1
  2
[iris:24725] Read -1, expected 493827156, errno = 14
[iris:24725] *** Process received signal ***
[iris:24725] Signal: Segmentation fault (11)
[iris:24725] Signal code: Address not mapped (1)
[iris:24725] Failing at address: 0x5ebb9f222880
[iris:24726] *** Process received signal ***
[iris:24726] Signal: Segmentation fault (11)
[iris:24726] Signal code: Address not mapped (1)
[iris:24726] Failing at address: 0x55ae28cd6000
[iris:24725] [ 0] [iris:24726] [ 0] /lib/x86_64-linux-gnu/libc.so.6(+0x45250) [0x70976c845250]
[iris:24725] [ 1] /lib/x86_64-linux-gnu/libc.so.6(+0x45250) [0x731ed9c45250]
[iris:24726] [ 1] /lib/x86_64-linux-gnu/libc.so.6(+0x1ae906) [0x731ed9dae906]
[iris:24726] [ 2] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_btl_vader.so(+0x338d) [0x731ed800738d]
[iris:24726] [ 3] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_pml_ob1.so(mca_pml_ob1_send_request_schedule_once+0x1b9) [0x731ed3e51ab9]
[iris:24726] [ 4] /lib/x86_64-linux-gnu/libc.so.6(+0x1ae962) [0x70976c9ae962]
[iris:24725] [ 2] /lib/x86_64-linux-gnu/libopen-pal.so.40(opal_convertor_unpack+0x85) [0x70976cab4b55]
[iris:24725] [ 3] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_pml_ob1.so(mca_pml_ob1_recv_request_progress_frag+0x13f) [0x70976b05f5af]
[iris:24725] [ 4] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_btl_vader.so(mca_btl_vader_poll_handle_frag+0x95) [0x70976bc36d15]
[iris:24725] [ 5] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_btl_vader.so(+0x8064) [0x70976bc37064]
[iris:24725] [ 6] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_pml_ob1.so(mca_pml_ob1_recv_frag_callback_ack+0x211) [0x731ed3e50881]
[iris:24726] [ 5] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_btl_vader.so(mca_btl_vader_poll_handle_frag+0x95) [0x731ed800bd15]
[iris:24726] [ 6] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_btl_vader.so(+0x8064) [0x731ed800c064]
[iris:24726] [ 7] /lib/x86_64-linux-gnu/libopen-pal.so.40(opal_progress+0x34) [0x70976ca9d6f4]
[iris:24725] [ 7] /lib/x86_64-linux-gnu/libmpi.so.40(ompi_request_default_wait+0x55) [0x70976cc35ed5]
[iris:24725] [ 8] /lib/x86_64-linux-gnu/libopen-pal.so.40(opal_progress+0x34) [0x731ed9ab86f4]
[iris:24726] [ 8] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_pml_ob1.so(mca_pml_ob1_send+0x2b5) [0x731ed3e4f8f5]
[iris:24726] [ 9] /lib/x86_64-linux-gnu/libmpi.so.40(ompi_coll_base_gather_intra_linear_sync+0xd2) [0x731ed9f369e2]
[iris:24726] [10] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_coll_tuned.so(ompi_coll_tuned_gather_intra_dec_fixed+0x83) [0x731ed3e2c333]
[iris:24726] [11] /lib/x86_64-linux-gnu/libmpi.so.40(ompi_coll_base_gather_intra_linear_sync+0x38c) [0x70976cc90c9c]
[iris:24725] [ 9] /usr/lib/x86_64-linux-gnu/openmpi/lib/openmpi3/mca_coll_tuned.so(ompi_coll_tuned_gather_intra_dec_fixed+0x83) [0x70976b046333]
[iris:24725] [10] /lib/x86_64-linux-gnu/libmpi.so.40(PMPI_Gather+0x173) [0x731ed9effb93]
[iris:24726] [12] ./a.out(+0x1426) [0x55ae048c7426]
[iris:24726] [13] /lib/x86_64-linux-gnu/libmpi.so.40(PMPI_Gather+0x173) [0x70976cc59b93]
[iris:24725] [11] ./a.out(+0x1426) [0x5ebb76730426]
[iris:24725] [12] /lib/x86_64-linux-gnu/libc.so.6(+0x2a3b8) [0x70976c82a3b8]
[iris:24725] [13] /lib/x86_64-linux-gnu/libc.so.6(+0x2a3b8) [0x731ed9c2a3b8]
[iris:24726] [14] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b) [0x731ed9c2a47b]
[iris:24726] [15] ./a.out(+0x11a5) [0x55ae048c71a5]
[iris:24726] *** End of error message ***
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b) [0x70976c82a47b]
[iris:24725] [14] ./a.out(+0x11a5) [0x5ebb767301a5]
[iris:24725] *** End of error message ***
--------------------------------------------------------------------------
Primary job  terminated normally, but 1 process returned
a non-zero exit code. Per user-direction, the job has been aborted.
--------------------------------------------------------------------------
--------------------------------------------------------------------------
mpirun noticed that process rank 0 with PID 0 on node iris exited on signal 11 (Segmentation fault).
--------------------------------------------------------------------------

Similarly, an out-of-bounds array access in OpenSHMEM might lead to incorrect results, hard-to-reproduce bugs, or core dumps:

out-of-bounds-shmem.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <shmem.h>

int main(int argc, char** argv) {
  int idx = atoi(argv[1]); // convert the first argument into an int

  int myRank = 0;
  int numRanks = 0;

  shmem_init();

  myRank = shmem_my_pe();
  numRanks = shmem_n_pes();

  size_t nPerRank = 1000;

  int* array = (int*) shmem_malloc(nPerRank*sizeof(int));
  memset(array, 1, nPerRank*sizeof(int));

  if (myRank == numRanks-1) {
    int val = 42;
    shmem_int_put(array + idx, &val, 1, 0); // OOPS: idx might be out-of-bounds
    shmem_int_get(&val, array + idx, 1, 1); // OOPS: idx might be out-of-bounds
    printf("Got value %#x\n", val);
  }

  return 0;
}

In this case, the value we got should be 0x1010101 if it is valid:

$ oshcc out-of-bounds-shmem.c
$ oshrun -np 3 ./a.out  1
Got value 0x1010101

Providing an index beyond the array bounds leads to incorrect results and possibly core dumps:

$ oshrun -np 3 ./a.out  2000
Got value 0
$ oshrun -np 3 ./a.out  123456789
[iris][[45244,1],2][pshmem_put.c:156:pshmem_int_put] Required address 0x11c6f3524 is not in symmetric space
--------------------------------------------------------------------------
SHMEM_ABORT was invoked on rank 2 (pid 55917, host=iris) with errorcode -1.
--------------------------------------------------------------------------

Address sanitizers and similar tools are difficult to use in this context. Ideally, the network hardware provides support for shmem_int_put. As a result, the out-of-bounds access won’t necessarily even occur in code run by the processor! It’s likely to make the program halt, but it can be challenging to figure out what caused the out-of-bounds array access.

Distributed-Memory Programs in Chapel

A distributed Chapel program has the same level of bounds-checking support as a non-distributed Chapel program.

For example, this program creates a distributed array and then accesses the index provided on the command line (which might be out of bounds):

out-of-bounds-dist.chpl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use BlockDist;

config const idx = 1;    // enable command-line options like --idx=2

// create a distributed array storing 100 elements
var array = blockDist.createArray(0..#100, int);

var x = array[idx];

writeln("array at index ", idx, " is ", x);

First, let’s show what happens if the index is in bounds:

$ chpl out-of-bounds-dist.chpl
$ ./out-of-bounds-dist -nl 3 --idx=1
array at index 1 is 0

If the index is not within bounds, you get an out-of-bounds error at run-time (provided it is compiled with bounds checking on, e.g., without --fast):

$ ./out-of-bounds-dist -nl 3 --idx=1000
out-of-bounds-dist.chpl:8: error: halt reached - array index out of bounds
note: index was 1000 but array bounds are 0..99
Summary

Bounds-checking errors when using MPI or OpenSHMEM cause undefined behavior and can be challenging to debug. In contrast, Chapel is unique in providing bounds checking for distributed-memory programming.

Conclusion

Memory safety in Chapel provides for productivity, safety, and performance. Chapel is significantly safer than C and C++, and significantly safer than using MPI or OpenSHMEM for distributed-memory programming. Compared to Python, Chapel is able to achieve higher performance because it’s a compiled, statically-typed language, and it does not need a garbage collector. Compared to Rust, Chapel is able to provide safety when requested without requiring programmers to prove to the compiler that the code is correct.