Skip to main content

atmos/kernel/
tls.rs

1#![allow(dead_code)]
2
3extern crate alloc;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use embedded_tls::blocking::{TlsConnection, TlsContext};
8use embedded_tls::TlsConfig;
9use rand_core::{CryptoRng, RngCore};
10
11// --- OsRng Implementation ---
12
13pub struct OsRng {
14    state: u32,
15}
16
17impl Default for OsRng {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23/// 【2026-07-24診断用】`get_ticks()`だけをシードにすると、短時間に連続して
24/// TLS接続を開く場合(例: 外部CSS/フォントの立て続けの取得)に同じ`tick`値を
25/// 読んでしまい、複数の接続で全く同一の「乱数」シーケンス(ClientHello random、
26/// 鍵交換の一時鍵等)が生成されうる。1回目のHTTPS接続は成功するが2回目以降が
27/// 毎回TLSハンドシェイクに失敗する未解明の現象の調査の一環として、呼び出し毎に
28/// 必ず異なる値になるよう単調増加カウンタを合成し、この可能性を切り分ける。
29static OS_RNG_CALL_COUNTER: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
30
31impl OsRng {
32    pub fn new() -> Self {
33        let t = crate::kernel::timer::get_ticks() as u32;
34        let call_no = OS_RNG_CALL_COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
35        let seed = t ^ call_no.wrapping_mul(0x9E37_79B9);
36        Self {
37            state: if seed == 0 { 0xA11C0B0B } else { seed },
38        }
39    }
40}
41
42impl RngCore for OsRng {
43    fn next_u32(&mut self) -> u32 {
44        let mut x = self.state;
45        x ^= x << 13;
46        x ^= x >> 17;
47        x ^= x << 5;
48        self.state = x;
49        x
50    }
51    fn next_u64(&mut self) -> u64 {
52        ((self.next_u32() as u64) << 32) | (self.next_u32() as u64)
53    }
54    fn fill_bytes(&mut self, dest: &mut [u8]) {
55        for chunk in dest.chunks_mut(4) {
56            let val = self.next_u32().to_le_bytes();
57            let len = chunk.len();
58            chunk.copy_from_slice(&val[..len]);
59        }
60    }
61    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
62        self.fill_bytes(dest);
63        Ok(())
64    }
65}
66
67impl CryptoRng for OsRng {}
68
69/// 【2026-08-05 計測】`retry_write` 経路で送信後に待った累計 tick と回数。
70///
71/// 変更の効果を「ページ読み込み全体」から推し量ると、
72/// ホスト負荷のばらつきに埋もれて A/B のための往復が何度も要る。
73/// **変えた当人を直接測れば 1 回の実行で判定できる。**
74pub static WRITE_WAIT_TICKS: core::sync::atomic::AtomicU32 =
75    core::sync::atomic::AtomicU32::new(0);
76pub static WRITE_WAIT_CALLS: core::sync::atomic::AtomicU32 =
77    core::sync::atomic::AtomicU32::new(0);
78/// そのうち再送に至った回数(=本当に応答が来なかった回)。
79pub static WRITE_RETX_COUNT: core::sync::atomic::AtomicU32 =
80    core::sync::atomic::AtomicU32::new(0);
81
82// --- TcpIoWrapper Implementation ---
83
84pub struct TcpIoWrapper {
85    pub local_port: u16,
86    buffer: Vec<u8>,
87    /// `buffer` の読み出し済み位置。
88    ///
89    /// 【2026-08-05】以前は読むたびに `buffer.drain(0..len)` していた。
90    /// TLS はレコードヘッダ 5 バイトのように**小さく読む**ので、
91    /// そのたびに残り全体を前へ詰め直すことになり O(n²) だった。
92    ///
93    /// 一斉計測で受信ループの内訳を割ったところ、
94    ///
95    /// ```text
96    /// 受信待ち=40968/16661
97    ///   受信sleep=8102/5191   recv呼出=233/6684
98    /// ```
99    ///
100    /// sleep でも recv でもない**残り 32633 tick** がここだった。
101    /// 位置を持てば詰め直しが要らない。
102    pos: usize,
103    // 【2026-07-24】書き込み後、応答が全く届かない場合に同一データを一度だけ
104    // 再送するかどうか。この自作TCPスタックには再送機構が無く、ネットワーク上
105    // でのパケットロス(特に外部CSS/フォント取得等、メインページ取得の直後に
106    // 連続して発生する2回目以降のHTTPS接続で顕著)でハンドシェイクが失敗する
107    // 問題への対策。既存の安定した経路(メインページ取得の`https_get`)へ
108    // 影響を与えないよう、`https_get_binary`(外部リソース取得側)でのみ
109    // `true`にする。
110    retry_write: bool,
111}
112
113impl TcpIoWrapper {
114    pub fn new(local_port: u16) -> Self {
115        Self {
116            local_port,
117            buffer: Vec::new(),
118            pos: 0,
119            retry_write: false,
120        }
121    }
122
123    pub fn new_with_retry_write(local_port: u16) -> Self {
124        Self {
125            local_port,
126            buffer: Vec::new(),
127            pos: 0,
128            retry_write: true,
129        }
130    }
131}
132
133impl embedded_io::ErrorType for TcpIoWrapper {
134    type Error = embedded_io::ErrorKind;
135}
136
137impl embedded_io::Read for TcpIoWrapper {
138    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
139        if buf.is_empty() {
140            return Ok(0);
141        }
142        let _pr = crate::os_lib::web_engine::perf::start();
143        let start_tick = crate::kernel::timer::get_ticks();
144        // 15秒タイムアウト。大きな応答 (検索結果 ~30KB 等) はサーバ処理 + 小受信ウィンドウ
145        // による多数の往復で到着が遅延バーストになりがちで、5秒では先頭バイト到着前に
146        // 取りこぼすことがあった。
147        let timeout_ticks = 1500usize; // 15秒 (1500 * 10ms)
148
149        // 【2026-08-05】待ちの粒度は 10ms(`sleep(1)`)だった。
150        //
151        // 内訳を測ると受信待ち 12496 tick のうち 12231 tick が
152        // この `sleep` そのもの(6234 回 × 約 2 tick)。
153        // データは 10ms より短い間隔で届きうるので、
154        // 眠るとそのぶん取りこぼす。
155        //
156        // 【2026-08-05 検証済み・入れていない】
157        // 「まず 32 回は `yield_now()` で粘る」を試し、`受信待ち` を直接測った。
158        //
159        // ```text
160        // 修正前: 受信待ち=12496  受信sleep=12231/6234
161        // 修正後: 受信待ち=11535  受信sleep=10741/30291
162        // ```
163        //
164        // **8% しか縮まらず、譲る回数は 5 倍**(6234 → 30291)。
165        // データは実際に 10ms より早くは届いていない。
166        // 縮まらないのに文脈切り替えを 3 万回増やすのは割に合わない。
167        //
168        // ここの待ちは**正当なネットワーク遅延**であり、無駄ではない。
169        // 削るなら待ち方ではなく、往復回数そのものを減らす方向
170        // (受信ウィンドウは検証済みで効かず。`spec/l1_l2_usb.md` 参照)。
171        loop {
172            if self.pos < self.buffer.len() {
173                let avail = self.buffer.len() - self.pos;
174                let len = avail.min(buf.len());
175                buf[..len].copy_from_slice(&self.buffer[self.pos..self.pos + len]);
176                self.pos += len;
177                // 読み切ったら領域を返す(容量は保つので再確保は起きない)。
178                if self.pos == self.buffer.len() {
179                    self.buffer.clear();
180                    self.pos = 0;
181                }
182                crate::os_lib::web_engine::perf::add(
183                    crate::os_lib::web_engine::perf::Slot::TlsReadWait,
184                    _pr,
185                );
186                return Ok(len);
187            }
188
189            // tcp_recv_real(.., 1) は即時タイムアウトで Err を返すが、これは
190            // 「まだデータが来ていない」だけであり EOF ではない。
191            // EOF 判定は下の tcp_is_socket_closed で行う(旧実装は Err を EOF と
192            // 誤解釈し、応答到着前の読みで body が空になるバグがあった)。
193            if let Ok(chunk) = crate::kernel::net::tcp_recv_real(self.local_port, 1) {
194                if !chunk.is_empty() {
195                    // 追記する前に、読み終わった前半を捨てて詰める。
196                    // 毎回の読み出しでは詰めない(そこが O(n²) の原因だった)。
197                    if self.pos > 0 {
198                        self.buffer.drain(0..self.pos);
199                        self.pos = 0;
200                    }
201                    self.buffer.extend_from_slice(&chunk);
202                    continue; // Loop back to read from buffer
203                }
204            }
205
206            if crate::kernel::net::tcp_is_socket_closed(self.local_port)
207                && self.pos >= self.buffer.len()
208            {
209                // 【2026-07-29診断】応答が Content-Length に届かず途中で切れる件。
210                // 受信ループがどの条件で抜けたかを一意に記録する。
211                return Ok(0); // EOF
212            }
213
214            // タイムアウト判定
215            let elapsed = crate::kernel::timer::get_ticks().wrapping_sub(start_tick);
216            if elapsed >= timeout_ticks {
217                // 【2026-08-02】**失敗は必ずログに残す**。
218                // 冗長な情報ログの整理をしたとき、このタイムアウトログまで
219                // 一緒に消してしまい「失敗しているのに何も出ない」状態を作った。
220                // 規約は `spec/logging_policy.md`。
221                crate::warn!(
222                    "[TLS][TIMEOUT] 受信タイムアウト local_port={} elapsed_ticks={} timeout_ticks={} seg_accepted={} seg_dropped={}",
223                    self.local_port,
224                    elapsed,
225                    timeout_ticks,
226                    crate::kernel::net::tcp::SEG_ACCEPTED.load(core::sync::atomic::Ordering::Relaxed),
227                    crate::kernel::net::tcp::SEG_DROPPED.load(core::sync::atomic::Ordering::Relaxed)
228                );
229                return Err(embedded_io::ErrorKind::TimedOut);
230            }
231
232            // 【2026-08-05 検証済み・変更せず】ここを「まず数十回は
233            // `yield_now()` で粘り、それでも来なければ眠る」に変えて測ったが、
234            // 3.7MB の取得は 3746 → 3704 tick で**変わらなかった**。
235            // 待ち方の粒度は律速ではない(律速は USB 転送の内側)。
236            //
237            crate::kernel::scheduler::sleep(1);
238        }
239    }
240}
241
242impl embedded_io::Write for TcpIoWrapper {
243    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
244        // 【2026-08-31 バグ修正】MSS 相当のセグメント分割が未実装だった。
245        //
246        // `build_tcp_ipv4` が組む IP+TCP ヘッダは 40 バイト(オプション無し)、
247        // `transmit_nic_ipv4_packet` はヘッダ込みで `NIC_TX_MAX_PAYLOAD`
248        // (1536 バイト)を超えると `Err("packet too large")` を返す。
249        // つまりこの `write()` に渡せる 1 回分の実ペイロードは最大 1496 バイト。
250        // TLS レコードは最大 16KB あり、`embedded_tls` がそれを分割せず
251        // まるごと渡してくると、そのまま `tcp_send_real` へ流れて失敗していた
252        // (`spec/TODO.md` の「TCP セグメント分割(MSS)が未実装」参照)。
253        //
254        // `embedded_io::Write::write()` の契約は「渡された分すべてを
255        // 送る」ではなく「送れた分だけ返す」(`Ok(n)`, `n <= buf.len()`)。
256        // 呼び出し側は残りを次の `write()` 呼び出しで送るのが仕様どおりの
257        // 振る舞いであり、この処理系専用の再送ロジック(下記)とは無関係の
258        // 別の仕組み。ここで安全な上限へ切り詰めるだけで済み、
259        // 以降の再送・シーケンス番号管理は一切変更しない。
260        const MAX_TCP_PAYLOAD: usize = 1400;
261        let buf = if buf.len() > MAX_TCP_PAYLOAD {
262            &buf[..MAX_TCP_PAYLOAD]
263        } else {
264            buf
265        };
266        // 【2026-07-24重要修正】以前は再送のたびに`tcp_send_real`を再度呼んで
267        // いたが、`tcp_send_real`は呼ぶ度に無条件で`snd_nxt`を進めてしまう
268        // ため、"再送"のつもりが実際には誤った(進みすぎた)シーケンス番号
269        // で送信してしまい、TCP/TLSストリームを破壊する重大なバグだった
270        // (結果として`embedded_tls`側が復号時に`InternalError`を返す原因に
271        // なっていたと判明)。正しい再送のため、最初の送信時点の`seq`を
272        // 記録しておき、再送には`snd_nxt`を変更しない`tcp_retransmit_real`
273        // を使う。
274        let seq_at_send = crate::kernel::net::tcp_current_snd_seq(self.local_port);
275        if crate::kernel::net::tcp_send_real(self.local_port, buf).is_err() {
276            return Err(embedded_io::ErrorKind::BrokenPipe);
277        }
278        if self.retry_write {
279            if let Some(seq) = seq_at_send {
280                // 送信直後、短い間隔を空けて何らかの応答(1バイトでも)が来て
281                // いるか確認する。何も来ておらず、かつソケットもまだ閉じられて
282                // いなければ、送信パケット自体がロストした可能性が高いと判断し、
283                // 同一データ・同一シーケンス番号で最大4回まで再送する。
284                // 【2026-08-05】以前はここで**確認する前に 600ms 眠って**いた
285                // (`sleep(60)` を先に実行してから応答を見る)。
286                //
287                // 応答が 1ms で返っても 600ms 待つので、
288                // TLS ハンドシェイクのように**書き込みが複数回**ある経路では
289                // それだけで数秒を捨てていた。
290                // 実測: 49KB の CSS 取得に 21 秒(3.7MB の画像が 27 秒=
291                // 137KB/s なのに対し、バイトあたり 50 倍遅い)。
292                //
293                // 待ち総量(600ms で応答が無ければ再送)は変えず、
294                // 短い間隔で確かめて**来ていたら即座に抜ける**。
295                /// 1 回の確認間隔(tick, 10ms 単位)。
296                const POLL_STEP: usize = 3;
297                /// 再送を決めるまでに待つ総量(従来と同じ 600ms)。
298                const WAIT_PER_TRY: usize = 60;
299                let t_w0 = crate::kernel::timer::get_ticks();
300                let _pw = crate::os_lib::web_engine::perf::start();
301                WRITE_WAIT_CALLS.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
302                'retry: for _ in 0..4 {
303                    // 【2026-08-05】待ち時間を**実経過**で数える。
304                    //
305                    // 以前は `waited += POLL_STEP`(指定値の足し算)だった。
306                    // だが一斉計測で
307                    //
308                    // ```text
309                    // 送信待ち=12191/39
310                    // sleep実測=12163/1105   ← ほぼ全部がこれ
311                    // 応答確認=9/1105  eth poll=16/1105
312                    // ```
313                    //
314                    // となり、**1105 回 × 3 tick = 3315 のはずが実測 12163**。
315                    // `sleep(3)` は実際には約 11 tick かかる(3.7 倍)。
316                    // スケジューラの `sleep` は「最低でもこれだけ」であって
317                    // 「ちょうどこれだけ」ではない。
318                    //
319                    // 指定値で数えていたので、600ms のつもりが実際は 2.2 秒。
320                    // 実経過で数えれば意図どおり 600ms で切り上がる。
321                    let t_wait0 = crate::kernel::timer::get_ticks();
322                    while crate::kernel::timer::get_ticks().wrapping_sub(t_wait0)
323                        < WAIT_PER_TRY
324                    {
325                        // 【2026-08-05】待ちの内訳を分解する。
326                        // 上限を 300ms にしても 1 回あたり 2.5 秒かかったので、
327                        // 時間は「待つと決めた量」ではなく別のところで消えている。
328                        let _ps = crate::os_lib::web_engine::perf::start();
329                        crate::kernel::scheduler::sleep(POLL_STEP);
330                        crate::os_lib::web_engine::perf::add(
331                            crate::os_lib::web_engine::perf::Slot::SleepActual,
332                            _ps,
333                        );
334                        let _pe = crate::os_lib::web_engine::perf::start();
335                        crate::kernel::usb::poll_ethernet_data_plane_only();
336                        crate::os_lib::web_engine::perf::add(
337                            crate::os_lib::web_engine::perf::Slot::EthPoll,
338                            _pe,
339                        );
340                        let _pk = crate::os_lib::web_engine::perf::start();
341                        let done = crate::kernel::net::tcp_recv_peek_nonempty(self.local_port)
342                            || crate::kernel::net::tcp_is_socket_closed(self.local_port);
343                        crate::os_lib::web_engine::perf::add(
344                            crate::os_lib::web_engine::perf::Slot::PeekCheck,
345                            _pk,
346                        );
347                        if done {
348                            break 'retry;
349                        }
350                    }
351                    // 総量を待っても何も来ない。パケットロストの疑いとして再送する。
352                    WRITE_RETX_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
353                    let _ = crate::kernel::net::tcp_retransmit_real(self.local_port, buf, seq);
354                }
355                WRITE_WAIT_TICKS.fetch_add(
356                    crate::kernel::timer::get_ticks().wrapping_sub(t_w0) as u32,
357                    core::sync::atomic::Ordering::Relaxed,
358                );
359                crate::os_lib::web_engine::perf::add(
360                    crate::os_lib::web_engine::perf::Slot::TlsWriteWait,
361                    _pw,
362                );
363            }
364        }
365        Ok(buf.len())
366    }
367
368    fn flush(&mut self) -> Result<(), Self::Error> {
369        Ok(())
370    }
371}
372
373
374/// 【2026-07-29】HTTPS 取得の結果を段階別に集計する。
375///
376/// 失敗段階が実行ごとにばらつくため、単発の観測では
377/// 「どの修正が効いたか」を評価できなかった(このセッションで
378/// 1 回の結果から誤った断定を何度も行った直接の原因)。
379/// 1 回のブート内で発生する全取得を分類して数え、
380/// 毎回サマリを出すことで成功率を数値で追えるようにする。
381/// 【2026-07-30】1 回の取得で許容する最大バイト数。0 なら無制限。
382/// 画像のような巨大リソースを `Content-Length` の時点で打ち切るために使う。
383/// 呼び出し側が設定し、取得後に必ず 0 へ戻すこと。
384pub static FETCH_BYTE_CAP: core::sync::atomic::AtomicUsize =
385    core::sync::atomic::AtomicUsize::new(0);
386
387pub mod stats {
388    use core::sync::atomic::{AtomicU32, Ordering::Relaxed};
389    pub static OK: AtomicU32 = AtomicU32::new(0);
390    pub static DNS_FAIL: AtomicU32 = AtomicU32::new(0);
391    pub static CONNECT_FAIL: AtomicU32 = AtomicU32::new(0);
392    pub static HANDSHAKE_FAIL: AtomicU32 = AtomicU32::new(0);
393    pub static TRUNCATED: AtomicU32 = AtomicU32::new(0);
394
395    pub fn bump(c: &AtomicU32) {
396        c.fetch_add(1, Relaxed);
397    }
398
399    /// 現在の集計を 1 行で出す。分母は全試行。
400    pub fn report(tag: &str) {
401        let ok = OK.load(Relaxed);
402        let dns = DNS_FAIL.load(Relaxed);
403        let conn = CONNECT_FAIL.load(Relaxed);
404        let hs = HANDSHAKE_FAIL.load(Relaxed);
405        let tr = TRUNCATED.load(Relaxed);
406        let total = ok + dns + conn + hs + tr;
407        crate::warn!(
408            "[NET][STATS] {} total={} ok={} dns_fail={} connect_fail={} handshake_fail={} truncated={}",
409            tag, total, ok, dns, conn, hs, tr
410        );
411    }
412}
413
414pub fn https_get(
415    host: &str,
416    path: &str,
417) -> Result<crate::kernel::net::HttpsGetResult, &'static str> {
418    let ordinal = crate::kernel::net::next_https_attempt_ordinal();
419    crate::warn!("[NET][DIAG] https_get ordinal={} host={} path={}", ordinal, host, path);
420    let remote_ip = crate::kernel::net::dns_query_a_via_udp(host).inspect_err(|e| {
421        crate::warn!("[NET][DIAG] https_get ordinal={} DNS FAILED err={}", ordinal, e);
422        stats::bump(&stats::DNS_FAIL);
423        stats::report("after-dns-fail");
424    })?;
425    crate::warn!("[NET][DIAG] https_get ordinal={} host={} resolved={}.{}.{}.{}", ordinal, host, remote_ip[0], remote_ip[1], remote_ip[2], remote_ip[3]);
426    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, 443).inspect_err(|e| {
427        crate::warn!("[NET][DIAG] https_get ordinal={} TCP CONNECT FAILED err={}", ordinal, e);
428        stats::bump(&stats::CONNECT_FAIL);
429        stats::report("after-connect-fail");
430    })?;
431
432    let mut stream = TcpIoWrapper::new(local_port);
433    let mut rng = OsRng::new();
434
435    let mut read_buf = alloc::vec![0u8; 18432]; // TLS最大レコード(16640)を収容
436    let mut write_buf = alloc::vec![0u8; 16384];
437
438    // Disable cert validation for bare metal
439    let config = TlsConfig::new().with_server_name(host);
440
441    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
442        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
443
444    // Perform TLS handshake
445    let mut provider =
446        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
447    let _ph = crate::os_lib::web_engine::perf::start();
448    let _open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
449    crate::os_lib::web_engine::perf::add(
450        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
451        _ph,
452    );
453    if let Err(e) = _open {
454        crate::warn!("[NET][DIAG] https_get ordinal={} TLS HANDSHAKE FAILED err={:?}", ordinal, e);
455        stats::bump(&stats::HANDSHAKE_FAIL);
456        stats::report("after-handshake-fail");
457        crate::warn!("[NET][DIAG] https_get ordinal={} about to tcp_close_real", ordinal);
458        let _ = crate::kernel::net::tcp_close_real(local_port);
459        crate::warn!("[NET][DIAG] https_get ordinal={} tcp_close_real returned, returning Err", ordinal);
460        return Err("TLS handshake failed");
461    }
462
463    // Now we can use `connection` which implements embedded_io::Read and Write
464    let request = alloc::format!("GET {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: Mozilla/5.0 (AtmOS Browser)\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n", path, host);
465    if embedded_io::Write::write_all(&mut connection, request.as_bytes()).is_err() {
466        crate::kernel::net::tcp_close_real(local_port)?;
467        return Err("HTTPS request failed");
468    }
469
470    // パケットをフラッシュして確実に送信
471    embedded_io::Write::flush(&mut connection).ok();
472
473    // パケット送出とサーバ側処理のためのウェイト
474    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
475    // 詳細は `net::tcp::wait_for_first_byte`。
476    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
477
478    let mut raw_bytes = Vec::new();
479    let mut chunk = [0u8; 1024];
480    loop {
481        match embedded_io::Read::read(&mut connection, &mut chunk) {
482            Ok(0) => break,
483            Ok(n) => raw_bytes.extend_from_slice(&chunk[..n]),
484            Err(e) => {
485                crate::info!("[NET] TLS read err: {:?}", e);
486                break;
487            }
488        }
489    }
490
491    // 【2026-07-26 バグ修正】応答を 1 バイトも読めなかった場合、これまでは
492    // そのまま `Ok` を返しており、呼び出し側のログにも
493    // `BROWSER: Load page SUCCESS. body_len=0` と「成功」として出ていた。
494    // 実際には何も取得できておらず、ブラウザは空ページを描画したうえで
495    // 外部 CSS の取得すら試みない(`<link>` が 1 つも無いため)。上位の
496    // リトライ機構も「成功」と受け取るので再試行されず、ページ読み込みの
497    // 機会が 1 回まるごと無駄になっていた。空応答は明確に失敗として返す。
498    if raw_bytes.is_empty() {
499        crate::warn!(
500            "[NET][DIAG] https_get ordinal={} EMPTY RESPONSE (0 bytes read) host={}",
501            ordinal,
502            host
503        );
504        let _ = crate::kernel::net::tcp_close_real(local_port);
505        return Err("empty https response");
506    }
507
508    let resp_str = match alloc::string::String::from_utf8(raw_bytes.clone()) {
509        Ok(s) => s,
510        Err(e) => {
511            let valid = e.into_bytes();
512            alloc::string::String::from_utf8_lossy(&valid).into_owned()
513        }
514    };
515
516    // プレビューは先頭 300 *文字*(バイト境界で切ると UTF-8 文字の途中に落ちてパニックする)。
517    let preview: String = resp_str.chars().take(300).collect();
518    crate::info!("[NET] HTTPS RAW RESP: [{} bytes] {:?}", resp_str.len(), preview);
519    let (status_code, reason, headers, body) = crate::kernel::net::parse_http_response(&resp_str)
520        .unwrap_or((
521            200,
522            String::from("OK"),
523            alloc::collections::BTreeMap::new(),
524            resp_str,
525        ));
526    let body_bytes = crate::kernel::net::parse_http_response_bytes(&raw_bytes)
527        .map(|(_, _, _, b)| b)
528        .unwrap_or_default();
529
530    crate::kernel::net::tcp_close_real(local_port)?;
531
532    crate::warn!(
533        "[NET][DIAG] https_get ordinal={} SUCCESS body_bytes={}",
534        ordinal,
535        body_bytes.len()
536    );
537
538    Ok(crate::kernel::net::HttpsGetResult {
539        remote_ip,
540        local_port,
541        tls_version: String::from("TLS1.3"),
542        cipher_suite: String::from("TLS_AES_128_GCM_SHA256"),
543        cert_subject: alloc::format!("CN={}", host),
544        cert_issuer: String::from("Unknown Root CA"),
545        cert_not_after: String::from("Unknown"),
546        status_code,
547        reason,
548        headers,
549        body,
550        body_bytes,
551    })
552}
553
554/// 任意メソッド + 任意ヘッダの HTTPS リクエスト(OAuth2 / REST API 用)。
555/// `Authorization: Bearer ...` などの追加ヘッダを付けて送れる。body が Some なら
556/// Content-Type / Content-Length を付与する(content_type が空なら付けない)。
557pub fn https_request(
558    method: &str,
559    host: &str,
560    path: &str,
561    extra_headers: &[(&str, &str)],
562    content_type: &str,
563    body: Option<&[u8]>,
564) -> Result<crate::kernel::net::HttpsGetResult, &'static str> {
565    let remote_ip = crate::kernel::net::dns_query_a_via_udp(host)?;
566    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, 443)?;
567
568    let mut stream = TcpIoWrapper::new(local_port);
569    let mut rng = OsRng::new();
570
571    let mut read_buf = alloc::vec![0u8; 18432];
572    let mut write_buf = alloc::vec![0u8; 16384];
573
574    let config = TlsConfig::new().with_server_name(host);
575    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
576        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
577    let mut provider =
578        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
579    let _ph = crate::os_lib::web_engine::perf::start();
580    let _open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
581    crate::os_lib::web_engine::perf::add(
582        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
583        _ph,
584    );
585    if let Err(e) = _open {
586        crate::warn!("[NET] TLS handshake error: {:?}", e);
587        let _ = crate::kernel::net::tcp_close_real(local_port);
588        return Err("TLS handshake failed");
589    }
590
591    let mut header = alloc::format!(
592        "{} {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: AtmOS/1.0\r\nAccept: application/json\r\n",
593        method,
594        path,
595        host
596    );
597    for (k, v) in extra_headers {
598        header.push_str(k);
599        header.push_str(": ");
600        header.push_str(v);
601        header.push_str("\r\n");
602    }
603    let body = body.unwrap_or(&[]);
604    if !content_type.is_empty() {
605        header.push_str(&alloc::format!("Content-Type: {}\r\n", content_type));
606    }
607    if !body.is_empty() || method == "POST" || method == "PUT" || method == "PATCH" {
608        header.push_str(&alloc::format!("Content-Length: {}\r\n", body.len()));
609    }
610    header.push_str("Accept-Encoding: identity\r\n");
611    header.push_str("Connection: close\r\n\r\n");
612
613    let mut request = header.into_bytes();
614    request.extend_from_slice(body);
615
616    if embedded_io::Write::write_all(&mut connection, &request).is_err() {
617        crate::kernel::net::tcp_close_real(local_port)?;
618        return Err("HTTPS request failed");
619    }
620    embedded_io::Write::flush(&mut connection).ok();
621    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
622    // 詳細は `net::tcp::wait_for_first_byte`。
623    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
624
625    let mut raw_bytes = Vec::new();
626    let mut chunk = [0u8; 1024];
627    loop {
628        match embedded_io::Read::read(&mut connection, &mut chunk) {
629            Ok(0) => break,
630            Ok(n) => raw_bytes.extend_from_slice(&chunk[..n]),
631            Err(_) => break,
632        }
633    }
634
635    let resp_str = match alloc::string::String::from_utf8(raw_bytes.clone()) {
636        Ok(s) => s,
637        Err(e) => alloc::string::String::from_utf8_lossy(&e.into_bytes()).into_owned(),
638    };
639    let (status_code, reason, headers, rbody) = crate::kernel::net::parse_http_response(&resp_str)
640        .unwrap_or((
641            200,
642            String::from("OK"),
643            alloc::collections::BTreeMap::new(),
644            resp_str,
645        ));
646    // ボディの生バイト列(バイナリダウンロード向け。JSON/テキストAPIは rbody を使えばよい)。
647    let body_bytes = crate::kernel::net::parse_http_response_bytes(&raw_bytes)
648        .map(|(_, _, _, b)| b)
649        .unwrap_or_default();
650    crate::kernel::net::tcp_close_real(local_port)?;
651
652    Ok(crate::kernel::net::HttpsGetResult {
653        remote_ip,
654        local_port,
655        tls_version: String::from("TLS1.3"),
656        cipher_suite: String::from("TLS_AES_128_GCM_SHA256"),
657        cert_subject: alloc::format!("CN={}", host),
658        cert_issuer: String::from("Unknown Root CA"),
659        cert_not_after: String::from("Unknown"),
660        status_code,
661        reason,
662        headers,
663        body: rbody,
664        body_bytes,
665    })
666}
667
668/// HTTPS POST。https_get と同じ TLS 経路で、リクエスト行を POST にし
669/// Content-Type / Content-Length ヘッダ + ボディを送信する。
670/// フォーム送信(DuckDuckGo lite の検索等)が HTTPS で正しく届くために必須。
671pub fn https_post(
672    host: &str,
673    path: &str,
674    content_type: &str,
675    body: &[u8],
676) -> Result<crate::kernel::net::HttpsGetResult, &'static str> {
677    let remote_ip = crate::kernel::net::dns_query_a_via_udp(host)?;
678    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, 443)?;
679
680    let mut stream = TcpIoWrapper::new(local_port);
681    let mut rng = OsRng::new();
682
683    let mut read_buf = alloc::vec![0u8; 18432]; // TLS最大レコード(16640)を収容
684    let mut write_buf = alloc::vec![0u8; 16384];
685
686    let config = TlsConfig::new().with_server_name(host);
687
688    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
689        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
690
691    let mut provider =
692        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
693    let _ph = crate::os_lib::web_engine::perf::start();
694    let _open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
695    crate::os_lib::web_engine::perf::add(
696        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
697        _ph,
698    );
699    if let Err(e) = _open {
700        crate::warn!("[NET] TLS handshake error: {:?}", e);
701        let _ = crate::kernel::net::tcp_close_real(local_port);
702        return Err("TLS handshake failed");
703    }
704
705    // ヘッダとボディを 1 つのバッファにまとめて送る
706    let header = alloc::format!(
707        "POST {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: Mozilla/5.0 (AtmOS Browser)\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: identity\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
708        path, host, content_type, body.len()
709    );
710    let mut request = header.into_bytes();
711    request.extend_from_slice(body);
712
713    if embedded_io::Write::write_all(&mut connection, &request).is_err() {
714        crate::kernel::net::tcp_close_real(local_port)?;
715        return Err("HTTPS POST request failed");
716    }
717    embedded_io::Write::flush(&mut connection).ok();
718    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
719    // 詳細は `net::tcp::wait_for_first_byte`。
720    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
721
722    let mut raw_bytes = Vec::new();
723    let mut chunk = [0u8; 1024];
724    loop {
725        match embedded_io::Read::read(&mut connection, &mut chunk) {
726            Ok(0) => break,
727            Ok(n) => raw_bytes.extend_from_slice(&chunk[..n]),
728            // ConnectionClosed は close 後の正常終了。蓄積済みデータを使う
729            Err(_) => break,
730        }
731    }
732
733    let resp_str = match alloc::string::String::from_utf8(raw_bytes.clone()) {
734        Ok(s) => s,
735        Err(e) => alloc::string::String::from_utf8_lossy(&e.into_bytes()).into_owned(),
736    };
737
738    crate::info!("[NET] HTTPS POST: {} bytes received", resp_str.len());
739    let (status_code, reason, headers, body) = crate::kernel::net::parse_http_response(&resp_str)
740        .unwrap_or((
741            200,
742            String::from("OK"),
743            alloc::collections::BTreeMap::new(),
744            resp_str,
745        ));
746    let body_bytes = crate::kernel::net::parse_http_response_bytes(&raw_bytes)
747        .map(|(_, _, _, b)| b)
748        .unwrap_or_default();
749
750    crate::kernel::net::tcp_close_real(local_port)?;
751
752    Ok(crate::kernel::net::HttpsGetResult {
753        remote_ip,
754        local_port,
755        tls_version: String::from("TLS1.3"),
756        cipher_suite: String::from("TLS_AES_128_GCM_SHA256"),
757        cert_subject: alloc::format!("CN={}", host),
758        cert_issuer: String::from("Unknown Root CA"),
759        cert_not_after: String::from("Unknown"),
760        status_code,
761        reason,
762        headers,
763        body,
764        body_bytes,
765    })
766}
767
768/// レスポンスヘッダから`Content-Length`(10進)を読み取る。無ければ`None`。
769/// 同一ホストの複数パスを **1 本の接続・1 回のハンドシェイク**で取得する。
770///
771/// # なぜ要るのか
772///
773/// 【2026-08-05 実測】1 ページの読み込みで
774/// `DNS=67/15 TCP接続=204/14 TLS握手=4118/14`。
775/// **ハンドシェイクだけで 4118 tick(約 41 秒)**、1 回あたり約 294 tick。
776/// DNS も TCP 接続も軽いので、重いのは TLS 交渉そのもの。
777/// 同じホストへ毎回張り直しているのが原因。
778///
779/// # 応答の切れ目
780///
781/// keep-alive では接続が閉じないので、**どこで 1 応答が終わるか**を
782/// 自分で決める必要がある。`Content-Length` を見て、その分だけ読む。
783/// 見つからない応答(chunked 等)が来たらそこで打ち切り、
784/// 残りは呼び出し元が個別取得へ切り替えられるよう `None` を返す。
785///
786/// # 戻り値
787///
788/// `paths` と同じ長さ。取れなかったものは `None`。
789/// 途中で接続が壊れたら、それ以降は全部 `None`(呼び出し元が再取得する)。
790pub fn https_get_binary_keepalive(
791    host: &str,
792    paths: &[String],
793) -> Result<Vec<Option<Vec<u8>>>, &'static str> {
794    if paths.is_empty() {
795        return Ok(Vec::new());
796    }
797    let mut out: Vec<Option<Vec<u8>>> = Vec::with_capacity(paths.len());
798
799    let remote_ip = crate::kernel::net::dns::dns_query_a_via_udp(host)?;
800    let local_port = crate::kernel::net::tcp::tcp_connect_real(remote_ip, 443)?;
801
802    let mut rng = OsRng::new();
803    let mut stream = TcpIoWrapper::new_with_retry_write(local_port);
804    let mut read_buf = alloc::vec![0u8; 16384];
805    let mut write_buf = alloc::vec![0u8; 16384];
806    let config = TlsConfig::new().with_server_name(host);
807    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
808        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
809    let mut provider =
810        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
811
812    let _ph = crate::os_lib::web_engine::perf::start();
813    let open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
814    crate::os_lib::web_engine::perf::add(
815        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
816        _ph,
817    );
818    if open.is_err() {
819        let _ = crate::kernel::net::tcp_close_real(local_port);
820        return Err("TLS handshake failed");
821    }
822
823    let mut broken = false;
824    for path in paths.iter() {
825        if broken {
826            out.push(None);
827            continue;
828        }
829        // 最後の 1 本だけ close を伝える(サーバ側の後始末を促す)。
830        let req = alloc::format!(
831            concat!(
832                "GET {} HTTP/1.1
833",
834                "Host: {}
835",
836                "User-Agent: Mozilla/5.0 (AtmOS Browser)
837",
838                "Accept: */*
839",
840                "Accept-Encoding: identity
841",
842                // 接続を閉じないよう明示する。閉じられると次のパスで
843                // 張り直しになり、この関数の目的(ハンドシェイク削減)が消える。
844                "Connection: keep-alive
845
846"
847            ),
848            path,
849            host
850        );
851        if embedded_io::Write::write_all(&mut connection, req.as_bytes()).is_err() {
852            broken = true;
853            out.push(None);
854            continue;
855        }
856        embedded_io::Write::flush(&mut connection).ok();
857        crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
858
859        // ヘッダを読み切り、`Content-Length` の分だけ本文を読む。
860        let mut buf: Vec<u8> = Vec::new();
861        let mut chunk = [0u8; 1024];
862        let mut header_end: Option<usize> = None;
863        let mut want: Option<usize> = None;
864        loop {
865            if header_end.is_none() {
866                if let Some(i) = crate::kernel::http_body::body_start(&buf) {
867                    header_end = Some(i);
868                    want = crate::kernel::http_body::content_length(&buf);
869                    if want.is_none() {
870                        // 長さが分からない応答は切れ目を決められない。
871                        broken = true;
872                        break;
873                    }
874                }
875            }
876            if let (Some(h), Some(w)) = (header_end, want) {
877                if buf.len() >= h + w {
878                    break;
879                }
880            }
881            match embedded_io::Read::read(&mut connection, &mut chunk) {
882                Ok(0) => {
883                    broken = true;
884                    break;
885                }
886                Ok(n) => buf.extend_from_slice(&chunk[..n]),
887                Err(_) => {
888                    broken = true;
889                    break;
890                }
891            }
892        }
893        match (header_end, want) {
894            (Some(h), Some(w)) if buf.len() >= h + w => {
895                // 【2026-08-05 バグ修正】`buf[..h + w]` は**ヘッダを含んでいた**。
896                //
897                // `h` は本文の開始位置なので、本文は `buf[h..h + w]`。
898                // ヘッダ込みで返していたため、キャッシュへ
899                // `HTTP/1.1 200 OK...` ごと書き込まれ、フォント登録が
900                // `[FONT] 未知のフォント形式 head=[48,54,54,50]` で失敗していた
901                // (`{:02x?}` 表示なので "HTTP" の 4 文字)。
902                //
903                // 実害: **Font Awesome が 1 つも登録されず**、
904                // ページ上のアイコンがすべて空の四角になっていた。
905                // まとめ取得(keep-alive)を入れたときに混入した回帰。
906                out.push(Some(crate::kernel::http_body::body_range(&buf)
907                        .map(|(a, b)| buf[a..b].to_vec())
908                        .unwrap_or_default()));
909                stats::bump(&stats::OK);
910            }
911            _ => {
912                out.push(None);
913                broken = true;
914            }
915        }
916    }
917
918    let _ = crate::kernel::net::tcp_close_real(local_port);
919    stats::report("after-resp");
920    Ok(out)
921}
922
923// 【2026-08-05】`find_header_end` は `kernel::http_body::body_start` と
924// **同じ計算を二重に持っていた**。片方だけ直す事故のもとなので一本化した。
925// (実際、本文の切り出しを誤って Font Awesome が全滅した直後に気づいた)
926
927// 【2026-08-05】`parse_content_length` も `kernel::http_body::content_length`
928// と重複していた。文字列添字で値を切り出しており、
929// UTF-8 の途中で切ると panic しうる書き方だった(clippy 指摘と同種)。
930// バイト列で処理する `http_body` 版へ一本化する。
931
932/// `Content-Length`が判明していて、かつ受信済みボディがそれより短い
933/// (=TLS読み取り途中で打ち切られた)場合に`true`を返す。`chunked`応答は
934/// 対象外(`Content-Length`ヘッダを通常持たないため判定不能)。
935fn response_looks_truncated(raw: &[u8]) -> bool {
936    // 判定は `kernel::http_body::looks_truncated`(単体試験あり)へ委譲。
937    crate::kernel::http_body::looks_truncated(raw)
938}
939
940pub fn https_get_binary(host: &str, path: &str) -> Result<Vec<u8>, &'static str> {
941    let _pg = crate::os_lib::web_engine::perf::start();
942    let r = https_get_binary_measured(host, path);
943    crate::os_lib::web_engine::perf::add(
944        crate::os_lib::web_engine::perf::Slot::HttpsGet,
945        _pg,
946    );
947    r
948}
949
950fn https_get_binary_measured(host: &str, path: &str) -> Result<Vec<u8>, &'static str> {
951    const MAX_ATTEMPTS: u32 = 3;
952    let ordinal = crate::kernel::net::next_https_attempt_ordinal();
953    crate::warn!("[NET][DIAG] https_get_binary ordinal={} host={} path={}", ordinal, host, path);
954    // 【2026-07-25】以前は`https_get_binary_attempt`内でDNS解決を毎回
955    // 行っていたため、同一ホストへの再試行のたびに無駄なDNS往復が発生し、
956    // DNS応答タイムアウト自体が再試行を無駄に消費する原因にもなっていた。
957    // 同一ホストの名前解決結果は数秒の再試行間隔では変わらないため、
958    // ここで1回だけ解決してから各TCP/TLS試行へ使い回す。
959    let remote_ip = crate::kernel::net::dns_query_a_via_udp(host).inspect_err(|e| {
960        crate::warn!("[NET][DIAG] https_get_binary ordinal={} DNS FAILED err={}", ordinal, e);
961        stats::bump(&stats::DNS_FAIL);
962        stats::report("after-dns-fail");
963    })?;
964    crate::warn!("[NET][DIAG] https_get_binary ordinal={} host={} resolved={}.{}.{}.{}", ordinal, host, remote_ip[0], remote_ip[1], remote_ip[2], remote_ip[3]);
965    let mut best: Option<Vec<u8>> = None;
966    for attempt in 1..=MAX_ATTEMPTS {
967        match https_get_binary_attempt(remote_ip, host, path) {
968            Ok((raw, complete)) => {
969                if complete {
970                    crate::warn!(
971                        "[NET][DIAG] https_get_binary ordinal={} SUCCESS attempt={} body_bytes={}",
972                        ordinal, attempt, raw.len()
973                    );
974                    return Ok(crate::kernel::net::extract_http_body_binary(&raw));
975                }
976                crate::warn!(
977                    "[NET][DIAG] https_get_binary ordinal={}: truncated response on attempt {}/{} ({} bytes), retrying",
978                    ordinal, attempt, MAX_ATTEMPTS, raw.len()
979                );
980                if best.as_ref().is_none_or(|b: &Vec<u8>| raw.len() > b.len()) {
981                    best = Some(raw);
982                }
983            }
984            Err(e) => {
985                crate::warn!(
986                    "[NET][DIAG] https_get_binary ordinal={} attempt {}/{} FAILED err={}",
987                    ordinal, attempt, MAX_ATTEMPTS, e
988                );
989                if attempt == MAX_ATTEMPTS {
990                    return Err(e);
991                }
992            }
993        }
994    }
995    // 全リトライで打ち切りが続いた場合は、最も長く取得できたものを返す
996    // (何も返さないより、部分的にでもCSS/フォントが使える方が良い)。
997    match best {
998        Some(raw) => Ok(crate::kernel::net::extract_http_body_binary(&raw)),
999        None => Err("HTTPS request failed"),
1000    }
1001}
1002
1003/// 1回分のHTTPS GET試行。戻り値は(生レスポンス, Content-Lengthと矛盾なく
1004/// 完全に受信できたか)。呼び出し元`https_get_binary`がこれを複数回試行し、
1005/// 打ち切り(TLSタイムアウト等による部分受信)を検出したら再試行する。
1006/// `remote_ip`は呼び出し元で1回だけ解決済みのものを使い回す
1007/// (DNS解決自体を毎試行で繰り返さないため)。
1008fn https_get_binary_attempt(
1009    remote_ip: [u8; 4],
1010    host: &str,
1011    path: &str,
1012) -> Result<(Vec<u8>, bool), &'static str> {
1013    crate::debug!(
1014        "[NET][DIAG] https_get_binary host={} resolved_ip={}.{}.{}.{}",
1015        host,
1016        remote_ip[0],
1017        remote_ip[1],
1018        remote_ip[2],
1019        remote_ip[3]
1020    );
1021    let local_port = crate::kernel::net::tcp_connect_real(remote_ip, 443)?;
1022    let handshake_start = crate::kernel::timer::get_ticks();
1023    crate::debug!("[NET][DIAG] tcp_connect_real returned local_port={}", local_port);
1024
1025    // 【2026-07-29 検証したが結論保留】`retry_write` は `https_get_binary` でのみ
1026    // 有効で、打ち切り(truncated)が起きるのもこの経路、という相関がある。
1027    // 積極的な再送が TLS ストリームを乱している可能性を疑い一時的に無効化して
1028    // 測定したが、**2 回の実行とも試行数が 1〜2 件しか得られず判断できなかった**
1029    // (ページが早期に失敗し後続の取得まで到達しないため)。
1030    // 証拠不十分のまま既存の対策を外すのは危険なので元に戻した。
1031    // 再検証するなら、1 回のブートで同じ URL を N 回取得する仕組みを先に用意すること。
1032    let mut stream = TcpIoWrapper::new_with_retry_write(local_port);
1033    let mut rng = OsRng::new();
1034
1035    // 【2026-07-24診断】静的バッファに変更して試したがCSS取得失敗は解消せず
1036    // (ヒープ確保・再利用は原因ではないと判明したため、ヒープ確保に戻す)。
1037    let mut read_buf = alloc::vec![0u8; 18432]; // TLS最大レコード(16640)を収容
1038    let mut write_buf = alloc::vec![0u8; 16384];
1039
1040    let config = TlsConfig::new().with_server_name(host);
1041
1042    let mut connection: TlsConnection<'_, &mut TcpIoWrapper, embedded_tls::Aes128GcmSha256> =
1043        TlsConnection::new(&mut stream, &mut read_buf[..], &mut write_buf[..]);
1044
1045    let mut provider =
1046        embedded_tls::UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(&mut rng);
1047    let _ph = crate::os_lib::web_engine::perf::start();
1048    let _open = connection.open(crate::kernel::tls::TlsContext::new(&config, &mut provider));
1049    crate::os_lib::web_engine::perf::add(
1050        crate::os_lib::web_engine::perf::Slot::TlsHandshake,
1051        _ph,
1052    );
1053    if let Err(e) = _open {
1054        let elapsed = crate::kernel::timer::get_ticks().wrapping_sub(handshake_start);
1055        crate::warn!("[NET] TLS handshake error: {:?} (elapsed_ticks={})", e, elapsed);
1056        let _ = crate::kernel::net::tcp_close_real(local_port);
1057        return Err("TLS handshake failed");
1058    }
1059
1060    let request = alloc::format!("GET {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: Mozilla/5.0 (AtmOS Browser)\r\nAccept: */*\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n", path, host);
1061    if embedded_io::Write::write_all(&mut connection, request.as_bytes()).is_err() {
1062        crate::kernel::net::tcp_close_real(local_port)?;
1063        return Err("HTTPS request failed");
1064    }
1065
1066    embedded_io::Write::flush(&mut connection).ok();
1067    // 【2026-08-05】無条件 500ms から「届いたら即抜ける」へ(上限は同じ)。
1068    // 詳細は `net::tcp::wait_for_first_byte`。
1069    crate::kernel::net::tcp::wait_for_first_byte(local_port, 50);
1070
1071    let mut body_bytes = Vec::new();
1072    let mut chunk = [0u8; 1024];
1073    loop {
1074        match embedded_io::Read::read(&mut connection, &mut chunk) {
1075            Ok(0) => break,
1076            Ok(n) => {
1077                body_bytes.extend_from_slice(&chunk[..n]);
1078                // 【2026-07-30】取得サイズの上限。0 なら無制限。
1079                // 画像のように巨大なリソースは、`Content-Length` が分かった
1080                // 時点で本文を読み切らずに打ち切る。
1081                // 実測で 3.7MB の背景写真のダウンロードが 120 秒を使い切り、
1082                // 描画へ進めなくなっていた(`spec/webfont_loading.md`)。
1083                let cap = FETCH_BYTE_CAP.load(core::sync::atomic::Ordering::Relaxed);
1084                if cap > 0 {
1085                    let declared = crate::kernel::http_body::content_length(&body_bytes);
1086                    if declared.is_some_and(|c| c > cap) || body_bytes.len() > cap {
1087                        crate::warn!(
1088                            "[TLS] host={} path={} 取得を打ち切り: 上限 {} バイト超過 (declared={:?} got={})",
1089                            host, path, cap, declared, body_bytes.len()
1090                        );
1091                        let _ = crate::kernel::net::tcp_close_real(local_port);
1092                        return Err("fetch exceeds size cap");
1093                    }
1094                }
1095            }
1096            Err(e) => {
1097                crate::warn!("[NET][DIAG] TLS read err: {:?} body_bytes_so_far={}", e, body_bytes.len());
1098                break;
1099            }
1100        }
1101    }
1102    crate::warn!(
1103        "[NET][DIAG] read loop done, body_bytes.len()={} preview={:?}",
1104        body_bytes.len(),
1105        String::from_utf8_lossy(&body_bytes[..body_bytes.len().min(80)])
1106    );
1107
1108    crate::kernel::net::tcp_close_real(local_port)?;
1109
1110    let complete = !response_looks_truncated(&body_bytes);
1111    // 【2026-07-29診断】実サイトで応答が実行ごとに違う長さで途中打ち切りになる件の
1112    // 切り分け。Content-Length と実読バイト数、打ち切り判定の結果を突き合わせる。
1113    if complete {
1114        stats::bump(&stats::OK);
1115    } else {
1116        stats::bump(&stats::TRUNCATED);
1117    }
1118    stats::report("after-resp");
1119    Ok((body_bytes, complete))
1120}