Comments
63 total · 63 loaded
In a language like Rust, the compiler will “lock” the pointers for you, and you can’t forget.
In a language like C++ (and presumably Zig), one could, in theory at least, have the iterators and slices that reference the storage of a dynamic array hold some sort of lock that pins the storage.
But this API requires the programmer to remember to lock the pointers and also requires the programmer to keep the lock alive for the correct region of code. And it looks to me like even the example in the blog post has the lock taken completely outside the function that requires stability, so there is nothing whatsoever that gets the lock scoping right. Even the type system can’t help — the offending parse function can’t declare that it wants a pointer-locked ArrayList parameter.
“I use it in a lot of places where I know the max capacity ahead of time -- ensureCapacity() followed by a lot of AssumeCapacity()-styled commands. It's convenient for all of the ... convenience ... methods (append() requires some bookkeeping somewhere, appendSlice() requires more, and so on). In those usages, it's basically syntactic sugar over a slice”*
I suspect “where I know the max capacity ahead of time” covers most if not all use cases (if it you use this without knowing max capacity, you either accept your code may panic, or you do some unlock, grow, lock again dance when you discover your initial estimate is wrong)
If so, wouldn’t adding a growable container where you specify capacity at construction time and removing access to the internal pointers of ArrayList be a better way to handle this?
It's awkward to do get right because you need an indirect pointer whose address remains fixed, but points to another pointer which can change (and is volatile).
While it might be possible to make something like this lockless - it's much simpler to stick a mutex in the array header. When we access the array_segment we can take a lock to prevent some other thread reallocating mid-way through accessing.
There is a similar proposal for trait objects in rust.
Examples would be eg, `string_view` or `ArraySegment`. They hold some offset relative to a base allocation, and when we index the string_view or ArraySegment we're indexing relative to that offset.
Similarly, CPU architectures that use descriptors can (have to?) have languages with that notion.
`thread_local` is an example of a "relative pointer" though. Instructions to access the thread local are prefixed with `fs:` or `gs:`, and point relative to the address in the respective segment register.
A far pointer sounds like the global based pointer described in that article. The far pointer Wikipedia article says they are problematic but doesn't give much reasoning as to why.
GCC still supports `__seg_fs` and `__seg_gs`, which behave similar to `far` in the example on the wiki page, as the FS and GS segment registers are still valid in x86-64 and used for TLS. Clang uses attributes `address_space(257)` and `address_space(256)` for the same thing.
The `__based` pointer in MSVC exploits the addressing modes by pinning the base in eg: `[base+index*scale+displacement]`. It's unrelated to segmentation.
Project CHERI would like to disagree.
Segmentation isn't used. There's no separate registers to hold the bounds information in CHERI - the bounds are held in the pointer value, unlike for example, the now obsolete Intel MPX, which held bounds information in separate registers.
There's some similarity to segmentation because the CHERI pointer restricts which addresses can be accessed, but I wouldn't compare them to far pointers.
Most modern processors have a single linear virtual address space and don't use segmentation, and even where segment registers exist (eg, FS and GS on x86-64), they're only superficial "address spaces" - allocated sections of the process's linear virtual address space which could be accessed without segmentation registers if you knew the base address held in FS or GS.
So right now, when we want control, we need to give up some safety, but weaker things are still helpful.
Also, in low-level code, the problem of "I might forget to do something" sometimes clashes with the problem of "I need to see exactly what operations are done and where". Various kinds of implicitness help with the former at the expense of the latter.
I'm not saying this is universally better than other approaches, but many people who do serious low-level programming would prefer this.
This is a very, very, very common claim. And unfortunately I have no other way to describe it other than a strawman.
In 95% (at least) of the application that need systems programming (not to talk about all applications that don't necessarily need it but will benefit from the performance and it wasn't an option because C++ wasn't an option), you have at most 20% (wildly overestimating) of code that needs to be unsafe. The rest could be completely safe. And amongst code that must be unsafe, you can very commonly encapsulate it in some safe pattern. Many times even extract it to a reusable crate.
That is the point of Rust. Not avoiding unsafety, but limiting and encapsulating it. And evidence proves that to work (for example https://blog.google/security/rust-in-android-move-fast-fix-t...).
The most important reason unsafe code is harder to write than C or C++ is that you must keep soundness, something none of these languages have. But yes the different rules also play part (although: do you know a single C or C++ codebase that does not violate TBAA? Some just disable it in the compiler, making them non-standard, while some just leave it potentially exploitable).
But the most important answer is the empirical evidence like I brought above. We have empirical evidence C and C++ codebases cannot be secure. We have empirical evidence Rust codebases can, even with unsafe code. Therefore, Rust is safer, period.
> Do Rust libraries, including std, historically have had UB bugs?
Did C or C++ libraries, historically, have UB bugs? Sorry, that just amplifies the strawman.
> Can Miri catch everything?
Miri is a dynamic analyzer, aka. a sanitizer. It will catch anything you test. It's like in C and C++, except you only need it for unsafe code.
> Are all the rules of unsafe, pinning, etc. fully specified and easy to learn and reason about?
Fully specified? People are working on it (are C's and C++'s UB rules fully specified? I'll save you the answer: no. Yes there is a standard and it's woefully incomplete).
Easy to learn and reason about? Probably not, which is why not everyone should be writing unsafe code.
Possible to learn and reason about? Absolutely yes. Especially with existing and emerging dynamic and static analyzers.
> https://github.com/rust-lang/rust/blob/main/library/core/src... How large a percentage of the logic code there is inside of an unsafe block?
The claim isn't "there's no unsafe". You've linked one file out of an entire stdlib; it uses unsafe to implement its algorithm, and of all the Rust code that could exist, this has one of the highest requirements for being maximally performant.
Now if you'd said "most of the Rust std library is unsafe", or "most Rust code is unsafe, you'd have a good rebuttal. But that's not the case.
> And, if you have an unsafe block that is 100% correct, but it relies on safe code being correct, do you need to vet all that safe code? Potentially whole modules needing to be vetted?
Then the unsafe block is not 100% correct. I can slap a wrapper around memcpy and call it "safe", and say that if anyone passes wrong parameters it's their fault. Rust as a language says I'm at fault for saying it's safe though.
> Is unsafe Rust code generally harder to get correct than code in other languages, due to...
Harder than other systems programming languages? Having worked in a fair few, I disagree. Harder than "higher" level languages? Some of them yes, some of them no; I've seen "simple" languages admit very poor architectures, and fall in a "safe" heap when the project has to grow.
> Do Rust libraries, including std, historically have had UB bugs? https://materialize.com/blog/rust-concurrency-bug-unbounded-...
Are you suggesting this is a bar a language should achieve? Some examples of this would be interesting.
As for the rest, I don't think anything meets this bar you're setting. Certainly not languages that would otherwise be used where Rust is.
That's not "for instance", that's literally the only place Rust has unfixable UB on embedded (code on OS has other such things, e.g. reading/writing to `/proc/self/mem`).
> projects that need performance often use unsafe
You'll be surprised to hear how often it's not needed at all. And when it is, you'll be surprised to hear how many times you can still avoid it with some tricks. Contrary to popular belief, performance isn't the most common reason for unsafe (FFI probably is).
Those who would give up low-level control to purchase a little memory safety, deserve neither control nor safety.”
- Benjamin Franklin, or something like thatAlso note that this require storing twice as much information: an index and a pointer. So you're using twice as much memory, which affects your cache etc. (but what I wrote stands on its own even ignoring this.)
The proposed change doesn't do much for me personally (memory safety is ensured in other ways, and if it weren't I wouldn't be annoyed debugging the allocator-observed errors), but I could see myself using it at some other point in time for the same class of usages, or I could see other people relying on it when they choose that class of coding.
At least we got Deque in exchange. I use that far more often than I used SegmentedList.
Try running both of these examples. They only differ in a pair of curly braces.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
https://play.rust-lang.org/?version=stable&mode=debug&editio...
https://fasterthanli.me/articles/a-rust-match-made-in-hell
Mojo handles this significantly better than Rust.
I use Array list a lot so excited to add this throughout the code to harden them.
I can imagine this is not everyone's cup of tea, but then you probably also wouldn't enjoy any of the other explicitness.
I recently implemented a custom C++ container for a path whose components could be iterated, backed by a std::string. I just store indices and a reference to the string, such that my iterators are not invalidated if the std::string gets reallocated after being modified. Far less error prone for little added cost.
So an iterator takes an immutable reference to the vector and mutation requires a mut ref, and you can't have both at the same time.
const text =
\\This is a long comment
\\But I can split it among lines arbitrarily
\\And keep my indentation.
;
I've started using the Rust macro library `docstr` [1], which does the same thing: const TEXT: &'static str = docstr!(
/// Now I can do it in Rust, too.
/// I prefer this style a lot of the time
/// for long texts.
);
It even works with macros (example from the docs): let greeting: String = docstr!(format!
/// Hello, my name is {name}.
/// I am {} years old!
age
);
1. https://docs.rs/docstr/latest/docstr/ string text = """
This is a long comment
But I can split it
And keep my indentation.
"""; printf ("Things:\n"
" thing1=%u\n"
" thing2=%u\n"
" thing3=%u\n",
thing1,
thing2,
thing3);Changing a segfault to a panic with a stack trace is an improvement in developer experience. It does not make better software. The advantage of automatic strategies to mitigate memory safety mistakes either by using GC to make the program sound or static analysis to prevent the mistake by construction is plainly better.
There is a direction in some systems programming circles away from this by eschewing "complexity" (in other words, fixing the damn problems) for programs that have better error messages when the programmer made a mistake. I don't see that as better software.