Skip to content

Commit 84fb9e4

Browse files
committed
ensure no extra data is every written to stdout
1 parent 55a1931 commit 84fb9e4

File tree

2 files changed

+44
-12
lines changed

2 files changed

+44
-12
lines changed

ffi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ alloy = { workspace = true }
1212
anyhow = { workspace = true }
1313
clap = { version = "4.5", features = ["derive", "env"] }
1414
hex = { version = "0.4" }
15+
libc = "0.2.159"
1516
risc0-ethereum-contracts = { workspace = true }
1617
risc0-zkvm = { workspace = true, features = ["client"] }
1718

ffi/src/main.rs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use std::io::Write;
15+
use std::{
16+
fs::{File, OpenOptions},
17+
io,
18+
io::Write,
19+
os::unix::io::{AsRawFd, FromRawFd},
20+
};
1621

1722
use alloy::{primitives::Bytes, sol_types::SolValue};
18-
use anyhow::{Context, Result};
23+
use anyhow::{ensure, Context, Result};
1924
use clap::Parser;
2025
use risc0_ethereum_contracts::encode_seal;
2126
use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts, VerifierContext};
@@ -35,7 +40,10 @@ enum Command {
3540

3641
/// Run the CLI.
3742
pub fn main() -> Result<()> {
38-
match Command::parse() {
43+
// Take stdout is ensure no extra data is written to it.
44+
let mut stdout = take_stdout()?;
45+
46+
let output = match Command::parse() {
3947
Command::Prove {
4048
guest_binary_path,
4149
input,
@@ -45,22 +53,19 @@ pub fn main() -> Result<()> {
4553
)?,
4654
};
4755

56+
// Forge test FFI calls expect hex encoded bytes sent to stdout
57+
write!(&mut stdout, "{}", hex::encode(output)).context("failed to write to stdout")?;
58+
stdout.flush().context("failed to flush stdout")?;
59+
4860
Ok(())
4961
}
5062

5163
/// Prints on stdio the Ethereum ABI and hex encoded proof.
52-
fn prove_ffi(elf_path: String, input: Vec<u8>) -> Result<()> {
64+
fn prove_ffi(elf_path: String, input: Vec<u8>) -> Result<Vec<u8>> {
5365
let elf = std::fs::read(elf_path).expect("failed to read guest ELF");
5466
let (journal, seal) = prove(&elf, &input)?;
5567
let calldata = (Bytes(journal.into()), Bytes(seal.into()));
56-
let output = hex::encode(calldata.abi_encode());
57-
58-
// Forge test FFI calls expect hex encoded bytes sent to stdout
59-
print!("{output}");
60-
std::io::stdout()
61-
.flush()
62-
.context("failed to flush stdout buffer")?;
63-
Ok(())
68+
Ok(calldata.abi_encode())
6469
}
6570

6671
/// Generates journal and snark seal as a pair (`Vec<u8>`, `Vec<u8>)
@@ -78,3 +83,29 @@ fn prove(elf: &[u8], input: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
7883
let seal = encode_seal(&receipt)?;
7984
Ok((journal, seal))
8085
}
86+
87+
/// "Takes" stdout such, returning a handle and ensuring no other code in this process can write to
88+
/// it. This is used to ensure that no additional data (e.g. log lines) is written to stdout, as
89+
/// any extra will cause a decoding failure.
90+
fn take_stdout() -> Result<File> {
91+
let stdout = io::stdout();
92+
let mut handle = stdout.lock();
93+
// Ensure all buffered data is written before redirection
94+
handle.flush()?;
95+
96+
let devnull = OpenOptions::new().write(true).open("/dev/null")?;
97+
98+
unsafe {
99+
// Create a copy of stdout to use for our output.
100+
let dup_fd = libc::dup(handle.as_raw_fd());
101+
ensure!(dup_fd >= 0, "call to libc::dup failed: {}", dup_fd);
102+
// Redirect stdout to the fd we opened for /dev/null
103+
let dup2_result = libc::dup2(devnull.as_raw_fd(), libc::STDOUT_FILENO);
104+
ensure!(
105+
dup2_result >= 0,
106+
"call to libc::dup2 failed: {}",
107+
dup2_result
108+
);
109+
Ok(File::from_raw_fd(dup_fd))
110+
}
111+
}

0 commit comments

Comments
 (0)