Java quiz


  • Discourse touched me in a no-no place

    @pleegwat said in Java quiz:

    you can probably even arrange a 64-bit refcount to overflow

    If refcounts are being kept correctly, that's rather hard to achieve. It'd require 8×264 bytes of memory (which is 147573953 TB or 147.5 exabytes) just to hold the pointer parts of the references themselves. You'll run out of memory long before you actually manage to create that many references.

    It'd also take quite a while to count up to that using simple increments. ;)


  • Considered Harmful

    @gąska TR:wtf: is not making mem::forget an unsafe function. That can't be sensible.


  • Banned

    @pie_flavor "safety" has very specific meaning in Rust. If there are no reads from unallocated memory and mutable references don't alias, it's safe. And forget() does neither.



  • @gąska "Ah, this is obviously some strange use of the word 'safe' that I wasn't previously aware of.”


  • Banned

    @scholrlea basically - in safe Rust, the only bugs you'll ever encounter are logic bugs and panics. And on release build, integer overflows.


  • Considered Harmful

    @gąska Destructors are guaranteed to be run when the value goes out of scope. By preventing them from being run, you implicitly introduce unsafety, because safe code could be relying on those guarantees. Destructors are part of Rust's memory safety. No different from Send and Sync being unsafe traits.


  • Banned

    @pie_flavor think about it: what exactly happens when you don't run destructors?


  • Considered Harmful

    @gąska Depends on the datatype, so pretty much anything. Memory freeing. Handle closing. Reference decrementing. Or the exact opposite. A destructor is a function that is run when the value is deallocated. There need not be any restrictions on what type of thing can be done. If safe code is built against a guarantee, then it should be unsafe to bypass that guarantee.


  • Banned

    @pie_flavor said in Java quiz:

    Memory freeing. Handle closing. Reference decrementing. Or the exact opposite.

    What's the consequence of never freeing memory? What's the consequence of never closing a handle? What's the consequence of never decrementing reference counter?

    These are definitely bugs and shouldn't happen. But they are of no consequence to the rest of the program. That's why it's essentially safe to do so. You're leaking resources, not fucking up anything. Your program will be fine.


  • Considered Harmful

    @gąska Hence the statement "or the exact opposite", and the rest of the stuff I said that wasn't quoted. It does not matter what the guarantee is about, or what it consists of; in a language which makes such a clear distinction between safe and unsafe code, any code that circumvents a guarantee must be considered unsafe.


  • Banned

    @pie_flavor Rust guarantees no memory access violations and no data races. That's it. Leaked memory is fine. So are unclosed handles. These are bad, but don't impact the working of a program. No reason (in Rust developers' mindset) to make leaking those unsafe. You cannot summon nasal demons by skipping destructors, therefore it's safe.

    Now, there are of course situations where running destructor is absolutely critical, like buffered output streams which are flushed in destructors, or mutex locks. Believe it or not, Rust developers are actually working very hard on figuring out how to express this necessity of running the destructor in code, and how to enforce it. But it takes a very long time to come up with the solution, because it's a hard problem and the half-assed solution - running destructors of local variables when they go out of scope, and call it a day - works 99.99% of times.

    In short - "safe" in Rust means no memory corruption ever, and nothing more. Literally. forget() doesn't circumvent any guarantee about this single thing, therefore it's "safe".


  • Considered Harmful

    @gąska Again, it all comes down to whether you view it in terms of common practice or in terms of concept. The destructor is commonly used for avoiding leaks, but theoretically it could hold anything - there is no limit to what code is run in a destructor. Not even as a concept - the destructor is simply designed to let you handle the destruction of the object. And a lot of languages don't guarantee destructors, but whether Rust guarantees no leaked memory or unclosed handles is beside the point - stupid code can be designed stupidly. Rust 100% guarantees, however, that when a value is dropped, the destructor is run, and 100% guarantees that a value is dropped right when it leaves the scope tied to its lifetime without being passed out of the scope. But with mem::forget, this guarantee is utterly meaningless and the destructor goes right back to not being guaranteed.
    The unsafe keyword is not just a key to the lock of unsafe functions and traits, it also represents your promise to the compiler that the unsafety doesn't leak out of the unsafe block, as the documentation states. And part of how you can make that promise is knowing what you can and cannot do in safe code. Your argument would only really make sense if TR:wtf: was the implementation of Rc, but it's not. It's just another example of how anything, including stuff to ensure that promise of safety elsewhere, could be run in a destructor.


  • Banned

    @pie_flavor said in Java quiz:

    Rust 100% guarantees, however, that when a value is dropped, the destructor is run, and 100% guarantees that a value is dropped right when it leaves the scope tied to its lifetime without being passed out of the scope. But with mem::forget, this guarantee is utterly meaningless and the destructor goes right back to not being guaranteed.

    If you just alter the wording a little bit, it all makes sense again. Rust guarantees that when a scope ends, then all local variables still in that scope run their destructors. forget() moves the value out of the current scope, so when the outer scope ends, destructor doesn't run on it. forget() itself uses some magic to make it look to the compiler that the variable isn't alive anymore when it is - but that's implementation detail.

    The point is. Rust's standard library only marks functions as unsafe if they can lead to memory corruption.


  • Discourse touched me in a no-no place

    @pie_flavor said in Java quiz:

    a lot of languages don't guarantee destructors

    It's extremely difficult to guarantee destructors. Indeed, there remain scenarios where it is actually a lot better to not have such a guarantee; guaranteeing destruction (outside the outright impossible scenarios, such as a Terminate With Extreme Prejudice signal) has quite a user-noticeable impact on the process at some points in real applications. It's often better to use a two-layer system, with selected resources that must be deallocated on exit (these are often associated with I/O and some databases) handled through a system of exit handlers, and the general layer (mostly in-memory structures) delegating through in ways that handle the disconnect between the two.

    It's all a bit tricky, but the tricky bits are almost all stuff that can be nailed out of sight of ordinary programmers.


  • Considered Harmful

    @dkf We're talking about stack-allocated structs here, whose destructors need to be run before they are deallocated from the stack so there's still anything there for the destructors to work with. If it can impact performance, fire off a new thread.


  • Discourse touched me in a no-no place

    @pie_flavor said in Java quiz:

    We're talking about stack-allocated structs here, whose destructors need to be run before they are deallocated from the stack so there's still anything there for the destructors to work with. If it can impact performance, fire off a new thread.

    I've seen apps take 5 minutes or more to exit just because every bit of memory allocated ever needed to be paged back in just to carefully pull it apart and deallocate it, just in case there's something in there that might need special handling. When the app is exiting, you don't need that; the few resources that need releasing properly (which normally have a damn good idea that they need to be careful) can handle that more directly, and the rest can be cleaned up by the OS just throwing the memory pages away when it terminates the process.

    I know there are people who are offended by this sort of thing, but it produces much better overall results.


  • Java Dev

    @dkf I used to agree with this strongly. However, especially for long-lived processes, I've come to favour deallocating many in-memory objects. Structures allocated once and then used throughout the lifetime are not a problem, but anything that may be dynamically replaced (such as certain configuration structures) are sensitive to leaking, and that's much easier to find if you clean up on shutdown.

    Also those pages which slow down shutdown because they've been paged out for the last hour and need to be paged back in probably should have been freed an hour ago.


  • Discourse touched me in a no-no place

    @pleegwat said in Java quiz:

    However, especially for long-lived processes, I've come to favour deallocating many in-memory objects.

    That's the sort of thing that GC is for. It really works quite well.


  • Considered Harmful

    @dkf I'm not talking about apps exiting. I'm talking about reclaiming stack space when a function call ends. The values must be gotten rid of at that point, and code needs to be run when they're gotten rid of. It's different in garbage collected languages because the space for all the objects is maintained outside of your code.


  • Banned

    We're talking about 3 different things at once in this topic:

    • Whether objects that aren't used anymore should be deallocated throughout the lifetime of application.
    • Whether objects whose destructors only deallocate memore shall run destructors on shutdown.
    • Whether skipping destructors on purpose is dangerous enough to mark it unsafe in Rust.

    I believe everybody agrees that answers are "yes", "not necessarily" and "technically no", respectively - and we're only arguing because each of us has different idea what we're arguing about.


  • Considered Harmful

    @gąska said in Java quiz:

    We're talking about 3 different things at once in this topic:

    • Whether objects that aren't used anymore should be deallocated throughout the lifetime of application.
    • Whether objects whose destructors only deallocate memore shall run destructors on shutdown.
    • Whether skipping destructors on purpose is dangerous enough to mark it unsafe in Rust.

    I believe everybody agrees that answers are "yes", "not necessarily" and "technically no", respectively - and we're only arguing because each of us has different idea what we're arguing about.

    No, I'm saying 'yes' on that last one.


  • Banned

    @pie_flavor I still don't see how not running destructors leads to memory corruption.


  • Considered Harmful

    @gąska Like this:


  • Banned

    @pie_flavor note that this is because of destructors running, not not running. Even without forget(), you can reproduce the same issue by creating usize::max_value()+1 Rc's to the same value.


  • Considered Harmful

    @gąska Not like that's literally the first thing addressed or anything.

    The reference counts in RcBox are of type usize, and creating a new Rc increments it with regular addition. In optimized code, where overflow checks are removed, it is possible to overflow this count, allowing a use-after-free.

    In the old world where values supposedly could not be forgotten (without running the destructor) without actually leaking memory, it would be impossible for this to wrap around: doing so requires 2^X Rcs where X is the pointer width, ergo sizeof(Rc) * 2^X bytes, which obviously cannot be allocated in a X-bit address space, so allocations would start failing before the count overflowed.


  • Banned

    @pie_flavor there are two things to consider - theory, and real world. In theory, it is possible on some architectures to allocate more memory than usize bytes. In real world, no one uses them anymore, but also no one creates two billion pointers to the same thing.


  • Considered Harmful

    @gąska said in Java quiz:

    @pie_flavor there are two things to consider - theory, and real world. In theory, it is possible on some architectures to allocate more memory than usize bytes. In real world, no one uses them anymore, but also no one creates two billion pointers to the same thing.

    So, basically, I'm right? If that's not what you're saying, I don't know what you're saying. And how would you even do that? You have to write to the memory somehow, and your pointer to the memory is a usize value. By definition, you can't have a pointer larger than the usize, so how would you access the memory?


  • Banned

    @pie_flavor said in Java quiz:

    By definition, you can't have a pointer larger than the usize

    Except...

    Anyway, we're discussing a corner case that will never happen in practice, so it all depends on which theoretical problems that will never happen are more important to us.

    Besides. The overflow itself is undefined behavior, and a program having various issues after triggering undefined behavior is not a valid bug (triggering the undefined behavior is).


  • Considered Harmful

    @gąska AFAIK Rust doesn't support "far" pointers, and even if it did they'd still be usize, or rather usize would be them. So no, a usize which counts the number of allocated memory blocks should never overflow ever. Meanwhile, behavior that exists in the standard library itself should definitely not be considered an edge case.


  • Banned

    @pie_flavor creating over two billion pointers to the same object is always going to be an edge case in my book, no matter what. Especially on 32-bit architecture.

    It's like complaining C++ doesn't tell you beforehand that you cannot put 100MB array on stack.



  • Ok, quick question: What does the forget function actually give you apart from intentional memory leaks? Isn't the whole point of Rust's type system that pointers are always either the only pointer to an object or a read-only pointer to a shared reference-counted object?


  • 🚽 Regular

    @gąska said in Java quiz:

    @pie_flavor I still don't see how not running destructors leads to memory corruption.

    What if the destructor of the element of a graph data structure is meant to update the links of neighbouring elements?


  • Banned

    @ben_lubar said in Java quiz:

    Ok, quick question: What does the forget function actually give you apart from intentional memory leaks?

    It's useful in FFI when you want to give ownership of the object - and thus responsibility for cleanup - to an external non-Rust library.


  • Winner of the 2016 Presidential Election

    @gąska said in Java quiz:

    @pie_flavor said in Java quiz:

    Memory freeing. Handle closing. Reference decrementing. Or the exact opposite.

    What's the consequence of never freeing memory? What's the consequence of never closing a handle? What's the consequence of never decrementing reference counter?

    These are definitely bugs and shouldn't happen. But they are of no consequence to the rest of the program. That's why it's essentially safe to do so. You're leaking resources, not fucking up anything. Your program will be fine.

    Given Firefox's history with memory leaks it should be considered 'unsafe' anyway.


  • Considered Harmful

    @ben_lubar said in Java quiz:

    Ok, quick question: What does the forget function actually give you apart from intentional memory leaks? Isn't the whole point of Rust's type system that pointers are always either the only pointer to an object or a read-only pointer to a shared reference-counted object?

    No, not really.
    A mutable pointer to an object is the only pointer to that object, but there can be infinite immutable pointers to an object. The actual object itself, unless implicitly copyable like a number, only exists in one place at a time - Rc is just a struct; an immutable pointer is not reference counted. The point of Rust's type system is that only one variable can actually hold the object itself; if you, for example, pass the object itself into a function, it's gone (excluding implicitly copyable types like numbers). The point of mem::forget is to take the object out of your ownership without running the destructor, which does in fact have its uses besides leaks.

    For an example of the proper use, take the into_iter function of Vec. It takes ownership of the Vec and returns an IntoIter, rather than taking a pointer and returning a pointer with the same lifetime. In this function, it has to put a raw pointer of its data into the IntoIter struct. However, if the Vec's destructor is run, it's automatically run on all of its contents, too, which would destroy the data. So, to prevent this, it must call mem::forget(self) before returning the created IntoIter, so as to prevent its data from being automatically released (as it would if the Vec expired normally).



  • @pie_flavor said in Java quiz:

    there can be infinite immutable pointers to an object

    There cannot be infinite immutable pointers to an object, because a computer has finitely large memory to hold those pointers. That's why the forget function breaks some sensible assumptions about how compiled code works.



  • @pie_flavor said in Java quiz:

    In this function, it has to put a raw pointer of its data into the IntoIter struct.

    Ok, is that a safe or unsafe operation? It seems to me like doing that would either break the rules about pointer safety or the number of references to an object never being less than or equal to zero.


  • Considered Harmful

    @ben_lubar said in Java quiz:

    @pie_flavor said in Java quiz:

    there can be infinite immutable pointers to an object

    There cannot be infinite immutable pointers to an object, because a computer has finitely large memory to hold those pointers. That's why the forget function breaks some sensible assumptions about how compiled code works.

    You know what I meant, and the pointers have nothing to do with the forget function. You still need full ownership of the object to pass the object into the forget method, and you can't do that if there's still pointers out to it.


  • Considered Harmful

    @ben_lubar said in Java quiz:

    @pie_flavor said in Java quiz:

    In this function, it has to put a raw pointer of its data into the IntoIter struct.

    Ok, is that a safe or unsafe operation? It seems to me like doing that would either break the rules about pointer safety or the number of references to an object never being less than or equal to zero.

    Raw pointers are inherently unsafe. This is done unsafely, in unsafe code. That's how vectors work - you can use the safe functions because the unsafety is carefully controlled, and by intelligent programming doesn't leak out of the unsafe functions.

    Regular pointers aka 'borrows' are the lifetimed things, &T or &mut T. Raw pointers, *mut T or *const T, are not lifetimed and act like C pointers.



  • @pie_flavor said in Java quiz:

    @ben_lubar said in Java quiz:

    @pie_flavor said in Java quiz:

    In this function, it has to put a raw pointer of its data into the IntoIter struct.

    Ok, is that a safe or unsafe operation? It seems to me like doing that would either break the rules about pointer safety or the number of references to an object never being less than or equal to zero.

    Raw pointers are inherently unsafe. This is done unsafely, in unsafe code. That's how vectors work - you use the safe functions because the unsafety is carefully controlled, and by intelligent programming doesn't leak out of the unsafe functions.

    Ok, so why can't the function that moves the data also remove the data from the vector? If it's O(1) to copy the data, it should also be O(1) to set it to zero.


  • Considered Harmful

    @ben_lubar Because then you'd have to reallocate space for it. This way, the IntoIter reuses the same block of memory, with the assurance that the Vec's gone and therefore doesn't point to it anymore. But its creation has to bypass the releasing of that memory that would normally happen if the Vec had just gone out of scope normally.



  • @pie_flavor said in Java quiz:

    @ben_lubar Because then you'd have to reallocate space for it. This way, the IntoIter reuses the same block of memory, with the assurance that the Vec's gone and therefore doesn't point to it anymore. But its creation has to bypass the releasing of that memory that would normally happen if the Vec had just gone out of scope normally.

    Can you name a situation where the forget function would be useful in non-unsafe code?


  • Considered Harmful

    @ben_lubar said in Java quiz:

    @pie_flavor said in Java quiz:

    @ben_lubar Because then you'd have to reallocate space for it. This way, the IntoIter reuses the same block of memory, with the assurance that the Vec's gone and therefore doesn't point to it anymore. But its creation has to bypass the releasing of that memory that would normally happen if the Vec had just gone out of scope normally.

    Can you name a situation where the forget function would be useful in non-unsafe code?

    No. Not at all. And that whole 'unsafety doesn't leak out of unsafe blocks' thing can only be ensured if other promises, like destructors always being run when a value is released, are kept too. Thus, mem::forget should definitely go back to being an unsafe function.



  • @pie_flavor said in Java quiz:

    Destructors are guaranteed to be run when the value goes out of scope.

    No, they are not. It turned out not to be possible.

    @pie_flavor said in Java quiz:

    By preventing them from being run, you implicitly introduce unsafety, because safe code could be relying on those guarantees. Destructors are part of Rust's memory safety.

    They were intended to be, but at one point it was realised it is not possible, because there are safe ways to leak memory that can't be declared unsafe while keeping the whole construction useful. See

    and

    Safe code can't, therefore, rely on them running. This caused at least one case that did to be deprecated and removed:

    @pie_flavor said in Java quiz:

    and 100% guarantees that a value is dropped right when it leaves the scope tied to its lifetime without being passed out of the scope.

    Only for values actually on the stack. After all, things on the heap don't have any scope tied to their lifetime, only upper and lower bounds of it.

    @gąska said in Java quiz:

    We're talking about 3 different things at once in this topic:

    • Whether objects that aren't used anymore should be deallocated throughout the lifetime of application.
    • Whether objects whose destructors only deallocate memore shall run destructors on shutdown.
    • Whether skipping destructors on purpose is dangerous enough to mark it unsafe in Rust.

    I believe everybody agrees that answers are "yes", "not necessarily" and "technically no", respectively - and we're only arguing because each of us has different idea what we're arguing about.

    I am pretty sure the first answer is “cannot be ensured”, which implies the same for the second and makes yes inconsistent for the third.


  • Considered Harmful

    @bulb said in Java quiz:

    @pie_flavor said in Java quiz:

    Destructors are guaranteed to be run when the value goes out of scope.

    No, they are not. It turned out not to be possible.

    What? It's required. This is with stack-allocated values; if you don't run them immediately, you don't run them at all because any data the destructor would have to work with is already gone.

    @pie_flavor said in Java quiz:

    By preventing them from being run, you implicitly introduce unsafety, because safe code could be relying on those guarantees. Destructors are part of Rust's memory safety.

    They were intended to be, but at one point it was realised it is not possible, because there are safe ways to leak memory that can't be declared unsafe while keeping the whole construction useful. See

    . https://github.com/rust-lang/rust/issues/24456

    and

    . https://github.com/rust-lang/rfcs/pull/1066

    Safe code can't, therefore, rely on them running. This caused at least one case that did to be deprecated and removed:

    You still have to be doing it on purpose, and there's no way you're not :doing_it_wrong: already if you're putting an Rc into itself.

    @pie_flavor said in Java quiz:

    and 100% guarantees that a value is dropped right when it leaves the scope tied to its lifetime without being passed out of the scope.

    Only for values actually on the stack. After all, things on the heap don't have any scope tied to their lifetime, only upper and lower bounds of it.

    Yes, but heap allocated things are only accessed from either unsafe code or from stack allocated structs containing unsafe code, and those structs are supposed to enforce proper lifetime rules and manage deallocating the heap values in the destructor.

    Even if you trap an object in a thread, it's still there, the memory is still being used. The reason Rc overflows is because the struct is deconstructed without the destructor being run, causing the counter to increment without the memory being used.



  • @pie_flavor said in Java quiz:

    What? It's required.

    Where?

    @pie_flavor said in Java quiz:

    Yes, but heap allocated things are only accessed from either unsafe code or from stack allocated structs containing unsafe code, and those structs are supposed to enforce proper lifetime rules and manage deallocating the heap values in the destructor.

    Except that it turned out they can't do it. Because Rc cycles, but potentially other things as well. So the requirement was dropped. Please, do read the RFC#1066 discussion.


  • Considered Harmful

    @bulb I could respond to your post with a coherent argument simply by reposting the segments of my last post that you didn't quote.


  • Discourse touched me in a no-no place

    @pie_flavor said in Java quiz:

    Yes, but heap allocated things are only accessed from either unsafe code or from stack allocated structs containing unsafe code, and those structs are supposed to enforce proper lifetime rules and manage deallocating the heap values in the destructor.

    Speaking as someone who is very deeply involved in language design and implementation, I find this whole discussion highly amusing. There are ways to produce entirely safe code (many languages actually achieve it) yet they inevitably come with performance and/or capability trade-offs. I'm guessing yet another bunch of developers are having to learn all this the hard way…

    🍿


  • ♿ (Parody)

    @dkf said in Java quiz:

    I'm guessing yet another bunch of developers are having to learn all this the hard way…

    I was just looking at the github oneboxes and feeling an urge to check the lawn for interlopers.


Log in to reply