Skip to content

Commit 761e09b

Browse files
committed
userland: shared DNS resolver + shell stderr fd wiring
Kernel syscall_resolve now maps each DnsResolveError variant to a distinct errno (EINVAL/ENETUNREACH/EAGAIN/EHOSTUNREACH) instead of flattening everything to EHOSTUNREACH, and slibc's SyscallError gains matching ENETUNREACH/EHOSTUNREACH constants. A new userland::net module is the single canonical hostname path: resolve_host returns Result<Ipv4Addr, ResolveError> with a Display impl per variant. The low-level syscall::net::resolve wrapper is now pub(crate) so a new tool physically cannot bypass the shared helper. curl, ping, nc, and the shell's cmd_resolve builtin all route through resolve_host — one place for IPv4 literal parsing, one place for errno translation, one user-facing message format. Shell stdio wiring: previously only fd 0 was dup'd onto the PTY slave, so fd 1/2 in the shell (and every forked child) still pointed at the default console TTY inherited from init — i.e. the kernel serial port. That made curl/ping stderr invisible in the shell window. Dup the slave onto fd 0/1/2 so children's output flows through the ldisc to the PTY master and lands in the shell display via forward_compositor_keyboard, plus drain the master one more time after waitpid in both the capture and pipeline paths so bytes written right before a child exits aren't stranded. 2352/2352 itests pass.
1 parent 12b73f0 commit 761e09b

12 files changed

Lines changed: 230 additions & 148 deletions

File tree

