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.
tar -xvf fixme3.tar.gz && cd fixme3cargo runless src/main.rsSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Wrap the unsafe callObservationI noticed the compiler errorcall to unsafe function is unsafe and requires unsafe blockand that lines 22 and 34 insrc/main.rsalready contained the correctunsafe { give_flag(); }structure but with//comment markers, which suggested that uncommenting those two lines was the exact fix the challenge was designed around.Uncomment theunsafe { ... }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 oneThe code compiles and runs, but if you add a duplicate call outside the structure the challenge expects, you may introduce a double-invocation or leave the original commented-out lines in place. The simpler correct approach is to remove the two
//comment markers already in the file at lines 22 and 34, since the scaffold was intentionally written that way - no new code is required.Tried: Mark the
give_flagfunction declaration itself asunsafe fn give_flag()to try to avoid needing an unsafe block at the call siteMaking the function declaration unsafe does not remove the need for an unsafe block; it makes the situation worse. Callers of an
unsafe fnare still required to wrap every call inunsafe { ... }. The compiler errorcall to unsafe function is unsafe and requires unsafe blockpersists because the obligation falls on the caller, not the function signature. The fix is always at the call site.Learn more
Rust's
unsafekeyword is a signal to both the compiler and human readers that the enclosed code bypasses Rust's automatic safety guarantees. Inside anunsafe { }block, five additional operations become legal:- Dereferencing a raw pointer (
*const T/*mut T). - Calling an
unsafefunction (including any C library through FFI). - Reading from or writing to a mutable
static. - Implementing an
unsafetrait (e.g.Send/Syncon a type that holds raw pointers). - Accessing a field of a
union(the compiler can't verify the active variant).
The
unsafeblock 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: everyunsafein 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'sstrlen, which Rust marksunsafebecause the caller must guaranteeptrpoints 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 unsafeopen(2)syscall and raw file-descriptor manipulation. The stdlib author wrote and audited theunsafeinternals, then exposed an API that callers can use without ever typingunsafethemselves. Most application code stays entirely safe by leaning on those wrappers.- Dereferencing a raw pointer (
Step 2
Rebuild and runObservationI noticed that the only obstacle was the missingunsafeblock and that no other logic or algorithm needed to change, which suggested that a singlecargo runafter the edit would compile cleanly and print the flag immediately.Re-runcargo runand the binary now prints the picoCTF flag immediately.Learn more
Cargo is Rust's integrated build system and package manager.
cargo runcompiles the project (in debug mode by default) and immediately executes the resulting binary.cargo build --releaseproduces 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.tomlandCargo.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. TheCargo.lockfile 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.
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.