Skip to main content

Rust fixme 2 picoCTF 2025 Solution

Debug a Rust program with ownership and error-handling issues that prevent it from compiling and revealing the flag.

Published: April 2, 2025Updated: August 25, 2026

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.

bash
wget https://challenge-files.picoctf.net/c_verbal_sleep/babfbee79718a6363826ba86300173ffde6d81577e9dd07d4130c53a7eecf6c3/fixme2.tar.gz
bash
tar -xvf fixme2.tar.gz && cd fixme2
bash
less src/main.rs
bash
cargo run

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Borrow the string mutably
    Observation
    The call to decrypt fails on a type mismatch: the variable is passed as a shared reference to a function that modifies it. Both the parameter type and the call site need the mutable form.
    Change the function signature to accept &mut String and call it with &mut party_foul so 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 quiets the type checker at the call site, but the function still takes an owned String rather than a mutable reference, so the signature is still wrong. Worse, every modification lands on the clone and is discarded when the call returns. The original is untouched and no flag appears.

    Tried: Changing only the call site to &mut party_foul without updating the function signature.

    Rust matches the reference type at the call site against the signature. Leave the signature on the shared reference and passing a mutable one is a mismatch. Change both 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 &mut reference. 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.

  2. Step 2Fix the early-return syntax
    Observation
    The error-check guard uses ret, which Rust reads as an unknown identifier rather than control flow. Write return so the function exits early when construction fails.
    The original code uses ret; instead of return; to exit early when the XORCryptor constructor fails. Change ret; to return;. The surrounding is_err() guard and res.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.

    Delete the error guard and the code falls through to unwrap even when the constructor failed. Unwrapping an error panics, so the program crashes instead of printing the flag. The guard is right; only its keyword is wrong.

    Tried: Replacing ret; with break; or continue; since they also interrupt normal control flow.

    break and continue only work inside loops and are a compile error anywhere else. return is the one keyword that exits a function early, and here it takes no value, because the function returns unit.

    Learn more

    Rust's Result<T, E> type is an enum with two variants: Ok(T) for success and Err(E) for failure. It forces callers to explicitly handle both outcomes, unlike exceptions in Python or Java, which can be silently uncaught. The if 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 Result include: .unwrap() (panics on Err, 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 a Result.

    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.

  3. Step 3Declare the string mutable with `let mut`
    Observation
    The compiler says the variable cannot be borrowed as mutable because it was not declared that way, pointing straight at the let binding. Add mut and the borrow from step 1 is legal.
    The compiler error reads cannot borrow 'party_foul' as mutable, as it is not declared as mutable. Change the binding to let mut party_foul = ... so the borrow on the previous step is allowed, then rerun cargo run to decrypt and print the flag.
    What didn't work first

    Tried: Fixing only the &mut in the function signature and call site without touching the let binding.

    Rust wants mutability declared at the binding as well as at each borrow. A mutable reference to an immutably bound variable is rejected outright. The let has to become let mut even when every borrow is already correct.

    Tried: Wrapping the variable in a RefCell or Cell to achieve interior mutability.

    RefCell and friends exist for mutating through a shared reference, usually inside data structures or around awkward ownership constraints. For a local variable in a single-threaded function, mut on the binding is the idiomatic answer, and RefCell only adds runtime cost and complexity.

    Learn more

    In Rust, variables are immutable by default. You must explicitly opt into mutability with the mut keyword: 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 borrow x as 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 of Arc<Mutex<T>>), simplifying concurrency logic significantly.

    The combination of skills this challenge reinforces (understanding mut, &mut references, and Result handling) 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.

Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.

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.

Key takeaway

Rust's ownership system puts mutation intent into the type system: a function holding a shared reference cannot mutate the data whatever the author intended, and the compiler enforces that without runtime checks. Use-after-free, data races, and iterator invalidation become compile errors rather than latent bugs found in production or by a fuzzer. Security-critical projects, the Linux kernel included, are adopting Rust because those bugs disappear structurally instead of being caught by review.

Related reading

Useful tools for General Skills

Where to go next