And an in-progress proposal to make these various "bubble" functions have consistent semantics: https://github.com/golang/go/issues/76477
(As an aside, the linked blog series is great, but if you're interested in new Go features, I've found it really helpful to also subscribe to https://go.dev/issue/33502 to get the weekly proposal updates straight from the source. Reading the debates on some of these proposals provides a huge level of insight into the evolution of Go.)
I mean, if you're worried about ensuring data gets zeroed out, you probably also don't want to leak it via side channels, either.
I don't understand. Why do you need it in a garbage-collected language?
My impression was that you are not able to access any register in these language. It is handled by the compiler instead.
In practice it provides a straightforward path to complying with government crypto certification requirements like FIPS 140 that were written with languages in mind where this is an issue.
If you are concerned about secrets being zeroed out in almost any language, you need some sort of support for it. Non-GC'd languages are prone to optimize away zeroing out of memory before deallocation, because under normal circumstances a write to a value just before deallocation that is never effectfully read can be dropped without visible consequence to the rest of the program. And as compilers get smarter it can be harder to fool them with code, like, simply reading afterwards with no further visible effect might have been enough to fool 20th century compilers but nowadays I wouldn't count on my compiler being that stupid.
There are also plenty of languages where you may want to use values that are immutable within the context of the language, so there isn't even a way to express "let's zero out this RAM".
Basically, if you don't build this in as a language feature, you have a whole lot of pressures constantly pushing you in the other direction, because why wouldn't you want to avoid the cost of zeroing memory if you can? All kinds of reasons to try to avoid that.
And any language which can call C code that is resident in the same virtual memory space can have its own restrictions bypassed by said C code. This even applies to more restrictive runtimes like the JVM or Python.
Since "safety" is an encompassing term, it's easy to find more rigorous definitions of the term that Go would flunk; for instance, it relies on explicit synchronization for shared memory variables. People aren't wrong for calling out that other languages have stronger correctness stories, especially regarding concurrency. But they are wrong for extending those claims to "Go isn't memory safe".
It is true that go is only memory unsafe in a specific scenario, but such things aren’t possible in true memory safe languages like c# or Java. That it only occurs in multithreaded scenarios matters little especially since concurrency is a huge selling point of the language and baked in.
Java can have data races, but those data races cannot be directly exploited into memory safety issues like you can with Go. I’m tired of Go fans treating memory safety as some continuum just because there are many specific classes of how memory safety can be violated and Go protecting against most is somehow the same as protecting against all (which is what being a memory safe language means whether you like it or not).
I’m not aware of any other major language claiming memory safety that is susceptible to segfaults.
Blatantly false. From Ralf’s post:
> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x2a pc=0x468863]
The panic address is 42, a value being mutated, not a nil pointer. You could easily imagine this address pointing to a legal but unintended memory address resulting in a read or write of unintended memory.
> When that happens, we will run the Ptr version of get, which will dereference the Int’s val field as a pointer – and hence the program accesses address 42, and crashes.
If you don’t see an exploit gadget there based on a violation of memory safety I don’t know how to have a productive conversation.
The 42 is an explicit value in the example code. From what I understand the code repeatedly changes the value assigned to an interface variable from an object containing a pointer to an object containing an integer. Since interface variables store the type of the assigned value, but do not update both type and value atomically a different thread can interpret whatever integer you put into it as a valid pointer. Putting a large enough value into the integer should avoid the protected memory page around 0 and allow for some old fashioned memory corruption.
Of course, Go allows more than that, with data races it's possible to reach use after free or other kinds of memory unsafety, but just segfaults don't mark a language memory unsafe.
This stems from the fact that Go uses fat pointers for interfaces, so they can't be atomically assigned. Built-in maps and slices are also not corruption-safe.
In contrast, Java does provide this guarantee. You can mutate structures across threads, and you will NOT get data corruption. It can result in null pointer exceptions, infinite loops, but not in corruption.
Serious systems built in memory-unsafe languages yield continual streams of exploitable vulnerabilities; that remains true even when those systems are maintained by the best-resourced security teams in the world. Functionally no Go projects have this property. The empirics are hard to get around.
> Serious systems built in memory-unsafe languages yield continual streams of exploitable vulnerabilities
I'm not saying that Go is as unsafe as C. But it definitely is NOT completely safe. I've seen memory corruptions from improper data sync in my own code.
It's not like this is a small track record. There is a lot of Go code, a fair bit of it important, and memory corruption exploits in non-FFI Go code is... not a thing. Like at all.
I triggered SSM agent crashes while developing my https://github.com/Cyberax/gimlet by doing concurrent requests.
I'm certain that they could have been used to do code execution, but it just makes no real sense given the context.
Also, by your definition, e.g. Rust is not memory safe. And "It is true that Rust is only memory unsafe in a specific scenario, but [...]". I hope you agree.
I used to not really see the need for this level of detail on things... then you see useful (IE in the wild exploits) for even complex factors like CPU branch prediction (for a while now), and the need starts to become much more clear.
It may sound naive, but packages which include data like said session related or any other that should not persist (until the next Global GC) - why don't you just scramble their value before ending your current action?
And dont get me wrong - yes that implies extra computation yada yada - but until a a solution is practical and builtin - i'd just recommend to scramble such variables with new data so no matter how long it will persist, a dump would just return your "random" scramble and nothing actually relevant.
Imagine, 3 level nesting calls where each calls another 3 methods, we are talking about 28 functions each with couple of variables, of course you can still clean them up, but imagine how clean code will look if you don't have to.
Just like garbage collection, you can free up memory yourself, but someone forgot something and we have either memory leak or security issues.
secretStash := NewSecretStash()
pString := secretStash.NewString()
....
....
secretStash.Thrash()
yes, you now have to deal in pointers, but that's not too ugly, and everything is stored in secretStash so can iterate over all the types it supports and thrash them to make them unusable, even without the gc running.If you had to prompt a user for a password, you’d read it in, use it, then thrash the value.
read -p “Password: “ PASSWD
# do something with $PASSWD
PASSWD=“XXXXXXXXXXXXXXXXXX”
It’s not pretty, but a similar concept. (I also don't know how helpful it actually is, but that's another question...) key := make([]byte, 32)
defer scramble(&key)
// do all the secret stuff
Unless I don't understand the problem correctly.As another response pointed out, its also possible that said secret data is still in the register, which no matter what we do to the curr value could exist.
Thanks for pointing it out!
This is essentially already the case whenever you use encryption, because there are tell-tale signs you can detect (e.g., RSA S-Box). But this will make it even easier and also tip you off to critical sections that are sensitive yet don't involve encryption (e.g., secure strings).
1) You are almost certainly going to be passing that key material to some other functions, and those functions may allocate and copy your data around; while core crypto operations could probably be identified and given special protection in their own right, this still creates a hole for "helper" functions that sit in the middle
2) The compiler can always keep some data in registers, and most Go code can be interrupted at any time, with the registers of the running goroutine copied to somewhere in memory temporarily; this is beyond your control and cannot be patched up after the fact by you even once control returns to your goroutine
So, even with your approach, (2) is a pretty serious and fundamental issue, and (1) is a pretty serious but mostly ergonomic issue. The two APIs also illustrate a basic difference in posture: secret.Do wipes everything except what you intentionally preserve beyond its scope, while scramble wipes only what you think it is important to wipe.
While in my case i had a program in which i created an instance of such a secret , "used it" and than scrambled the variable it never left so it worked.
Tho i didn't think of (2) which is especially problematic.
Prolly still would scramble on places its viable to implement, trying to reduce the surface even if i cannot fully remove it.
func do_another_important_thing(key []byte) []byte {
newKey := append(key, 0x0, 0x1) // this might make a copy!
return newKey
}
key := make([]byte, 32)
defer scramble(&key)
do_another_important_thing(key)
// do all the secret stuff
Because of the copy that append might do, you now have 2 copies of the key in data, but you only scramble one. There are many functions that might make a copy of the data given that you don't manually manage memory in Go. And if you are writing an open source library that might have dozens of authors, it's better for the language to provide a guarantee, rather than hope that a developer that probably isn't born yet will remember not to call an "insecure" function.First one being, it was -very- tricky to use properly for most cases, APIs to the outside world typically would give a byte[] or string or char[] and then you fall into the problem space you mention. That is, if you used a byte[] or char[] array, and GC does a relocation of the data, it may still be present in the old spot.
(Worth noting, the type itself doesn't do that, whatever you pass in gets copied to a non-gc buffer.)
The second issue is that there's not a unified unix memory protection system like in windows; The windows implementation is able to use Crypt32 such that only the current process can read the memory it used for the buffeer.
In a nutshell, if you have a function like
fn secret_func() -> LargeType {
/* do some secret calculations */
LargeType::init_with_safe_Data()
}
...then even if you sanitize heap allocations and whatnot, there is still a possibility that those "secret calculations" will use the space allocated for the return value as a temporary location, and then you'll have secret data leaked in that type's padding.More realistically, I'm assuming you're aware that optimizing compilers often simplify `memset(p, 0, size); free(p);` to `free(p);`. A compiler frontend can use things like `memset_s` to force rewrites, but this will only affect the locals created by the frontend. It's entirely possible that the LLVM backend notices that the IR wants to erase some variable, and then decides to just copy the data to another location on the stack and work with that, say to utilize instruction-level parallelism.
I'm partially talking out of my ass here, I don't actually know if LLVM utilizes this. I'm sure it does for small types, but maybe not with aggregates? Either way, this is something that can break very easily as optimizing compilers improve, similarly to how cryptography library authors have found that their "constant-time" hacks are now optimized to conditional jumps.
Of course, this ignores the overall issue that Rust does not have a runtime. If you enter the secret mode, the stack frames of all nested invoked functions needs to be erased, but no information about the size of that stack is accessible. For all you know, memcpy might save some dangerous data to stack (say, spill the vector registers or something), but since it's implemented in libc and linked dynamically, there is simply no information available on the size of the stack allocation.
This is a long yap, but personally, I've found that trying to harden general-purpose languages simply doesn't work well enough. Hopefully everyone realizes by now that a borrow checker is a must if you want to prevent memory unsoundness issues in a low-level language; similarly, I believe an entirely novel concept is needed for cryptographical applications. I don't buy that you can just bolt it onto an existing language.
You could definitely zero registers that way, and a allocator that zeros on drop should be easy. The only tricky thing would be zeroing the stack - how do you know how deep to go? I wonder what Go's solution to that is...
And stack's the main problem, yeah. It's kind of the main reason why zeroing registers is not enough. That and inter-procedural optimizations.
In general though getting to a fairly predictable place is possible and the typical case of key material shouldn’t have highly arbitrary stacks, if you do you’re losing (see io comment above).
https://docs.rs/zeroize/1.8.1/zeroize/ has been effective for some users, it’s helped black box tests searching for key material no longer find it. There are also some docs there on how to avoid common pitfalls and links to ongoing language level discussions on the remaining and more complex register level issues.
use std::ptr;
struct SecretData {
data: Vec<u8>,
}
impl Drop for SecretData {
fn drop(&mut self) {
// Zero out the data
unsafe {
ptr::write_bytes(self.data.as_mut_ptr(), 0, self.data.len());
}
}
}Big thumbs down from me.
I think that means this proposal adds a very specific form of finalisers to go.
How is that implemented efficiently? I can think of doing something akin to NSAutoReleasePool (https://developer.apple.com/documentation/foundation/nsautor...), with all allocations inside a `secret.Do` block going into a separate section of the heap (like a new generation), and, on exit of the block, the runtime doing a GC cycle, collecting and clearing every now inaccessible object in that section of the heap.
It can’t do that, though, because the article also says:
“Heap allocations are only erased if the program drops all references to them, and then the garbage collector notices that those references are gone. The program controls the first part, but the second part depends on when the runtime decides to act”
and I think what I am thinking of will guarantee that the garbage collector will eagerly erase any heap allocations that can be freed.
Also, the requirement “ the program drops all references to them” means this is not a 100% free lunch. You can’t simply wrap your code in a `secret.Do` and expect your code to be free of leaking secrets.
AddCleanup might be too heavy, it is cheaper to just set a bit in the header/info zone of memory blocks.
This is just my own speculation, I don't know the internals of Go beyond a few articles I've read over the years. Somewhat more familiar with C#, JS and Rust, and even then I'll sometimes confuse certain features between them.
Linux has memfd_secret ( https://man7.org/linux/man-pages/man2/memfd_secret.2.html ), that allow you to create a secure memory region that can't be directly mapped into regular RAM.
https://github.com/rust-lang/rust/issues/17046
https://github.com/conradkleinespel/rpassword/issues/100#iss...
func Encrypt(message []byte) ([]byte, error) {
var ciphertext []byte
var encErr error
secret.Do(func() {
// ...
})
return ciphertext, encErr
}
As that suggests that somehow for PFS it is critical that the ephemeral key (not the long-term one) is zeroed out, while the plaintext message - i.e. the thing that in the example we allegedly want secrecy for - is totally fine to be outside of the whole `secret` machinery, and remain in memory potentially "forever".I get that the example is simplified (because what it should actually do is protect the long-term key, not the ephemeral one)... so, yeah, it's just a bad example.
Seems like this should raise a compiler error or panic on runtime.
1) allocations via memguard bypass gc entirely
2) they are encrypted at all times when not unsealed
3) pages are mprotected to prevent leakage via swap
4) and so on...
Not as ergonomic as OP's proposal, of course.
// Only the ciphertext leaves this closure.
This ideally should be describable by the type system.That doesn't make sense to me. How can the "offset in an array itself" be "secret" if it's "always" 100? 100 isn't secret.
That’s my hunch at least, but I’m not a security expert.
The example could probably have been better phrased.
The only thing that makes sense to me is a scenario with a lot of addresses. E.g. if there's an array of 256 integers, and those integers themselves aren't secret. Then there's a key composed of 32 of those integers, and the code picks which integers to use for the key by using pointers to them. If an attacker is able to know those 32 pointers, then the attacker can easily know what 32 integers the key is made of, and can thus know the key. Since the secret package doesn't erase pointers, it doesn't protect against this attack. The solution is to use 32 array indexes to choose the 32 integers, not 32 pointers to choose the 32 integers. The array indexes will be erased by the secret package.
fsmv•1mo ago
Go has the best support for cryptography of any language
samdoesnothing•1mo ago
pants2•1mo ago
1. Well-supported standard libraries generally written by Google
2. Major projects like Vault and K8s that use those implementations and publish new stuff
3. Primary client language for many blockchains, bringing cryptography contributions from the likes of Ethereum Foundation, Tendermint, Algorand, ZK rollups, etc
adastra22•1mo ago
Because there is tremendous support for cryptography in, say, the C/C++ ecosystem, which has traditionally been the default language of cryptographers.
fsmv•1mo ago
I'm a big fan of the go standard library + /x/ packages.
Mawr•1mo ago
int_19h•1mo ago
drowsspa•1mo ago
fastest963•1mo ago
awithrow•1mo ago
kbolino•1mo ago
kbolino•1mo ago
cafxx•1mo ago
cafxx•1mo ago
https://go-review.googlesource.com/c/go/+/729920
oncallthrow•1mo ago
Edit: also, the supported platforms are ARM and x86. If your code isn’t running on one of those platforms, you probably know what you’re doing.
ctoth•1mo ago
Windows and MacOS?
Go is supposed to be cross-platform. I guess it's cross-platform until it isn't, and will silently change the semantics of security-critical operations (yes, every library builder will definitely remember to check if it's enabled.)
YesThatTom2•1mo ago
Many advanced Go features start in certain platforms and then expand to others once the kinks are worked out. It’s a common pattern and has many benefits. Why port before its stable?
I look forward to your PR.
baq•1mo ago
hypeatei•1mo ago
Which is exactly why it should fail explicitly on unsupported platforms unless the developer says otherwise. I'm not sure how Go developers make things obvious, but presumably you have an ugly method or configuration option like:
...for when a developer understands the risk and doesn't want to panic.kbolino•1mo ago
satellite2•1mo ago
alanfranz•1mo ago
But, this is probably a net improvement over the current situation, and this is still experimental, so, changes can happen before it gets to GA.
treyd•1mo ago
This isn't true at all.
Writing cryptography code in Go is incredibly annoying and cumbersome due lack of operator overloading, forcinforcing you to do method calls like `foo.Add(bar.Mul(baz).Mod(modulus)).Mod(modulus)`. These also often end up having to be bignums instead of using generic fixed-size field arithmetic types. Rust has incredibly extensive cryptographic libraries, the low-level taking advantage of this operator overloading so the code ends up being able to following the notation in literature more closely. The elliptic_curve crate in particular is very nice to work with.
pjmlp•1mo ago
tracker1•1mo ago
pjmlp•1mo ago
Most of our agency projects that have .NET in them, are brown field ongoing projects that mostly focus on .NET Framework, thus we end up only using modern .NET when given the opportunity to deliver new microservices, and the customer happens to be a .NET shop.
The last time this happened, .NET 8 had just been released, and most devs I work with tend to be journeyman they aren't chasing programming language blogs to find out what changes in each release, or online communities, they do the agency work and go home for friends, family and non programming related hobbies.
tracker1•1mo ago
This is likely done for platform performance and having a manual version likely hinders the GC in a way that's deemed too impactful. Beyond this, if SysV or others contribute specific patches that aren't brute forced (such as RiscV extensions), I would assume that the go maintainers would accept it..