Description
Fix three errors in a simple XOR decrypter: change a ret typo to return, pass a mutable string to decrypt, and declare the output variable with let mut so the flag prints.
Extract the archive and cd fixme2.
Open src/main.rs and look for the decrypt function and its call site; the three fixes all live around those few lines.
Use cargo run to see the borrow checker complaints and compiler errors. Fix one error at a time and re-run after each change.
tar -xvf fixme2.tar.gz && cd fixme2less src/main.rscargo runSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Borrow the string mutablyObservationI noticed the compiler rejected the call todecryptwith a type mismatch becauseparty_foulwas passed as&Stringto a function that modifies its argument, which suggested the parameter type and call site both needed to be changed to&mut Stringand&mut party_foul.Change the function signature to accept&mut Stringand call it with&mut party_foulso the helper can append to the string.What didn't work first
Tried: Adding
.clone()at the call site to give the function its own copy of the string.Cloning satisfies the type checker at the call site, but the function still receives an owned
Stringrather than a mutable reference, so the signature must also change. More importantly, any appends or modifications the function makes apply to the clone and are discarded after the call returns - the original variable in main is untouched and the flag never appears.Tried: Changing only the call site to
&mut party_foulwithout updating the function signature.Rust matches the reference type at the call site against the type declared in the function signature. If the signature still says
&String, passing&mut party_foulis a type mismatch and the compiler rejects it. Both the signature and the call site must be updated together.Learn more
Rust's ownership and borrowing system is the language's most distinctive feature and its primary mechanism for achieving memory safety without a garbage collector. Every value has exactly one owner. You can create references ("borrows") to a value, either many immutable references (
&T) or exactly one mutable reference (&mut T) at a time, but never both simultaneously.When a function needs to modify a value owned by its caller, it must receive a
&mutreference. Passing&T(immutable reference) to a function that tries to mutate the data is a compile-time error. This forces the programmer to be explicit about mutation intentions at every function boundary, a sharp contrast with C where any pointer can be silently used to modify data.This system prevents entire classes of bugs at compile time: use-after-free (the owner drops the value while a reference exists), data races (two threads holding mutable references simultaneously), and iterator invalidation (modifying a collection while iterating over it). These are among the most common and dangerous bugs in C and C++ codebases, and Rust eliminates them without runtime overhead.
Step 2
Fix the early-return syntaxObservationI noticed the source file usedret;inside the error-check guard, which Rust treats as an unknown identifier rather than a control-flow keyword, and this suggested renaming it to the correctreturn;keyword so the function can exit early when XORCryptor construction fails.The original code usesret;instead ofreturn;to exit early when the XORCryptor constructor fails. Changeret;toreturn;. The surroundingis_err()guard andres.unwrap()are already correct and compile fine once the borrow fixes in steps 1 and 3 are applied.What didn't work first
Tried: Deleting the early-exit guard entirely instead of fixing the keyword.
Removing the
if res.is_err() { ret; }block means the code falls through tores.unwrap()even when the XORCryptor constructor failed. Unwrapping anErrvalue causes a panic at runtime, so the program crashes instead of printing the flag. The guard exists for good reason - it just uses a non-existent keyword that must be spelled out in full.Tried: Replacing
ret;withbreak;orcontinue;since they also interrupt normal control flow.breakandcontinueare only valid inside loops; using them outside a loop is a compile-time error.returnis the only keyword that exits the enclosing function early. In this context,return;with no value is correct because the function returns the unit type().Learn more
Rust's
Result<T, E>type is an enum with two variants:Ok(T)for success andErr(E)for failure. It forces callers to explicitly handle both outcomes, unlike exceptions in Python or Java, which can be silently uncaught. Theif let Ok(value) = result { ... }pattern is a concise way to handle the success case and implicitly ignore the error case.Other idiomatic ways to unwrap a
Resultinclude:.unwrap()(panics onErr, useful in tests and prototypes, dangerous in production),.expect("message")(panics with a custom message),.unwrap_or(default)(returns a default value on error), and the?operator (propagates errors to the caller). Each has its place depending on how fatal the error is and whether the function itself returns aResult.XOR-based encryption is simple but instructive: the same operation (XOR with the key) both encrypts and decrypts, making it a symmetric cipher with trivial implementation. See stream ciphers in CTFs for how key reuse and short keys break this construction in practice. XOR is also the core operation inside ciphers like ChaCha20 and block-cipher modes like CTR and OFB.
CTF relevance: this exact pattern is why naive XOR ciphers fall in seconds. The same key masks every block, so any known plaintext anywhere in the message immediately leaks the keystream and every other block at that offset decrypts for free. Real stream ciphers fix this by combining the key with a nonce + counter so the keystream never repeats; that nonce discipline is what separates a secure construction from a CTF-grade XOR.
Step 3
Declare the string mutable with `let mut`ObservationI noticed the compiler error explicitly statedcannot borrow 'party_foul' as mutable, as it is not declared as mutable, which pointed directly at thelet party_foulbinding and indicated that addingmutto the declaration was the final fix needed to allow the&mutborrow set up in step 1.The compiler error readscannot borrow 'party_foul' as mutable, as it is not declared as mutable. Change the binding tolet mut party_foul = ...so the borrow on the previous step is allowed, then reruncargo runto decrypt and print the flag.What didn't work first
Tried: Fixing only the
&mutin the function signature and call site without touching theletbinding.Rust requires mutability to be declared at the variable's binding site as well as at each borrow. A
&mutreference to an immutably-bound variable is rejected by the compiler with the message 'cannot borrow as mutable, as it is not declared as mutable'. Theletdeclaration must becomelet muteven if&mutis already correct everywhere else.Tried: Wrapping the variable in a
RefCellorCellto achieve interior mutability.Interior mutability types like
RefCellexist for cases where you need to mutate through a shared&Treference - typically in data structures or when working around ownership constraints in complex code. For a simple local variable in a single-threaded function, addingmutto theletbinding is the correct and idiomatic solution.RefCelladds runtime overhead and complexity that is completely unnecessary here.Learn more
In Rust, variables are immutable by default. You must explicitly opt into mutability with the
mutkeyword:let mut x = 5;. This is the opposite of most languages and is a deliberate design choice; immutability makes code easier to reason about and prevents accidental state mutations. The compiler error "cannot borrowxas mutable, as it is not declared as mutable" is one of the most common Rust beginner errors and one of the most educational.Immutability by default also has performance implications: the compiler can make stronger aliasing assumptions for immutable data, potentially enabling more aggressive optimizations. In concurrent code, immutable data can be freely shared across threads without locks (
Arc<T>instead ofArc<Mutex<T>>), simplifying concurrency logic significantly.The combination of skills this challenge reinforces (understanding
mut,&mutreferences, andResulthandling) covers a significant portion of Rust's learning curve. Developers who master these concepts find that the compiler becomes a powerful assistant that catches design mistakes before they become runtime bugs, making Rust code unusually reliable despite its initial steepness of learning.
Flag
Reveal flag
picoCTF{4r3_y0u_h4v1n5_fun_y31?}
This task reinforces Rust's borrowing rules, and Fixme 3 builds on the same pattern with a slightly larger project.