Summary
On main at b91c909, a csrw satp whose MODE field the core does not support correctly leaves MODE unchanged, but still writes the new ASID and PPN. The Privileged Architecture version this core declares requires the write to have no effect at all.
The result is that satp lands in a state no single legal write could produce — the old MODE beside a new PPN — and the address translation root is silently replaced while software is being told the write was rejected.
What is measured below is the satp readback, which is the architectural interface the specification sentence is about. That the walk then proceeds from the new root is argued from the source and not observed: the walker's root-PPN port is not architecturally visible, so a full-core run cannot show it. Both halves are set out under What the RTL does.
The rule
RISC-V Privileged Architecture 1.10, src/supervisor.tex:
Implementations are not required to support all MODE settings, and if satp is written with an unsupported MODE, the entire write has no effect; no fields in satp are modified.
The C910 manual §1.5 declares Privileged Architecture 1.10 and names exactly one post-1.10 adoption (mcountinhibit), so this rule is in the declared target. The sentence is also byte-identical in ratified 1.11 and 1.12, and it entered the manual more than four years before this core's first public commit, so there is no contemporaneous version in which it is absent or weaker:
riscv-isa-manual 26f0d7a5567a8c1a1b9faa9ce4d9a70e05b2f7a6 // added 2017-04-11
What the RTL does
C910_RTL_FACTORY/gen_rtl/mmu/rtl/ct_mmu_regs.v. Two write blocks with different guards:
:634 else if(satp_write_en && cp0_mmu_wdata[62:60] == 3'b0) // MODE: guarded correctly
:635 satp_mode[3:0] <= {cp0_mmu_wdata[63],3'b0};
:645 else if(satp_write_en) // ASID and PPN: no MODE condition
:647 satp_asid[ASID_WIDTH-1:0] <= cp0_mmu_wdata[59:44];
:648 satp_ppn[PPN_WIDTH-1:0] <= cp0_mmu_wdata[PPN_WIDTH-1:0];
The MODE guard admits only wdata[62:60] == 3'b0, i.e. Bare and Sv39. That is correct for an Sv39-only core — this is a narrow finding about the second block, not a claim that satp is broken.
Two lines out of a priority chain cannot show that those guards are the only conditions under which the fields are written, so here are both blocks in full, and the readback, from ct_mmu_regs.v:
The complete always blocks and the readback
:630 always @(posedge mmu_regs_clk or negedge cpurst_b)
:631 begin
:632 if(!cpurst_b)
:633 satp_mode[3:0] <= {4{1'b0}};
:634 else if(satp_write_en && cp0_mmu_wdata[62:60] == 3'b0)
:635 satp_mode[3:0] <= {cp0_mmu_wdata[63],3'b0};
:636 end
:638 always @(posedge mmu_regs_clk or negedge cpurst_b)
:639 begin
:640 if(!cpurst_b)
:641 begin
:642 satp_asid[ASID_WIDTH-1:0] <= {ASID_WIDTH{1'b0}};
:643 satp_ppn[PPN_WIDTH-1:0] <= {PPN_WIDTH{1'b0}};
:644 end
:645 else if(satp_write_en)
:646 begin
:647 satp_asid[ASID_WIDTH-1:0] <= cp0_mmu_wdata[59:44];
:648 satp_ppn[PPN_WIDTH-1:0] <= cp0_mmu_wdata[PPN_WIDTH-1:0];
:649 end
:650 end
:652 assign satp_data[63:0] = {satp_mode[3:0],satp_asid[ASID_WIDTH-1:0],
:653 16'b0,satp_ppn[PPN_WIDTH-1:0]};
Every line from :630 to :653 is quoted above; the only two omitted are the blank lines :637 and :651. That matters, because an excerpt cannot establish what the next sentence claims: two arms each, reset and write, and nothing else. There is no third arm to mask either one, no other write path to satp_asid or satp_ppn, and the readback assembles all three fields with satp_ppn among them — which is why the table below is a measurement of the architectural register, and why the one-line change is sufficient rather than merely necessary.
The write enable both guards share is one line in the same file, and it carries no field condition of its own:
:284 assign satp_write_en = cp0_mmu_satp_sel;
The name is sel and not wen, and it is driven from another module, so two things are worth closing rather than assuming: that a csrr satp does not assert it, and that the signal reaching ct_mmu_regs.v is the one ct_cp0_regs.v drives. Neither holds by name. Both are below, and the read question closes on a conjunction: satp_write_en is cp0_mmu_satp_sel, which is iui_regs_sel && satp_local_en, which needs inst_csr_ex2, which is only ever loaded with inst_csr_ex1 && !iui_inst_ro. Every step is an AND, so iui_inst_ro alone forces satp_write_en low, whatever the other terms do — and iui_inst_ro is exactly the architectural no-write set, CSRRS and CSRRC and their immediate forms with a zero source, which is what csrr assembles to.
The read/write chain and the module-to-module connection, quoted
In ct_cp0_regs.v, the address decode carries no read-or-write information, so the whole question is iui_regs_sel:
:1433 assign satp_local_en = iui_regs_addr[11:0] == SATP;
:4246 assign cp0_mmu_satp_sel = iui_regs_sel && satp_local_en;
iui_regs_sel is produced in ct_cp0_iui.v. iui_uimm is the instruction's rs1 field, so the one zero-source test covers the register forms and the immediate forms alike:
:797 assign iui_uimm[63:0] = {59'b0, iui_ex1_opcode[19:15]};
:801 assign iui_inst_ro = (iui_inst_csrrs || iui_inst_csrrc
:802 || iui_inst_csrrsi || iui_inst_csrrci)
:803 && iui_uimm[4:0] == 5'b0;
:1415 always @ (posedge cpuclk or negedge cpurst_b)
:1416 begin
:1417 if(!cpurst_b)
:1418 inst_csr_ex2 <= 1'b0;
:1419 else if(cp0_ex1_select)
:1420 inst_csr_ex2 <= (inst_csr_ex1 && !iui_inst_ro);
:1421 else
:1422 inst_csr_ex2 <= 1'b0;
:1423 end
:1425 assign iui_regs_sel = inst_csr_ex2 && cp0_ex2_select;
The block is quoted whole because the conjunction depends on it: only ever loaded with is an exhaustiveness claim, and one arm that set inst_csr_ex2 unconditionally would break the chain. The other two arms both clear it.
cp0_ex2_select is left as a conjunct here, unexpanded. It resolves in the same file to iui_ex2_commit, which is RTU's commit of this instruction, so opening it would answer a different question — whether a write can happen on a flushed instruction. A conjunct is enough to show that a read cannot write, and one that is not needed to answer the question is not opened, so no line of it is quoted here.
The name cp0_mmu_satp_sel occurs in six files, and a shared name is not a shared net. The port connections are, from the driver in ct_cp0_regs.v:
:402 output cp0_mmu_satp_sel;
up through ct_cp0_top.v, on its x_ct_cp0_regs instance:
:930 .cp0_mmu_satp_sel (cp0_mmu_satp_sel ),
and ct_core.v, on its x_ct_cp0_top instance:
:4625 .cp0_mmu_satp_sel (cp0_mmu_satp_sel ),
The two subtrees meet in ct_top.v, at the x_ct_core instance and the x_ct_mmu_top instance:
:917 .cp0_mmu_satp_sel (cp0_mmu_satp_sel ),
:1331 .cp0_mmu_satp_sel (cp0_mmu_satp_sel ),
then down through ct_mmu_top.v, on its x_ct_mmu_regs instance:
:700 .cp0_mmu_satp_sel (cp0_mmu_satp_sel ),
to the input in ct_mmu_regs.v that :284 consumes:
:76 input cp0_mmu_satp_sel;
Case A in the reproduction comment is consistent with the read half and does not establish it: csrr is CSRRS with a zero source, so a read that erroneously wrote would write back the value already there, and no readback separates that from no write at all. The conjunction above, and not the run, is what carries it.
Two more facts about the same file support the table below rather than the finding itself: the readback is decodable field by field, and every satp write flushes the micro-TLB, including the rejected ones — so stale translations do not survive to mask the seam.
Decoding the readback, and the micro-TLB flush
The readback in ct_mmu_regs.v is decodable, so the table below can be checked rather than believed. :230 and :233 give the widths — PPN_WIDTH = 40-12 and ASID_WIDTH = 16 — so :652 reads {MODE[3:0], ASID[15:0], 16'b0, PPN[27:0]}. Take the setup value 0x801110000000aaaa: MODE 0x8 (Sv39), ASID 0x0111, bits 43:28 zero, PPN 0xaaaa. That is the setup written field for field.
:230 parameter PPN_WIDTH = 40-12; // PPN
:233 parameter ASID_WIDTH = 16; // Flags
One thing that decoding makes visible and is not part of this report: satp_ppn is 28 bits, so a write's bits 43:28 are dropped and read back as the 16'b0 field. That is the architecture's own PPN width for Sv39 on a 40-bit physical address, not a second partial update.
The same file also flushes the micro-TLB on every satp write, including the rejected ones:
:238 assign regs_utlb_clr = satp_write_en;
So the old translations do not survive to mask the seam: the walker is forced to re-walk from the root PPN that was just installed.
Reproduction
A full reproduction — tool version strings, the exact commands, every test program and the whole of each run's output, each with a sha256 — is posted as the first comment on this issue.
Every transcript excerpt in this report is reformatted for reading — 0x added, short annotations appended, cases grouped by what they demonstrate rather than kept in the order the program prints them, and lines the paragraph is not about left out. The reproduction comment carries each run's output verbatim and in full, so a line here can always be found there.
Whole SoC from reset, under the vendor's own smart_run Verilator flow with logical/tb/tb_verilator.v unmodified, rv64imafdc, Verilator 5.048. The test is ordinary M-mode csrw / csrr.
openc910 at b91c90914c19f114d35c8f6b73408eb241ed847c
Each case re-runs its own setup write and then its probe write, so the two columns of every transcript line are that case's own satp after setup and after probe. Six of the seven use the Sv39 setup 0x801110000000aaaa (ASID 0x111, PPN 0xaaaa); case F sets up from Bare instead, which is why its rows differ in the MODE nibble.
That matters for the last column. "Required by 1.10" is not an independent assertion: the rule is that no field is modified, so the required value IS the same row's setup readback, which the run measured.
| case |
satp after setup, observed |
probe write |
satp after probe, observed |
required by 1.10 |
| C |
0x801110000000aaaa |
0x902220000000bbbb (MODE=9, Sv48) |
0x802220000000bbbb |
0x801110000000aaaa |
| D |
0x801110000000aaaa |
0x103330000000cccc (MODE=1) |
0x803330000000cccc |
0x801110000000aaaa |
| E |
0x801110000000aaaa |
0xa04440000000dddd (MODE=10, Sv57) |
0x804440000000dddd |
0x801110000000aaaa |
| F |
0x001110000000aaaa |
0x905550000000eeee (MODE=9, from Bare) |
0x005550000000eeee |
0x001110000000aaaa |
Controls in the same run, which behave correctly — each is a legal write, and each lands in full:
| case |
satp after setup, observed |
probe write |
satp after probe, observed |
| B |
0x801110000000aaaa |
0x802220000000bbbb (MODE=8, supported) |
0x802220000000bbbb |
| G |
0x801110000000aaaa |
0x006660000000ffff (MODE=0, Bare) |
0x006660000000ffff |
The reproduction comment carries a seventh case. A makes no probe write at all and reports the setup value twice; it is the bring-up control that says the harness is driving the CSR write path before any of the six above is read as evidence.
No trap is taken in any case, and that part is correct: satp.MODE is WARL, and the specification requires an unsupported MODE to be ignored, not refused. It is worth stating only because it is what leaves software with nothing to notice — the write instruction completes, the MODE readback is unchanged, and there is no signal of any kind that the other two fields moved.
Why routine CSR testing does not find this
The correct behaviour depends on the register's current value, because "no fields are modified" means the old ASID and PPN must survive. A single-write check that writes an unsupported MODE and inspects the MODE readback passes: MODE really is rejected. Single-write WARL sweeps, random CSR generators and CSR-access suites structurally cannot construct the precondition.
Whether this is isolated
Within the scope swept, the neighbours of this line are right: this is one line and not a general pattern in the file.
What was swept, what was conformant, and the bound
We swept every CSR field written from the write-data bus in ct_cp0_regs.v, plus the satp path, looking for this specific shape: legalization that depends on the register's current contents and not on the written value alone. Six neighbours were checked and are conformant, including the two nearest misreads — mtvec.MODE and stvec.MODE, where the store is raw but the readback and the vector the fetch unit receives both drop the illegal bit, so readback and behaviour agree; and the mideleg-dependent masking of sie and sip, which is this same current-value shape on the register where it matters most.
We are not claiming that sweep was exhaustive, and it is worth being exact about why: it asked one question, and a field that diverges for some other reason would not answer it. The bound matters as much as the result: readback paths, trap-entry side effects and the custom C910 extension CSRs were not covered, and the sweep is a source-level reading — nothing in it was elaborated or simulated. What it does support is the narrow statement a maintainer needs to prioritise this: within that scope, the neighbours of this line are right, so what follows is one line and not a general pattern in the file.
Practical scenario
Supervisor software probes for Sv48 the conventional way a WARL field's supported values are discovered — and the way the same sentence implies, since it is the rejected write's lack of effect that makes the probe safe: write satp with MODE=9 and the Sv48 root, read back, see whether MODE stuck. Per the specification, nothing changed. On this core the software reads MODE=8, correctly concludes Sv48 is unsupported — and has no indication that its address-space root was replaced by the Sv48 root it just probed with, with the micro-TLB flushed so the walker must use it.
Suggested change
Apply the guard the module already computes eleven lines above:
diff --git a/C910_RTL_FACTORY/gen_rtl/mmu/rtl/ct_mmu_regs.v b/C910_RTL_FACTORY/gen_rtl/mmu/rtl/ct_mmu_regs.v
index f9520ee..177a8d3 100644
--- a/C910_RTL_FACTORY/gen_rtl/mmu/rtl/ct_mmu_regs.v
+++ b/C910_RTL_FACTORY/gen_rtl/mmu/rtl/ct_mmu_regs.v
@@ -642,7 +642,7 @@ begin
satp_asid[ASID_WIDTH-1:0] <= {ASID_WIDTH{1'b0}};
satp_ppn[PPN_WIDTH-1:0] <= {PPN_WIDTH{1'b0}};
end
- else if(satp_write_en)
+ else if(satp_write_en && cp0_mmu_wdata[62:60] == 3'b0)
begin
satp_asid[ASID_WIDTH-1:0] <= cp0_mmu_wdata[59:44];
satp_ppn[PPN_WIDTH-1:0] <= cp0_mmu_wdata[PPN_WIDTH-1:0];
That block is a file git apply will take from the repository root, checked against b91c909 as it stands rather than being retyped here. sha256 7fad1b49639cff090fa189646ddf0ef0a44123021d4a4f81c655343eb6c877b7 is over exactly those bytes, so sha256sum on what you paste says whether you have the same file we applied. No line of it carries trailing whitespace, so an ordinary copy out of this page is enough.
The one hunk touches only the satp_asid and satp_ppn write block in ct_mmu_regs.v; it does not overlap any other region of that file.
Nothing new is computed and no signal is introduced. We re-elaborated the entire SoC from this patched source and re-ran the same seven cases on the same simulator; the reproduction comment carries every line of both runs under The whole output, with the suggested change applied. The four rejected writes become no-ops and both controls are byte-identical to the run above:
C 0x801110000000aaaa 0x801110000000aaaa // the MODE=9 probe write no longer moves ASID or PPN
D 0x801110000000aaaa 0x801110000000aaaa // MODE=1
E 0x801110000000aaaa 0x801110000000aaaa // MODE=10
F 0x001110000000aaaa 0x001110000000aaaa // MODE=9 written from Bare
B 0x801110000000aaaa 0x802220000000bbbb // control: the supported-MODE write still lands in full
G 0x801110000000aaaa 0x006660000000ffff // control: the Bare write still lands in full
TRAPS 0x0000000000000000 // nothing is achieved by trapping a write 1.10 requires to be ignored
We are reporting a divergence, not prescribing a fix — whether this is the right change under your constraints is your call.
The change does not touch regs_utlb_clr at :238, and we are not asking for that line to change. Once the rejected write is a no-op, flushing the micro-TLB on it costs a re-walk and nothing more; it is architecturally harmless. It is quoted above because it removes an alternative explanation for the readback, not because it is part of the finding.
Summary
On
mainatb91c909, acsrw satpwhose MODE field the core does not support correctly leaves MODE unchanged, but still writes the new ASID and PPN. The Privileged Architecture version this core declares requires the write to have no effect at all.The result is that
satplands in a state no single legal write could produce — the old MODE beside a new PPN — and the address translation root is silently replaced while software is being told the write was rejected.What is measured below is the
satpreadback, which is the architectural interface the specification sentence is about. That the walk then proceeds from the new root is argued from the source and not observed: the walker's root-PPN port is not architecturally visible, so a full-core run cannot show it. Both halves are set out under What the RTL does.The rule
RISC-V Privileged Architecture 1.10,
src/supervisor.tex:The C910 manual §1.5 declares Privileged Architecture 1.10 and names exactly one post-1.10 adoption (
mcountinhibit), so this rule is in the declared target. The sentence is also byte-identical in ratified 1.11 and 1.12, and it entered the manual more than four years before this core's first public commit, so there is no contemporaneous version in which it is absent or weaker:What the RTL does
C910_RTL_FACTORY/gen_rtl/mmu/rtl/ct_mmu_regs.v. Two write blocks with different guards:The MODE guard admits only
wdata[62:60] == 3'b0, i.e. Bare and Sv39. That is correct for an Sv39-only core — this is a narrow finding about the second block, not a claim thatsatpis broken.Two lines out of a priority chain cannot show that those guards are the only conditions under which the fields are written, so here are both blocks in full, and the readback, from
ct_mmu_regs.v:The complete always blocks and the readback
Every line from
:630to:653is quoted above; the only two omitted are the blank lines:637and:651. That matters, because an excerpt cannot establish what the next sentence claims: two arms each, reset and write, and nothing else. There is no third arm to mask either one, no other write path tosatp_asidorsatp_ppn, and the readback assembles all three fields withsatp_ppnamong them — which is why the table below is a measurement of the architectural register, and why the one-line change is sufficient rather than merely necessary.The write enable both guards share is one line in the same file, and it carries no field condition of its own:
The name is
seland notwen, and it is driven from another module, so two things are worth closing rather than assuming: that acsrr satpdoes not assert it, and that the signal reachingct_mmu_regs.vis the onect_cp0_regs.vdrives. Neither holds by name. Both are below, and the read question closes on a conjunction:satp_write_eniscp0_mmu_satp_sel, which isiui_regs_sel && satp_local_en, which needsinst_csr_ex2, which is only ever loaded withinst_csr_ex1 && !iui_inst_ro. Every step is an AND, soiui_inst_roalone forcessatp_write_enlow, whatever the other terms do — andiui_inst_rois exactly the architectural no-write set,CSRRSandCSRRCand their immediate forms with a zero source, which is whatcsrrassembles to.The read/write chain and the module-to-module connection, quoted
In
ct_cp0_regs.v, the address decode carries no read-or-write information, so the whole question isiui_regs_sel:iui_regs_selis produced inct_cp0_iui.v.iui_uimmis the instruction'srs1field, so the one zero-source test covers the register forms and the immediate forms alike:The block is quoted whole because the conjunction depends on it:
only ever loaded withis an exhaustiveness claim, and one arm that setinst_csr_ex2unconditionally would break the chain. The other two arms both clear it.cp0_ex2_selectis left as a conjunct here, unexpanded. It resolves in the same file toiui_ex2_commit, which is RTU's commit of this instruction, so opening it would answer a different question — whether a write can happen on a flushed instruction. A conjunct is enough to show that a read cannot write, and one that is not needed to answer the question is not opened, so no line of it is quoted here.The name
cp0_mmu_satp_seloccurs in six files, and a shared name is not a shared net. The port connections are, from the driver inct_cp0_regs.v:up through
ct_cp0_top.v, on itsx_ct_cp0_regsinstance::930 .cp0_mmu_satp_sel (cp0_mmu_satp_sel ),and
ct_core.v, on itsx_ct_cp0_topinstance::4625 .cp0_mmu_satp_sel (cp0_mmu_satp_sel ),The two subtrees meet in
ct_top.v, at thex_ct_coreinstance and thex_ct_mmu_topinstance:then down through
ct_mmu_top.v, on itsx_ct_mmu_regsinstance::700 .cp0_mmu_satp_sel (cp0_mmu_satp_sel ),to the input in
ct_mmu_regs.vthat:284consumes:Case
Ain the reproduction comment is consistent with the read half and does not establish it:csrrisCSRRSwith a zero source, so a read that erroneously wrote would write back the value already there, and no readback separates that from no write at all. The conjunction above, and not the run, is what carries it.Two more facts about the same file support the table below rather than the finding itself: the readback is decodable field by field, and every
satpwrite flushes the micro-TLB, including the rejected ones — so stale translations do not survive to mask the seam.Decoding the readback, and the micro-TLB flush
The readback in
ct_mmu_regs.vis decodable, so the table below can be checked rather than believed.:230and:233give the widths —PPN_WIDTH = 40-12andASID_WIDTH = 16— so:652reads{MODE[3:0], ASID[15:0], 16'b0, PPN[27:0]}. Take the setup value0x801110000000aaaa: MODE0x8(Sv39), ASID0x0111, bits 43:28 zero, PPN0xaaaa. That is the setup written field for field.One thing that decoding makes visible and is not part of this report:
satp_ppnis 28 bits, so a write's bits 43:28 are dropped and read back as the16'b0field. That is the architecture's own PPN width for Sv39 on a 40-bit physical address, not a second partial update.The same file also flushes the micro-TLB on every
satpwrite, including the rejected ones:So the old translations do not survive to mask the seam: the walker is forced to re-walk from the root PPN that was just installed.
Reproduction
A full reproduction — tool version strings, the exact commands, every test program and the whole of each run's output, each with a
sha256— is posted as the first comment on this issue.Every transcript excerpt in this report is reformatted for reading —
0xadded, short annotations appended, cases grouped by what they demonstrate rather than kept in the order the program prints them, and lines the paragraph is not about left out. The reproduction comment carries each run's output verbatim and in full, so a line here can always be found there.Whole SoC from reset, under the vendor's own
smart_runVerilator flow withlogical/tb/tb_verilator.vunmodified,rv64imafdc, Verilator 5.048. The test is ordinary M-modecsrw/csrr.Each case re-runs its own setup write and then its probe write, so the two columns of every transcript line are that case's own
satpafter setup and after probe. Six of the seven use the Sv39 setup0x801110000000aaaa(ASID0x111, PPN0xaaaa); case F sets up from Bare instead, which is why its rows differ in the MODE nibble.That matters for the last column. "Required by 1.10" is not an independent assertion: the rule is that no field is modified, so the required value IS the same row's setup readback, which the run measured.
satpafter setup, observedsatpafter probe, observed0x801110000000aaaa0x902220000000bbbb(MODE=9, Sv48)0x802220000000bbbb0x801110000000aaaa0x801110000000aaaa0x103330000000cccc(MODE=1)0x803330000000cccc0x801110000000aaaa0x801110000000aaaa0xa04440000000dddd(MODE=10, Sv57)0x804440000000dddd0x801110000000aaaa0x001110000000aaaa0x905550000000eeee(MODE=9, from Bare)0x005550000000eeee0x001110000000aaaaControls in the same run, which behave correctly — each is a legal write, and each lands in full:
satpafter setup, observedsatpafter probe, observed0x801110000000aaaa0x802220000000bbbb(MODE=8, supported)0x802220000000bbbb0x801110000000aaaa0x006660000000ffff(MODE=0, Bare)0x006660000000ffffThe reproduction comment carries a seventh case.
Amakes no probe write at all and reports the setup value twice; it is the bring-up control that says the harness is driving the CSR write path before any of the six above is read as evidence.No trap is taken in any case, and that part is correct:
satp.MODEis WARL, and the specification requires an unsupported MODE to be ignored, not refused. It is worth stating only because it is what leaves software with nothing to notice — the write instruction completes, the MODE readback is unchanged, and there is no signal of any kind that the other two fields moved.Why routine CSR testing does not find this
The correct behaviour depends on the register's current value, because "no fields are modified" means the old ASID and PPN must survive. A single-write check that writes an unsupported MODE and inspects the MODE readback passes: MODE really is rejected. Single-write WARL sweeps, random CSR generators and CSR-access suites structurally cannot construct the precondition.
Whether this is isolated
Within the scope swept, the neighbours of this line are right: this is one line and not a general pattern in the file.
What was swept, what was conformant, and the bound
We swept every CSR field written from the write-data bus in
ct_cp0_regs.v, plus thesatppath, looking for this specific shape: legalization that depends on the register's current contents and not on the written value alone. Six neighbours were checked and are conformant, including the two nearest misreads —mtvec.MODEandstvec.MODE, where the store is raw but the readback and the vector the fetch unit receives both drop the illegal bit, so readback and behaviour agree; and themideleg-dependent masking ofsieandsip, which is this same current-value shape on the register where it matters most.We are not claiming that sweep was exhaustive, and it is worth being exact about why: it asked one question, and a field that diverges for some other reason would not answer it. The bound matters as much as the result: readback paths, trap-entry side effects and the custom C910 extension CSRs were not covered, and the sweep is a source-level reading — nothing in it was elaborated or simulated. What it does support is the narrow statement a maintainer needs to prioritise this: within that scope, the neighbours of this line are right, so what follows is one line and not a general pattern in the file.
Practical scenario
Supervisor software probes for Sv48 the conventional way a WARL field's supported values are discovered — and the way the same sentence implies, since it is the rejected write's lack of effect that makes the probe safe: write
satpwith MODE=9 and the Sv48 root, read back, see whether MODE stuck. Per the specification, nothing changed. On this core the software reads MODE=8, correctly concludes Sv48 is unsupported — and has no indication that its address-space root was replaced by the Sv48 root it just probed with, with the micro-TLB flushed so the walker must use it.Suggested change
Apply the guard the module already computes eleven lines above:
That block is a file
git applywill take from the repository root, checked againstb91c909as it stands rather than being retyped here.sha256 7fad1b49639cff090fa189646ddf0ef0a44123021d4a4f81c655343eb6c877b7is over exactly those bytes, sosha256sumon what you paste says whether you have the same file we applied. No line of it carries trailing whitespace, so an ordinary copy out of this page is enough.The one hunk touches only the
satp_asidandsatp_ppnwrite block inct_mmu_regs.v; it does not overlap any other region of that file.Nothing new is computed and no signal is introduced. We re-elaborated the entire SoC from this patched source and re-ran the same seven cases on the same simulator; the reproduction comment carries every line of both runs under The whole output, with the suggested change applied. The four rejected writes become no-ops and both controls are byte-identical to the run above:
We are reporting a divergence, not prescribing a fix — whether this is the right change under your constraints is your call.
The change does not touch
regs_utlb_clrat:238, and we are not asking for that line to change. Once the rejected write is a no-op, flushing the micro-TLB on it costs a re-walk and nothing more; it is architecturally harmless. It is quoted above because it removes an alternative explanation for the readback, not because it is part of the finding.