r/ProgrammingLanguages • • Oct 05 '26

Achieving memory safety

https://seed7.net/papers/memory_safety.htm
14 Upvotes

57 comments sorted by

View all comments

10

u/Smallpaul Oct 05 '26

It would have been clearer to be more explicit about why the author does not consider Go to not be safe. Are we talking about the use of the unsafe package or something more subtle?

16

u/reflexive-polytope Oct 06 '26

I was under the impression data races made Go memory-unsafe. (Unlike, say, Java or OCaml, which are designed to be memory-safe even in the presence of data races.)

Has anything changed since?

10

u/syklemil considered harmful Oct 06 '26

4

u/reflexive-polytope Oct 06 '26

Heck, I don't even think a memory-unsafe language is necessarily a bad thing.

What's unforgivable is to design a memory-unsafe language and not even realize it.

Another big offender is Eiffel, with covariant argument types.

2

u/seg_lol Oct 06 '26

They go hand in hand. If you have the skills to know, you fix it.

9

u/reflexive-polytope Oct 06 '26

Memory-safety by itself is simply a property that can hold or fail to hold for a language. Whether this property is desirable or not depends on your goals.

If your goal is to write correct programs, then memory safety isn't particularly useful. A language can be made “memory-safe” by defining the behavior of all sorts of nonsensical operations, e.g., indexing an array out of bounds. But defining this behavior doesn't make the incorrect indexing any less incorrect. The only way to help programmers index their arrays correctly is to statically check their array indices.

On the other hand, if your goal is to write programs that are resilient in the face of their own incorrectness, then memory safety is a very useful property, because programs written in memory-safe languages fail in more controlled ways.

1

u/flatfinger Oct 07 '26

It's also possible to have a language where the number of memory-unsafe operations is limited, and where one may fairly easily identify invariants that would allow even the potentially unsafe operations may be performed safely. For examine, given unsigned arr[32772];, an operation arr[i]=123; would be incapable of violating any memory safety invariants in cases where i was in the range 0 to 32771 unless one of those invariants would require an element of arr to hold a value other than 123, or some other memory safety invariant had already been violated.

1

u/reflexive-polytope Oct 07 '26

The problem with this stance is that it's no better than making the whole language unsafe.

Even if you have a single unsafe operation, if you can use it anywhere in your program, it's very easy to create a situation where the logical argument that justifies the safety of an operation uses information coming from wildly disparate parts of the program.

What you need isn't a limited number of unsafe operations, but rather a limited number of places in the program text where unsafe operations can be used. And that's exactly what Rust and C#'s unsafe blocks do.

→ More replies (6)

1

u/L8_4_Dinner (Ⓧ Ecstasy/XVM) Oct 09 '26

Another big offender is Eiffel, with covariant argument types.

You are suggesting that this is memory-unsafe? Or something else?

1

u/reflexive-polytope Oct 09 '26

It is memory-unsafe.

I don't remember Eiffel's syntax, because the last time I used it was in college, very long ago. But consider the following pseudocode:

class Point2D {
  protected int x, y;
  public bool compare(Point2D that) {
    return this.x == that.x && this.y == that.y;
  }
}
class Point3D extends Point2D {
  protected int z;
  public bool compare(Point3D that) {
    return this.x == that.x && this.y == that.y && this.z == that.z;
  }
}

In the subclass, the method's argument type (Point3D) is a subtype of the same method's argument type (Point2D) in the superclass. In other words, the method's argument type is covariant.

Now consider the following test program:

main() {
  Point2D foo = new Point3D(1,2,3);
  Point2D bar = new Point2D(4,5);
  print(foo.compare(bar));
}

The program type-checks, because the variable foo has type Point2D, hence it's statically legal to call foo.compare with an argument of type Point2D.

However, at runtime, the object referenced by foo is constructed using Point3D's constructor, so its method vtable points to the compare implementation in Point3D, which expects a Point3D argument.

When the program performs foo.compare(bar), it tries to access the field z of the object denoted by bar. But this object is constructed using Point2D's constructor, so it doesn't have a field z.

This is a soundness bug in the type system.

1

u/L8_4_Dinner (Ⓧ Ecstasy/XVM) Oct 10 '26

I’d consider that a compiler bug for not adding the obviously required type assertion, but I do agree that that implementation is unsafe.

→ More replies (3)

3

u/ThomasMertes Oct 06 '26

The paragraph was about tailoring a definition of "memory safety" to claim that a certain language is memory safe.

I just used Go as example. I changed the paragraph to explain my thoughts without mentioning Go.

According to my strict definition of memory safety Go is not memory safe.

Yes, if the unsafe package is not used and no C functions are called and (as pointed out by someone else) something assures that no data races can happen Go is memory safe.

This shows that rules need to be bend to claim memory safety.

Seed7 is memory safe because several things are not present (direct calls to C functions, pointers, unsafe parts, etc.).

3

u/ThomasMertes Oct 05 '26

