1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
use std::io::Write;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use crate::ast::*;
use crate::error::ShellError;
use crate::eval::Shell;
use crate::shell_bytes::ShellBytes;
use crate::sys;
/// Saved file descriptor for restoration after redirections.
pub(crate) struct SavedFd {
pub(crate) target_fd: RawFd,
pub(crate) saved_copy: Option<RawFd>,
}
impl Shell {
/// Apply redirections. Returns saved FD state for restoration.
pub(crate) fn setup_redirections(
&mut self,
redirs: &[Redir],
) -> crate::error::Result<Vec<SavedFd>> {
let mut saved = Vec::new();
for redir in redirs {
let target_fd = redir.fd;
let preserve_fd = !(target_fd == 0
&& matches!(
redir.kind,
RedirKind::HereDoc(_) | RedirKind::HereDocStrip(_)
)
&& (self.stdout_sink.is_some() || self.stderr_sink.is_some()));
if preserve_fd {
let saved_copy = {
let copy = sys::fcntl_dupfd_cloexec(target_fd, 10);
if copy >= 0 { Some(copy) } else { None }
};
saved.push(SavedFd {
target_fd,
saved_copy,
});
}
match &redir.kind {
RedirKind::Input(word) => {
let filename = self.expand_string(word)?;
let filepath =
self.resolve_path_bytes(&ShellBytes::from_str_lossless(&filename));
let file = std::fs::File::open(&filepath).map_err(|e| {
self.err_msg(&format!("{filename}: {e}"));
ShellError::Io(e)
})?;
// SAFETY: file fd is valid from File::open; target_fd is the redirect target.
unsafe {
sys::dup2(file.as_raw_fd(), target_fd);
}
}
RedirKind::Output(word) | RedirKind::Clobber(word) => {
let filename = self.expand_string(word)?;
let filepath =
self.resolve_path_bytes(&ShellBytes::from_str_lossless(&filename));
let file = std::fs::File::create(&filepath).map_err(|e| {
self.err_msg(&format!("{filename}: {e}"));
ShellError::Io(e)
})?;
// SAFETY: file fd is valid from File::create; target_fd is the redirect target.
unsafe {
sys::dup2(file.as_raw_fd(), target_fd);
}
}
RedirKind::Append(word) => {
let filename = self.expand_string(word)?;
let filepath =
self.resolve_path_bytes(&ShellBytes::from_str_lossless(&filename));
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.append(true)
.open(&filepath)
.map_err(|e| {
self.err_msg(&format!("{filename}: {e}"));
ShellError::Io(e)
})?;
// SAFETY: file fd is valid from OpenOptions::open; target_fd is the redirect target.
unsafe {
sys::dup2(file.as_raw_fd(), target_fd);
}
}
RedirKind::ReadWrite(word) => {
let filename = self.expand_string(word)?;
let filepath =
self.resolve_path_bytes(&ShellBytes::from_str_lossless(&filename));
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&filepath)
.map_err(|e| {
self.err_msg(&format!("{filename}: {e}"));
ShellError::Io(e)
})?;
// SAFETY: file fd is valid from OpenOptions::open; target_fd is the redirect target.
unsafe {
sys::dup2(file.as_raw_fd(), target_fd);
}
}
RedirKind::DupInput(word) | RedirKind::DupOutput(word) => {
let fd_str = self.expand_string(word)?;
if fd_str == "-" {
// SAFETY: target_fd is a valid fd number from the redirect syntax.
unsafe {
sys::close(target_fd);
}
} else if let Ok(source_fd) = fd_str.parse::<i32>() {
// SAFETY: source_fd is user-specified (may fail at OS level); target_fd is from redirect syntax.
unsafe {
sys::dup2(source_fd, target_fd);
}
} else {
return Err(ShellError::Runtime {
msg: format!("{fd_str}: bad file descriptor"),
span: redir.span,
});
}
}
RedirKind::HereDoc(body) | RedirKind::HereDocStrip(body) => {
let mut fds = [0i32; 2];
// SAFETY: fds is a valid 2-element array for pipe() to write into.
unsafe {
sys::pipe(fds.as_mut_ptr());
}
// SAFETY: fds[1] is a valid fd just returned by pipe().
let write_end = unsafe { std::fs::File::from_raw_fd(fds[1]) };
let read_fd = fds[0];
let expanded = match body {
HereDocBody::Literal(s) => s.clone(),
HereDocBody::Parsed(parts) => {
let word = Word {
parts: parts.clone(),
span: redir.span,
};
crate::shell_bytes::ShellBytes::from_str_lossless(
&self.expand_string(&word)?,
)
}
};
let _ = (&write_end).write_all(expanded.as_bytes());
drop(write_end);
// When sinks are active the shell is embedded in a multi-threaded
// process. Calling dup2(read_fd, 0) in the parent would replace the
// process-wide fd 0 and race with other threads reading stdin (e.g.
// nerv's keyboard input loop). Instead, stash the read fd and let
// eval_external pass it via cmd.stdin() after fork — only the child
// inherits it. For builtins/functions that need stdin, the dup2 path
// is still used since they run in-process on this thread.
if target_fd == 0 && (self.stdout_sink.is_some() || self.stderr_sink.is_some())
{
// Close any previously pending stdin (shouldn't happen in practice).
if let Some(old) = self.pending_stdin.take() {
// SAFETY: old is a valid fd we own.
unsafe {
sys::close(old);
}
}
self.pending_stdin = Some(read_fd);
// No SavedFd entry needed: we never touched fd 0.
} else {
// SAFETY: read_fd is valid from pipe(); target_fd is the redirect target.
unsafe {
sys::dup2(read_fd, target_fd);
sys::close(read_fd);
}
}
}
}
}
Ok(saved)
}
/// Restore file descriptors after redirections.
pub(crate) fn restore_redirections(&self, saved: Vec<SavedFd>) {
for s in saved.into_iter().rev() {
if let Some(copy) = s.saved_copy {
// SAFETY: copy is a valid fd from fcntl_dupfd_cloexec; target_fd is the original fd being restored.
unsafe {
sys::dup2(copy, s.target_fd);
sys::close(copy);
}
}
}
}
}