Skip to main content

Rust fixme 3 picoCTF 2025 Solution

Fix a Rust program that fails to compile due to a missing safety annotation required for low-level code.

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

Description

The final Rust warm-up simply asks you to call an unsafe helper correctly by wrapping the FFI-style function in an unsafe block so the compiler agrees to execute it.

Extract the archive, enter the project directory, and run cargo run first to see the exact compiler error: call to unsafe function is unsafe and requires unsafe block.

Open src/main.rs and look at lines 22 and 34. The challenge already wrote the unsafe { ... } braces; they're just commented out so you can see them.

bash
tar -xvf fixme3.tar.gz && cd fixme3
bash
cargo run
bash
less src/main.rs

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Wrap the unsafe call
    Observation
    The compiler says the call needs an unsafe block, and lines 22 and 34 already hold exactly that structure behind comment markers. Uncomment them.
    Uncomment the unsafe { ... } block (lines 22 and 34) so the compiler allows the call into the unsafe helper. No other code changes are needed.
    bash
    # Edit src/main.rs and remove the // before the unsafe block, e.g.:
    # unsafe {
    #     give_flag();
    # }
    What didn't work first

    Tried: Write a brand-new unsafe { give_flag(); } block instead of uncommenting the existing one

    That compiles and runs, but adding a call of your own alongside the commented-out ones risks invoking twice and leaves the scaffold half-edited. Just remove the two comment markers already at lines 22 and 34; the file was written that way on purpose.

    Tried: Mark the give_flag function declaration itself as unsafe fn give_flag() to try to avoid needing an unsafe block at the call site

    Marking the function unsafe does not remove the need for a block; it adds one. Callers of an unsafe fn must still wrap every call. The error persists because the obligation belongs to the caller, not the signature.

    Learn more

    Rust's unsafe keyword is a signal to both the compiler and human readers that the enclosed code bypasses Rust's automatic safety guarantees. Inside an unsafe { } block, five additional operations become legal:

    1. Dereferencing a raw pointer (*const T / *mut T).
    2. Calling an unsafe function (including any C library through FFI).
    3. Reading from or writing to a mutable static.
    4. Implementing an unsafe trait (e.g. Send/Sync on a type that holds raw pointers).
    5. Accessing a field of a union (the compiler can't verify the active variant).

    The unsafe block does not disable the borrow checker or turn off other Rust features; it merely signals that the programmer has manually verified that the enclosed operations are sound. This creates a clear, searchable boundary: every unsafe in a Rust codebase is a location where memory safety was asserted by the programmer rather than the compiler, making audits focused and efficient. In contrast, C has no such boundary; any code can perform unsafe operations without annotation.

    Unsafe Rust is essential for system programming tasks. The canonical example is FFI into a C library: unsafe { libc::strlen(ptr) } calls the C standard library's strlen, which Rust marks unsafe because the caller must guarantee ptr points to a valid, NUL-terminated buffer. Other uses include OS kernel components, hardware memory-mapped registers, and zero-cost abstractions like custom allocators or lock-free data structures.

    The principle is to minimize unsafe surface area and encapsulate it behind safe abstractions. Concretely: std::fs::File::open() is a safe wrapper around an unsafe open(2) syscall and raw file-descriptor manipulation. The stdlib author wrote and audited the unsafe internals, then exposed an API that callers can use without ever typing unsafe themselves. Most application code stays entirely safe by leaning on those wrappers.

  2. Step 2Rebuild and run
    Observation
    Nothing else in the program needs changing, so one build after that edit compiles cleanly and prints the flag.
    Re-run cargo run and the binary now prints the picoCTF flag immediately.
    Learn more

    Cargo is Rust's integrated build system and package manager. cargo run compiles the project (in debug mode by default) and immediately executes the resulting binary. cargo build --release produces an optimized binary without running it. The distinction matters for performance-sensitive code: debug builds include overflow checks and backtraces but run slowly; release builds apply full LLVM optimization passes.

    Cargo's dependency management (via Cargo.toml and Cargo.lock) is one of Rust's major ergonomic advantages over C/C++. Adding a dependency is as simple as listing it under [dependencies], and Cargo fetches, compiles, and links it automatically. The Cargo.lock file pins exact versions of all dependencies (including transitive ones), ensuring reproducible builds, a critical property for security-sensitive software supply chains.

    The three Fixme challenges collectively introduce the most common Rust beginner stumbling blocks: syntax rules, borrowing, and unsafe. These skills translate directly to reading real Rust security tooling; many modern fuzzing frameworks (cargo-fuzz), memory-safe rewrites of critical utilities (ripgrep, fd, bat), and security research tools are written in Rust. Understanding the basics opens up a growing ecosystem of high-quality software.

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{n0w_y0uv3_f1x3d_1h3m_...}

Rust requires explicit `unsafe` blocks around code that might violate safety guarantees, even when the code was provided by the challenge.

Key takeaway

The unsafe keyword draws an explicit, auditable line between what the compiler verifies and what the programmer asserts. Instead of allowing unsafe operations anywhere, as C does, Rust corrals every raw pointer dereference, FFI call, and mutable static into a labeled region, so an auditor can grep for it and review only those sites. Minimizing and isolating trusted code is the same idea behind privilege separation, sandboxing, and capability restrictions.

Related reading

Useful tools for General Skills

Where to go next