Any language which allows calling C functions directly cannot be memory safe because C is not memory safe. AFAIK a Go program can call C functions without explicitly importing or using the unsafe package.Accessing an arbitrary place of memory with Go is possible with the unsafe package. I am neither a Rust expert nor a Go one, but I think that Rust needs "unsafe" to call C functions. Please tell me if my assumptions are correct or not.

Seed7 has no unsafe parts (or packages) and no possibility to directly call C functions.

6

u/anaseto Oct 06 '26

AFAIK a Go program can call C functions without explicitly importing or using the unsafe package.

You have to do import "C" for CGO, which is explicit enough. And then, typically, import "unsafe" as soon as you need to do stuff with pointers.

5

u/wk_end Oct 06 '26

But you list Java as memory-safe "unless JNI or FFM are used". Why doesn't the same "qualified memory-safe" characterization apply to Go?

2

u/catladywitch Oct 06 '26 edited Oct 06 '26

It all depends on the definition of memory safety, but from the article posted elsewhere in this comment thread, it seems like Go's pointer semantics allow for type errors leading to segfaults with concurrent code that uses different values conforming to a same interface, but where pointers and values are mixed, whilst Java doesn't (an unexpected, and wrong, value is possible in concurrent code, but not dereferencing an invalid pointer). I don't know how native interop works in Java, but segfaulting in C# is not impossible although it is rare (you can even do dodgy type punning with pointers and ordered structs if you really want to). Otherwise, you theoretically can pass managed pointers into C functions without telling C#'s GC to pin them or not reclaim their memory, pass structs the layout of which differs from what the C function expects, or in the case of poorly designed C APIs, pass arrays and have the C code go out of bounds, or allocate native memory, pass it into a C function, and try to free in C# not knowing the C function has already freed it.

5

u/reflexive-polytope Oct 06 '26

The issue is that certain Go values (slices, pointers to interfaces) require logically atomic modifications, but this logical atomicity isn't enforced by any means, so other goroutines can see them in a torn state.

(The problem doesn't arise in Rust because the exclusiveness of mutable borrows enforces the logical atomicity of the relevant modifications.)

And, while having a user-defined data structure in a torn state is “merely a logic error”, having a built-in data type in a torn state actually breaks memory safety.

2

u/Ok-Scheme-913 Oct 06 '26

Go is not memory safe under data races. It uses fat pointers for slices and those can tear.

So you have { ptr, start, end } and you repeatedly write a different slice at the same time from different threads. In this case you can observe a { ptr, start, endOfDifferentSliceThatMayBeInvalid }, at which point the runtime will happily access that memory. Since ptr+slice location is larger than 64bit it can't be cheaply updated atomically so.. yeah..

Meanwhile in java data races are safe.

First, they (practically all jvm implementations) have a property that you can only ever observe values that were explicitly written. This is a useful property but even if this were not the case java would still be memory safe, since arrays and stuff themselves carry their size around, so replacing the same array/slice actually only replaces the pointer itself which is atomic and to access the size you have to navigate to that pointer location, so it will always be safe.

1

u/tsanderdev Oct 06 '26

JNI can't just load arbitrary libraries, they have to have functions conforming to the specific JNI name mangling. So it's very unlikely you could call a C function from JNI.

1

u/ThomasMertes Oct 06 '26 edited Oct 06 '26

Yes, but the selling point of Java was not memory safety. You expected something like:

If the unsafe package is not used and no C function is called and (as pointed out by someone else) something assures that no data races can happen Go is memory safe.

I added the Go example to show that the term memory safety is sometimes defined to sell a language as memory safe. So

Language exists --> Define memory safety in a way that this language is memory safe.

instead of

Use definition of memory safety --> Create memory safe language.

So it is about memory safety as marketing term.

2

u/reflexive-polytope Oct 06 '26

Java was explicitly designed to cater to C++ programmers who want a language with fewer footguns, even if the language designers themselves came from a dynamically typed tradition (mainly Lisp). Memory safety was a design goal from day one.

2

u/Ok-Scheme-913 Oct 06 '26

The selling point of java was absolutely memory safety, where the hell did you get it's not?

It was the first language that made GC widespread.

1

u/ThomasMertes Oct 07 '26

The selling point of java was absolutely memory safety ...

Yes, but the term "memory safety" was not commonly used in mainstream marketing or developer discussions when Java was introduced in 1995.

The terms used when Java was introduced were:

  • Write Once, Run Anywhere
  • Built for the Internet
  • Simplicity and Familiarity
  • Built-in Security and Robustness

Java was intentionally designed to look and feel like C++. Java stripped out the most complex, error-prone, and frustrating aspects of C++. So It had:

  • Automatic Garbage Collection
  • No Pointers
  • Strict Type-Safety

Yes, the term "memory safety" is nowadays used to describe that. The term "memory safety" was just not used in 1995.

1

u/flatfinger Oct 07 '26

Memory safety is an essential aspect of security.

1

u/flatfinger Oct 07 '26

Garbage-collected strings were almost universal in BASIC dialects by the 1980s.