The Pedigree Project 0.1
Ext2Filesystem-quota.cc
1/* Copyright (c) 2026, Pedigree Developers. */
2#include "pedigree/kernel/LockGuard.h"
3#include "pedigree/kernel/Log.h"
4#include "pedigree/kernel/machine/Disk.h"
5#include "pedigree/kernel/syscallError.h"
6#include "pedigree/kernel/utilities/Pointers.h"
7
8#include "Ext2File.h"
9#include "Ext2Filesystem.h"
10#include "ext2.h"
11
12#ifdef EXT2_STANDALONE
13#include <errno.h>
14#else
15#include "pedigree/kernel/process/Thread.h"
16#include "pedigree/kernel/processor/Processor.h"
17#include "pedigree/kernel/processor/ProcessorInformation.h"
18#endif
19
20int Ext2Filesystem::currentIoError() {
21#ifdef EXT2_STANDALONE
22 const int error = errno;
23#else
24 auto* thread = Processor::information().getCurrentThread();
25 const int error = thread ? thread->getErrno() : 0;
26#endif
27 return error ? error : Error::IoError;
28}
29
30QuotaStatus Ext2Filesystem::quotaIoStatus() {
31 switch (currentIoError()) {
32 case Error::OutOfMemory:
33 return QuotaStatus::NoMemory;
34 case Error::ValueTooLarge:
35 return QuotaStatus::Overflow;
36 case Error::FileTooLarge:
37 return QuotaStatus::TooLarge;
38 case Error::NoSpaceLeftOnDevice:
39 return QuotaStatus::NoSpace;
40 case Error::QuotaExceeded:
41 return QuotaStatus::Limit;
42 default:
43 return QuotaStatus::IoError;
44 }
45}
46
47bool Ext2Filesystem::quotaSucceeded(QuotaStatus status) {
48 if (status == QuotaStatus::Success)
49 return true;
50 syscallError(quotaError(status));
51 return false;
52}
53
54bool Ext2Filesystem::isQuotaFile(uint32_t inode) {
55 LockGuard<Mutex> allocation(m_WriteLock);
56 const auto* charge = m_Quota.find(inode);
57 return charge && charge->exempt;
58}
59
60QuotaStatus Ext2Filesystem::prepareQuotaInodeLocked(uint32_t inode) {
61 if (m_Quota.find(inode))
62 return QuotaStatus::Success;
63 const uint32_t total = LITTLE_TO_HOST32(m_pSuperblock->s_inodes_count);
64 const uint32_t perGroup = LITTLE_TO_HOST32(m_pSuperblock->s_inodes_per_group);
65 if (!inode || !perGroup || (total && inode > total) ||
66 (inode - 1) / perGroup >= m_nGroupDescriptors)
67 return QuotaStatus::Invalid;
68 syscallError(0);
69 const Inode* metadata = getInode(inode);
70 if (!metadata)
71 return quotaIoStatus();
72 return m_Quota.track(inode, Ext2Owner::uid(*metadata), Ext2Owner::gid(*metadata),
73 static_cast<uint64_t>(LITTLE_TO_HOST32(metadata->i_blocks)) * 512);
74}
75
76QuotaStatus Ext2Filesystem::scanQuotaInodesLocked() {
77 const uint32_t perGroup = LITTLE_TO_HOST32(m_pSuperblock->s_inodes_per_group);
78 const uint32_t total = LITTLE_TO_HOST32(m_pSuperblock->s_inodes_count);
79 uint32_t first = LITTLE_TO_HOST32(m_pSuperblock->s_first_ino);
80 if (!LITTLE_TO_HOST32(m_pSuperblock->s_rev_level))
81 first = 11;
82 if (!first)
83 first = 2;
84 if (!perGroup || !total || !m_BlockSize ||
85 (static_cast<uint64_t>(total) + perGroup - 1) / perGroup > m_nGroupDescriptors)
86 return QuotaStatus::IoError;
87 for (uint32_t group = 0; group < m_nGroupDescriptors; ++group) {
88 syscallError(0);
89 if (!ensureFreeInodeBitmapLoaded(group))
90 return quotaIoStatus();
91 for (uint32_t index = 0; index < perGroup; ++index) {
92 const uint64_t inode = static_cast<uint64_t>(group) * perGroup + index + 1;
93 if (inode > total)
94 break;
95 if (inode < first && inode != 2)
96 continue;
97 const size_t field = (index / 8) / m_BlockSize;
98 if (field >= m_pInodeBitmaps[group].count())
99 return QuotaStatus::IoError;
100 const auto* byte = reinterpret_cast<const uint8_t*>(m_pInodeBitmaps[group][field] +
101 (index / 8) % m_BlockSize);
102 if (!(*byte & (1U << (index % 8))))
103 continue;
104 // Existing overlays include allocations not attached to i_blocks yet.
105 const auto status = prepareQuotaInodeLocked(static_cast<uint32_t>(inode));
106 if (status != QuotaStatus::Success)
107 return status;
108 }
109 }
110 return QuotaStatus::Success;
111}
112
113QuotaStatus Ext2Filesystem::flushQuotaLocked(QuotaType type) {
114 auto* file = m_QuotaFiles[static_cast<size_t>(type)];
115 if (!file)
116 return QuotaStatus::Success;
118 bool prepared = false;
119 for (unsigned attempt = 0; attempt < 8; ++attempt) {
120 size_t required;
121 {
122 LockGuard<Mutex> allocation(m_WriteLock);
123 const auto& entries = m_Quota.table(type).entries();
124 required = entries.count();
125 if (required <= snapshot.size()) {
126 for (const auto& entry : entries) {
127 if (entry.dirty)
128 snapshot.pushBack(entry);
129 }
130 prepared = true;
131 break;
132 }
133 }
134 if (!snapshot.tryReserve(required, false))
135 return QuotaStatus::NoMemory;
136 }
137
138 if (!prepared)
139 return QuotaStatus::Busy;
140 QuotaStatus result = QuotaStatus::Success;
141 for (const auto& entry : snapshot) {
142 const auto status = file->writeQuotaRecord(entry.id, entry.record);
143 if (result == QuotaStatus::Success)
144 result = status;
145 }
146 // A successful snapshot is durable before its dirty bits can be cleared.
147 // Even an empty snapshot reaches the device barrier.
148 if (!file->sync())
149 return QuotaStatus::IoError;
150 {
151 LockGuard<Mutex> allocation(m_WriteLock);
152 if (!flushAttributeWritesLocked() || !m_pDisk->syncAll())
153 return QuotaStatus::IoError;
154 }
155 if (result != QuotaStatus::Success)
156 return result;
157 {
158 LockGuard<Mutex> allocation(m_WriteLock);
159 for (const auto& saved : snapshot) {
160 auto* entry = m_Quota.table(type).find(saved.id);
161 if (entry && QuotaTable::sameRecord(entry->record, saved.record))
162 entry->dirty = false;
163 }
164 }
165 return QuotaStatus::Success;
166}
167
168QuotaStatus Ext2Filesystem::flushQuotas() {
169 LockGuard<Mutex> control(m_QuotaControlLock);
170 QuotaStatus result = QuotaStatus::Success;
171 const QuotaType types[] = {QuotaType::User, QuotaType::Group};
172 for (auto type : types) {
173 const auto status = flushQuotaLocked(type);
174 if (result == QuotaStatus::Success)
175 result = status;
176 }
177 return result;
178}
179
180QuotaStatus Ext2Filesystem::quotaControl(const QuotaRequest& request, QuotaResponse& response,
181 File* quotaFile) {
182 OperationBarrier::Lease operation;
183 if (!tryAcquireOperation(operation)) {
184 return QuotaStatus::IoError;
185 }
186 if (request.type != QuotaType::User && request.type != QuotaType::Group)
187 return QuotaStatus::Invalid;
188 if (!m_pDisk || !m_pSuperblock || !m_BlockSize)
189 return QuotaStatus::IoError;
190 LockGuard<Mutex> control(m_QuotaControlLock);
191 const size_t index = static_cast<size_t>(request.type);
192 if (request.operation == QuotaOperation::Enable) {
193 if (m_bReadOnly)
194 return QuotaStatus::ReadOnly;
195 if (request.format != Quota::OldFormat)
196 return QuotaStatus::Unsupported;
197 if (!quotaFile || quotaFile->getFilesystem() != this || quotaFile->isDirectory() ||
198 quotaFile->isSymlink() || quotaFile->isPipe() || quotaFile->isSocket())
199 return QuotaStatus::Invalid;
200 if (m_QuotaFiles[index])
201 return QuotaStatus::Busy;
202 LockGuard<Mutex> names(m_QuotaNamespaceLock);
203 Inode* metadata;
204 {
205 LockGuard<Mutex> allocation(m_WriteLock);
206 metadata = getInode(quotaFile->getInode());
207 }
208 if (!metadata)
209 return QuotaStatus::IoError;
210 if ((LITTLE_TO_HOST16(metadata->i_mode) & 0xf000) != EXT2_S_IFREG)
211 return QuotaStatus::Invalid;
212 auto* file = new Ext2File(quotaFile->getName(), quotaFile->getInode(), metadata, this);
213 if (!file)
214 return QuotaStatus::NoMemory;
215 if (!file->valid()) {
216 delete file;
217 return QuotaStatus::NoMemory;
218 }
219 QuotaTable loaded;
220 auto status = file->beginQuota(loaded);
221 if (status == QuotaStatus::Success) {
222 LockGuard<Mutex> allocation(m_WriteLock);
223 status = scanQuotaInodesLocked();
224 if (status == QuotaStatus::Success) {
225 const uint64_t slots = file->maximumFileSize() / QuotaOld::RecordSize;
226 const uint32_t maximumId = slots > 0x100000000ULL ? 0xffffffffU : slots - 1;
227 status = m_Quota.enable(request.type, loaded, maximumId);
228 }
229 }
230 if (status == QuotaStatus::Success) {
231 m_QuotaFiles[index] = file;
232 status = flushQuotaLocked(request.type);
233 }
234 if (status != QuotaStatus::Success) {
235 file->endQuota(request.type, false);
236 m_QuotaFiles[index] = nullptr;
237 delete file;
238 }
239 return status;
240 }
241
242 if (!m_QuotaFiles[index])
243 return request.operation == QuotaOperation::Sync ? QuotaStatus::Success
244 : QuotaStatus::NotEnabled;
245 if (request.operation == QuotaOperation::Disable) {
246 LockGuard<Mutex> names(m_QuotaNamespaceLock);
247 for (unsigned attempt = 0; attempt < 8; ++attempt) {
248 const auto status = flushQuotaLocked(request.type);
249 if (status != QuotaStatus::Success)
250 return status;
251#if defined(PEDIGREE_BUILDUTILS)
252 if (m_QuotaOffTestHook)
253 m_QuotaOffTestHook(m_QuotaOffTestContext);
254#endif
255 const auto finished = m_QuotaFiles[index]->endQuota(request.type, true);
256 if (finished == QuotaStatus::Busy)
257 continue;
258 if (finished != QuotaStatus::Success)
259 return finished;
260 delete m_QuotaFiles[index];
261 m_QuotaFiles[index] = nullptr;
262 return QuotaStatus::Success;
263 }
264 return QuotaStatus::Busy;
265 }
266 if (request.operation == QuotaOperation::Sync)
267 return flushQuotaLocked(request.type);
268 if (request.operation == QuotaOperation::GetFormat) {
269 response.format = Quota::OldFormat;
270 return QuotaStatus::Success;
271 }
272 if (request.operation == QuotaOperation::Get) {
273 LockGuard<Mutex> allocation(m_WriteLock);
274 const auto* entry = m_Quota.table(request.type).find(request.id);
275 response.record = entry ? entry->record : QuotaRecord();
276 response.record.valid = Quota::Supported;
277 return QuotaStatus::Success;
278 }
279 if (request.operation == QuotaOperation::Set) {
280 if (m_bReadOnly)
281 return QuotaStatus::ReadOnly;
282 if ((static_cast<uint64_t>(request.id) + 1) * QuotaOld::RecordSize >
283 m_QuotaFiles[index]->maximumFileSize())
284 return QuotaStatus::Overflow;
285 QuotaStatus status;
286 {
287 LockGuard<Mutex> allocation(m_WriteLock);
288 status = m_Quota.table(request.type).set(request.id, request.record);
289 }
290 return status == QuotaStatus::Success ? flushQuotaLocked(request.type) : status;
291 }
292 return QuotaStatus::Unsupported;
293}
294
295bool Ext2Filesystem::closeQuotaFiles(bool discardOnFailure) {
296 LockGuard<Mutex> control(m_QuotaControlLock);
297 bool succeeded = true;
298 const QuotaType types[] = {QuotaType::User, QuotaType::Group};
299 for (auto type : types) {
300 const size_t index = static_cast<size_t>(type);
301 if (!m_QuotaFiles[index])
302 continue;
303 if (!isDeviceRemoved() && flushQuotaLocked(type) != QuotaStatus::Success) {
304 succeeded = false;
305 ERROR("Ext2: quota writeback failed at filesystem teardown");
306 if (!discardOnFailure)
307 continue;
308 }
309 m_QuotaFiles[index]->endQuota(type, false);
310 delete m_QuotaFiles[index];
311 m_QuotaFiles[index] = nullptr;
312 }
313 return succeeded;
314}
virtual MUST_USE_RESULT bool syncAll()
Definition Disk.cc:375
size_t m_nGroupDescriptors
Vector< size_t > * m_pInodeBitmaps
Superblock * m_pSuperblock
Definition File.h:75
String getName() const
Definition File.cc:782
virtual bool isSocket() const
Definition File.cc:816
virtual bool isSymlink()
Definition File.cc:800
virtual bool isDirectory()
Definition File.cc:804
virtual bool isPipe() const
Definition File.cc:808
Disk * m_pDisk
Definition Filesystem.h:188
bool m_bReadOnly
Definition Filesystem.h:186
static ProcessorInformation & information()
A vector / dynamic array.
Definition Vector.h:33
Definition ext2.h:152