core/src/syscall/net_handlers.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,6 @@ define_syscall!(syscall_resolve(ctx, args) requires(let process_id) {
557557
return ctx.err_with(ERRNO_EINVAL);
558558
}
559559

560-
// Copy hostname from user memory
561560
let mut hostname_buf = [0u8; 253];
562561
let user_hostname = try_or_err!(ctx, slopos_mm::user_ptr::UserBytes::try_new(args.arg0, hostname_len));
563562
let copied = try_or_err!(ctx, slopos_mm::user_copy::copy_bytes_from_user(user_hostname, &mut hostname_buf[..hostname_len]));
@@ -567,10 +566,14 @@ define_syscall!(syscall_resolve(ctx, args) requires(let process_id) {
567566

568567
let result_addr = match dns::dns_resolve(&hostname_buf[..hostname_len]) {
569568
Ok(addr) => addr,
570-
Err(_) => return ctx.err_with(ERRNO_EHOSTUNREACH),
569+
Err(dns::DnsResolveError::InvalidHostname) => return ctx.err_with(ERRNO_EINVAL),
570+
Err(dns::DnsResolveError::NoDnsServer) => return ctx.err_with(ERRNO_ENETUNREACH),
571+
Err(dns::DnsResolveError::Timeout | dns::DnsResolveError::TransmitFailed) => {
572+
return ctx.err_with(ERRNO_EAGAIN);
573+
}
574+
Err(dns::DnsResolveError::ParseFailed) => return ctx.err_with(ERRNO_EHOSTUNREACH),
571575
};
572576

573-
// Copy result to user memory
574577
let user_result = try_or_err!(ctx, slopos_mm::user_ptr::UserBytes::try_new(args.arg2, 4));
575578
try_or_err!(ctx, slopos_mm::user_copy::copy_bytes_to_user(user_result, &result_addr));
576579

scripts/patch_std.sh

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,11 @@ if [ ! -d "$STD_SYS" ]; then
1818
exit 1
1919
fi
2020

21-
# Note: no outer idempotency marker.
22-
#
23-
# An earlier version of this script used a `.slopos_patched` marker file
24-
# to short-circuit re-runs. That was unsafe by design — when new patch
25-
# sections were added to the script, the outer gate prevented them from
26-
# ever running against sysroots that had been marked by an older script
27-
# version, leaving the std library silently half-patched (the `net`
28-
# section's 2026-04-05 addition is an example that went unnoticed for
29-
# weeks because nothing exercises `std::net::ToSocketAddrs` at build
30-
# time).
31-
#
32-
# Every patch section below is individually idempotent via its own
33-
# `grep -q 'target_os = "slopos"'` guard, so re-running the script on
34-
# an already-patched sysroot is a no-op (~50 fast sed passes). We
35-
# remove the stale marker if we see one so old installations recover.
21+
# Don't add an outer idempotency marker — a stale marker would prevent
22+
# new patch sections from applying to already-patched sysroots. Every
23+
# section below is individually idempotent via its own `grep -q` guard,
24+
# and the post-patch verification at the bottom catches drift. Strip
25+
# any legacy marker so old installations recover.
3626
MARKER="$STD_SYS/.slopos_patched"
3727
if [ -f "$MARKER" ]; then
3828
rm -f "$MARKER"
@@ -397,14 +387,9 @@ if ! grep -q 'target_os = "slopos"' "$STD_OS/mod.rs" 2>/dev/null; then
397387
patch_os_fd
398388
fi
399389

400-
# 4. Post-patch verification
401-
#
402-
# Catches the "script updated, sysroot stale" drift that used to produce
403-
# a silently half-patched std library (see comment at the top of this
404-
# file). Each entry below is a (label, file, anchor) triple: after all
405-
# patches run, every anchor MUST be present in its file or we bail out.
406-
# Adding a new patch section means adding a matching verification row.
407-
390+
# Post-patch verification. Each new patch section above needs a
391+
# matching check_patched row so drift fails the build loudly instead
392+
# of leaving the sysroot half-patched.
408393
failed=0
409394
check_patched() {
410395
local label="$1"
@@ -440,7 +425,6 @@ check_patched "sync/rwlock/mod.rs" "$STD_SYS/sync/rwlock/mod.rs"
440425
check_patched "sync/once/mod.rs" "$STD_SYS/sync/once/mod.rs" 'target_os = "slopos"'
441426
check_patched "sync/thread_parking/mod.rs" "$STD_SYS/sync/thread_parking/mod.rs" 'target_os = "slopos"'
442427

443-
# Net routing — this is the one that silently broke for weeks.
444428
check_patched "net/connection/mod.rs" "$STD_SYS/net/connection/mod.rs" 'target_os = "slopos"'
445429
check_patched "net/connection/socket/mod.rs" "$STD_SYS/net/connection/socket/mod.rs" 'target_os = "slopos"'
446430
check_patched "net/connection/socket/slopos.rs" "$STD_SYS/net/connection/socket/slopos.rs" 'SlopOS platform implementation for `std::net`'

slibc/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ impl SyscallError {
3434
pub const EROFS: Self = Self(30);
3535
pub const EPIPE: Self = Self(32);
3636
pub const ENOSYS: Self = Self(38);
37+
pub const ENETUNREACH: Self = Self(101);
3738
pub const ETIMEDOUT: Self = Self(110);
3839
pub const ECONNREFUSED: Self = Self(111);
40+
pub const EHOSTUNREACH: Self = Self(113);
3941

4042
#[inline]
4143
pub const fn from_errno(errno: i32) -> Self {
@@ -77,8 +79,10 @@ impl SyscallError {
7779
30 => "Read-only file system",
7880
32 => "Broken pipe",
7981
38 => "Function not implemented",
82+
101 => "Network is unreachable",
8083
110 => "Connection timed out",
8184
111 => "Connection refused",
85+
113 => "No route to host",
8286
_ => "Unknown error",
8387
}
8488
}

userland/src/apps/curl.rs

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::io::{self, Read, Write};
2-
use std::net::{Ipv4Addr, Shutdown, SocketAddrV4, TcpStream, ToSocketAddrs};
2+
use std::net::{Ipv4Addr, Shutdown, SocketAddrV4, TcpStream};
33
use std::time::Duration;
44

5+
use crate::net::ResolveError;
56
use crate::syscall::process;
67

78
const MAX_HEADERS: usize = 8;
@@ -68,7 +69,7 @@ enum CurlError {
6869
InvalidHost,
6970
InvalidPort,
7071
InvalidPath,
71-
ResolveFailed,
72+
Resolve(ResolveError),
7273
SocketFailed,
7374
ConnectFailed,
7475
SendFailed,
@@ -86,6 +87,10 @@ fn print_usage() {
8687
}
8788

8889
fn print_error(err: CurlError) {
90+
if let CurlError::Resolve(reason) = err {
91+
eprintln!("curl: {}", reason);
92+
return;
93+
}
8994
let msg = match err {
9095
CurlError::Usage => "curl: invalid usage",
9196
CurlError::InvalidFlag => "curl: invalid flag",
@@ -97,7 +102,7 @@ fn print_error(err: CurlError) {
97102
CurlError::InvalidHost => "curl: invalid host",
98103
CurlError::InvalidPort => "curl: invalid port",
99104
CurlError::InvalidPath => "curl: invalid path",
100-
CurlError::ResolveFailed => "curl: hostname resolution failed",
105+
CurlError::Resolve(_) => unreachable!("handled above"),
101106
CurlError::SocketFailed => "curl: socket creation failed",
102107
CurlError::ConnectFailed => "curl: connect failed",
103108
CurlError::SendFailed => "curl: send failed",
@@ -303,31 +308,13 @@ fn parse_url(url: &[u8]) -> Result<ParsedUrl, CurlError> {
303308
})
304309
}
305310

306-
fn parse_ipv4(host: &[u8]) -> Option<[u8; 4]> {
307-
let host_str = core::str::from_utf8(host).ok()?;
308-
Some(host_str.parse::<Ipv4Addr>().ok()?.octets())
309-
}
310-
311311
fn resolve_host(parsed: &mut ParsedUrl) -> Result<(), CurlError> {
312312
let host = &parsed.host[..parsed.host_len];
313-
if let Some(ip) = parse_ipv4(host) {
314-
parsed.ip = ip;
315-
return Ok(());
316-
}
317-
318-
let host_str = core::str::from_utf8(host).map_err(|_| CurlError::ResolveFailed)?;
319-
let addr = (host_str, 0u16)
320-
.to_socket_addrs()
321-
.ok()
322-
.and_then(|mut addrs| addrs.find(|a| a.is_ipv4()))
323-
.ok_or(CurlError::ResolveFailed)?;
324-
match addr {
325-
std::net::SocketAddr::V4(v4) => {
326-
parsed.ip = v4.ip().octets();
327-
Ok(())
328-
}
329-
_ => Err(CurlError::ResolveFailed),
330-
}
313+
let host_str = core::str::from_utf8(host)
314+
.map_err(|_| CurlError::Resolve(ResolveError::InvalidHostname))?;
315+
let addr = crate::net::resolve_host(host_str).map_err(CurlError::Resolve)?;
316+
parsed.ip = addr.octets();
317+
Ok(())
331318
}
332319

333320
fn choose_method(config: &CurlConfig) -> &str {

userland/src/apps/nc/mod.rs

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ enum NcError {
4646
MissingHost,
4747
MissingPort,
4848
InvalidPort,
49-
ResolveFailed,
49+
Resolve(crate::net::ResolveError),
5050
UnknownFlag,
5151
}
5252

@@ -178,11 +178,15 @@ fn print_usage() {
178178
}
179179

180180
fn print_error(err: NcError) {
181+
if let NcError::Resolve(reason) = err {
182+
eprintln!("nc: {}", reason);
183+
return;
184+
}
181185
let msg = match err {
182186
NcError::MissingHost => "nc: missing host",
183187
NcError::MissingPort => "nc: missing port",
184188
NcError::InvalidPort => "nc: invalid port number",
185-
NcError::ResolveFailed => "nc: cannot resolve hostname",
189+
NcError::Resolve(_) => unreachable!("handled above"),
186190
NcError::UnknownFlag => "nc: unknown flag",
187191
};
188192
eprintln!("{msg}");
@@ -196,23 +200,10 @@ fn parse_port(s: &str) -> Option<u16> {
196200
Some(parsed)
197201
}
198202

199-
/// Parse a dotted-quad IPv4 address (e.g. "10.0.2.2").
200-
fn parse_ipv4(s: &str) -> Option<[u8; 4]> {
201-
Some(s.parse::<Ipv4Addr>().ok()?.octets())
202-
}
203-
204-
/// Resolve a host argument: try dotted-quad first, then kernel DNS.
205203
fn resolve_host(host: &[u8]) -> Result<[u8; 4], NcError> {
206-
if let Ok(host_str) = core::str::from_utf8(host)
207-
&& let Some(ip) = parse_ipv4(host_str)
208-
{
209-
return Ok(ip);
210-
}
211-
// Try kernel DNS resolution
212-
match crate::syscall::net::resolve(host) {
213-
Some(ip) => Ok(ip),
214-
_ => Err(NcError::ResolveFailed),
215-
}
204+
let host_str = core::str::from_utf8(host)
205+
.map_err(|_| NcError::Resolve(crate::net::ResolveError::InvalidHostname))?;
206+
crate::net::resolve_host_raw(host_str).map_err(NcError::Resolve)
216207
}
217208

218209
/// Core argument parsing logic operating on clean Rust slices.
@@ -437,27 +428,6 @@ mod tests {
437428
assert_eq!(parse_port("99999"), None);
438429
}
439430

440-
#[test]
441-
fn test_parse_ipv4_valid() {
442-
assert_eq!(parse_ipv4("10.0.2.2"), Some([10, 0, 2, 2]));
443-
assert_eq!(parse_ipv4("192.168.1.1"), Some([192, 168, 1, 1]));
444-
assert_eq!(parse_ipv4("0.0.0.0"), Some([0, 0, 0, 0]));
445-
assert_eq!(parse_ipv4("255.255.255.255"), Some([255, 255, 255, 255]));
446-
assert_eq!(parse_ipv4("127.0.0.1"), Some([127, 0, 0, 1]));
447-
}
448-
449-
#[test]
450-
fn test_parse_ipv4_invalid() {
451-
assert_eq!(parse_ipv4(""), None);
452-
assert_eq!(parse_ipv4("10.0.2"), None);
453-
assert_eq!(parse_ipv4("10.0.2.2.1"), None);
454-
assert_eq!(parse_ipv4("256.0.0.1"), None);
455-
assert_eq!(parse_ipv4("10.0.2.abc"), None);
456-
assert_eq!(parse_ipv4("..."), None);
457-
assert_eq!(parse_ipv4("1.2.3."), None);
458-
assert_eq!(parse_ipv4(".1.2.3"), None);
459-
}
460-
461431
// -----------------------------------------------------------------------
462432
// Argument parsing tests
463433
// -----------------------------------------------------------------------

userland/src/apps/ping.rs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,6 @@ fn print_usage() {
3636
println!("usage: ping [-c count] [-i interval] [-s size] [-W timeout] [-v] <host>");
3737
}
3838

39-
fn parse_ipv4(host: &str) -> Option<[u8; 4]> {
40-
Some(host.parse::<Ipv4Addr>().ok()?.octets())
41-
}
42-
43-
fn resolve_host(host: &str) -> Option<[u8; 4]> {
44-
if let Some(ip) = parse_ipv4(host) {
45-
return Some(ip);
46-
}
47-
net::resolve(host.as_bytes())
48-
}
49-
5039
fn parse_u32_arg(value: &str) -> Option<u32> {
5140
value.parse::<u32>().ok()
5241
}
@@ -271,11 +260,12 @@ pub fn ping_main(args: Vec<String>) -> ! {
271260
}
272261
};
273262

274-
let target_ip = if let Some(ip) = resolve_host(&config.host) {
275-
ip
276-
} else {
277-
eprintln!("ping: cannot resolve {}", config.host);
278-
std::process::exit(2);
263+
let target_ip = match crate::net::resolve_host_raw(&config.host) {
264+
Ok(ip) => ip,
265+
Err(err) => {
266+
eprintln!("ping: {}: {}", config.host, err);
267+
std::process::exit(2);
268+
}
279269
};
280270

281271
let fd = match net::socket(

userland/src/apps/shell/builtins/system.rs

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -338,48 +338,21 @@ pub fn cmd_resolve(argc: i32, argv: &[&[u8]]) -> i32 {
338338
return 1;
339339
}
340340

341-
let hostname = argv[1];
342-
343-
let host_str = match core::str::from_utf8(hostname) {
341+
let host_str = match core::str::from_utf8(argv[1]) {
344342
Ok(s) => s,
345343
Err(_) => {
346344
shell_write(b"resolve: invalid hostname\n");
347345
return 1;
348346
}
349347
};
350348

351-
// Surface the real `io::Error` from `to_socket_addrs` — the
352-
// previous implementation discarded it with `.ok()`, which
353-
// meant a "operation not supported" PAL error from a
354-
// half-patched std sysroot looked identical to a real "name not
355-
// found" DNS failure, and the user had no way to tell from
356-
// the shell alone.
357-
use std::net::ToSocketAddrs;
358-
let iter = match (host_str, 0u16).to_socket_addrs() {
359-
Ok(it) => it,
360-
Err(err) => {
361-
shell_write(format!("resolve: {}: {}\n", host_str, err).as_bytes());
362-
return 1;
363-
}
364-
};
365-
366-
let v4 = iter
367-
.filter_map(|a| match a {
368-
std::net::SocketAddr::V4(v4) => Some(v4),
369-
_ => None,
370-
})
371-
.next();
372-
373-
match v4 {
374-
Some(v4) => {
375-
let o = v4.ip().octets();
376-
shell_write(
377-
format!("{}: -> {}.{}.{}.{}\n", host_str, o[0], o[1], o[2], o[3]).as_bytes(),
378-
);
349+
match crate::net::resolve_host(host_str) {
350+
Ok(addr) => {
351+
shell_write(format!("{}: -> {}\n", host_str, addr).as_bytes());
379352
0
380353
}
381-
None => {
382-
shell_write(format!("resolve: {}: no IPv4 address returned\n", host_str).as_bytes());
354+
Err(err) => {
355+
shell_write(format!("resolve: {}: {}\n", host_str, err).as_bytes());
383356
1
384357
}
385358
}

userland/src/apps/shell/exec.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ fn execute_registry_spawn(
570570
Err(_) => break,
571571
}
572572
}
573+
forward_compositor_keyboard();
573574
exit_status = st;
574575
break;
575576
}
@@ -1012,7 +1013,6 @@ fn execute_pipeline(pipeline: &ParsedPipeline, tokens: &ParsedTokens) -> i32 {
10121013
break;
10131014
}
10141015
}
1015-
// Final drain after children have exited.
10161016
loop {
10171017
match fs::read_slice(capture_fd, &mut buf) {
10181018
Ok(0) => break,
@@ -1022,6 +1022,7 @@ fn execute_pipeline(pipeline: &ParsedPipeline, tokens: &ParsedTokens) -> i32 {
10221022
Err(_) => break,
10231023
}
10241024
}
1025+
forward_compositor_keyboard();
10251026
let _ = fs::close_fd_raw(capture_fd);
10261027
leave_foreground();
10271028
return status;

userland/src/apps/shell/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,8 @@ fn shell_interactive_main() {
297297
if let Ok(slave_fd) = fs::ioctl_tiocgptpeer(master_fd.raw()) {
298298
set_shell_pty_master_fd(master_fd.into_raw());
299299
let _ = fs::dup2(slave_fd.raw(), 0);
300-
// slave_fd dropped here, auto-closed
300+
let _ = fs::dup2(slave_fd.raw(), 1);
301+
let _ = fs::dup2(slave_fd.raw(), 2);
301302
}
302303
}
303304
}

userland/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
pub mod apps;
44
pub mod gfx;
5+
pub mod net;
56
pub mod program_registry;
67
pub mod readiness;
78
pub mod runtime;

0 commit comments

Comments
 (0)