Làm thế nào để ngăn chặn scanf gây ra tràn bộ đệm trong C?


81

Tôi sử dụng mã này:

while ( scanf("%s", buf) == 1 ){

Cách tốt nhất để ngăn chặn tràn bộ đệm có thể xảy ra để nó có thể được chuyển các chuỗi có độ dài ngẫu nhiên là gì?

Tôi biết tôi có thể giới hạn chuỗi đầu vào bằng cách gọi ví dụ:

while ( scanf("%20s", buf) == 1 ){

Nhưng tôi muốn có thể xử lý bất cứ thứ gì người dùng nhập vào. Hoặc điều này không thể được thực hiện một cách an toàn bằng cách sử dụng scanf và tôi nên sử dụng fgets?

Câu trả lời:


64

Trong cuốn sách Thực hành lập trình của họ (rất đáng đọc), Kernighan và Pike thảo luận về vấn đề này, và họ giải quyết nó bằng cách sử dụng snprintf()để tạo chuỗi với kích thước bộ đệm chính xác để chuyển đến scanf()họ hàm. Có hiệu lực:

int scanner(const char *data, char *buffer, size_t buflen)
{
    char format[32];
    if (buflen == 0)
        return 0;
    snprintf(format, sizeof(format), "%%%ds", (int)(buflen-1));
    return sscanf(data, format, buffer);
}

Lưu ý, điều này vẫn giới hạn đầu vào ở kích thước được cung cấp dưới dạng 'bộ đệm'. Nếu bạn cần thêm dung lượng, thì bạn phải thực hiện cấp phát bộ nhớ hoặc sử dụng một hàm thư viện không chuẩn để cấp phát bộ nhớ cho bạn.


Lưu ý rằng POSIX 2008 (2013) phiên bản của scanf()gia đình các chức năng hỗ trợ sửa đổi định dạng m(một nhân vật nhiệm vụ phân bổ) cho đầu vào chuỗi ( %s, %c, %[). Thay vì lấy một char *đối số, nó lấy một char **đối số và phân bổ không gian cần thiết cho giá trị mà nó đọc:

char *buffer = 0;
if (sscanf(data, "%ms", &buffer) == 1)
{
    printf("String is: <<%s>>\n", buffer);
    free(buffer);
}

Nếu sscanf()hàm không đáp ứng được tất cả các thông số kỹ thuật chuyển đổi, thì tất cả bộ nhớ mà nó phân bổ cho các %mschuyển đổi-like sẽ được giải phóng trước khi hàm trả về.


@Sam: Đúng vậy buflen-1- Cảm ơn bạn. Sau đó, bạn phải lo lắng về dòng dưới không được đánh dấu (gói đến một số lượng khá lớn), do đó, ifthử nghiệm. Tôi rất muốn thay thế nó bằng một assert()hoặc sao lưu nó bằng một assert()trước khi ifnó phát hỏa trong quá trình phát triển nếu bất kỳ ai đó đủ bất cẩn để vượt qua 0 làm kích thước. Tôi đã không xem xét cẩn thận tài liệu để biết %0scó nghĩa là gì sscanf()- kiểm tra có thể tốt hơn if (buflen < 2).
Jonathan Leffler

Vì vậy, snprintfghi một số dữ liệu vào bộ đệm chuỗi và sscanfđọc từ chuỗi đã tạo đó. Chính xác thì điều này thay thế scanfở đâu mà nó đọc từ stdin?
krb686

Cũng khá khó hiểu khi bạn sử dụng từ "định dạng" cho chuỗi kết quả của mình và do đó chuyển vào "định dạng" làm đối số đầu tiên snprintfnhưng nó không phải là tham số định dạng thực tế.
krb686

@ krb686: Mã này được viết để dữ liệu được quét nằm trong tham số datavà do đó sscanf()phù hợp. Thay vào đó, nếu bạn muốn đọc từ đầu vào chuẩn, hãy bỏ datatham số và gọi scanf(). Đối với sự lựa chọn tên formatcho biến trở thành chuỗi định dạng trong lệnh gọi sscanf(), bạn có quyền đổi tên nó nếu bạn muốn, nhưng tên của nó không chính xác. Tôi không chắc thay thế nào có ý nghĩa; sẽ in_formatlàm cho nó rõ ràng hơn? Tôi không định thay đổi nó trong mã này; bạn có thể nếu bạn sử dụng ý tưởng này trong mã của riêng bạn.
Jonathan Leffler

1
@mabraham: Nó vẫn đúng với macOS Sierra 10.12.5 (lên đến 2017-06-06) - scanf()trên macOS không được ghi nhận là hỗ trợ %ms, mặc dù nó sẽ hữu ích.
Jonathan Leffler

30

Nếu bạn đang sử dụng gcc, bạn có thể sử dụng công acụ chỉ định phần mở rộng GNU để scanf () cấp phát bộ nhớ cho bạn để giữ đầu vào:

int main()
{
  char *str = NULL;

  scanf ("%as", &str);
  if (str) {
      printf("\"%s\"\n", str);
      free(str);
  }
  return 0;
}

Chỉnh sửa: Như Jonathan đã chỉ ra, bạn nên tham khảo các scanftrang người đàn ông vì trình xác định có thể khác ( %m) và bạn có thể cần bật một số định nghĩa khi biên dịch.


8
Đó là vấn đề của việc sử dụng glibc (Thư viện GNU C) hơn là sử dụng Trình biên dịch GNU C.
Jonathan Leffler

3
Và lưu ý rằng tiêu chuẩn POSIX 2008 cung cấp công cụ msửa đổi để thực hiện công việc tương tự. Thấy chưa scanf(). Bạn sẽ cần kiểm tra xem hệ thống bạn sử dụng có hỗ trợ công cụ sửa đổi này hay không.
Jonathan Leffler

4
GNU (như được tìm thấy trên Ubuntu 13.10, ở bất kỳ tỷ lệ nào) hỗ trợ %ms. Ký hiệu %alà một từ đồng nghĩa với %f(trên đầu ra, nó yêu cầu dữ liệu dấu phẩy động thập lục phân). Trang người dùng GNU cho scanf()biết: _ Nó không khả dụng nếu chương trình được biên dịch với gcc -std=c99hoặc gcc -D_ISOC99_SOURCE (trừ khi _GNU_SOURCEcũng được chỉ định), trong trường hợp đó, nó ađược hiểu là một mã định nghĩa cho số dấu phẩy động (xem ở trên) ._
Jonathan Leffler

8

Hầu hết thời gian là sự kết hợp của fgetssscanfthực hiện công việc. Việc khác sẽ là viết trình phân tích cú pháp của riêng bạn, nếu đầu vào được định dạng tốt. Cũng lưu ý rằng ví dụ thứ hai của bạn cần một chút sửa đổi để được sử dụng một cách an toàn:

#define LENGTH          42
#define str(x)          # x
#define xstr(x)         str(x)

/* ... */ 
int nc = scanf("%"xstr(LENGTH)"[^\n]%*[^\n]", array); 

Ở trên loại bỏ luồng đầu vào tối đa nhưng không bao gồm ký tự newline ( \n). Bạn sẽ cần phải thêm một getchar()để sử dụng điều này. Ngoài ra, hãy kiểm tra xem bạn đã đến cuối luồng hay chưa:

if (!feof(stdin)) { ...

và đó là về nó.


2
Bạn có thể đặt feofmã vào một bối cảnh lớn hơn không? Tôi đang hỏi vì chức năng đó thường bị sử dụng sai.
Roland Illig,

1
arraycần phải đượcchar array[LENGTH+1];
jxh

4

Việc sử dụng trực tiếp scanf(3)và các biến thể của nó đặt ra một số vấn đề. Thông thường, người dùng và các trường hợp sử dụng không tương tác được xác định theo các dòng đầu vào. Hiếm khi gặp trường hợp, nếu không tìm thấy đủ đối tượng, nhiều dòng hơn sẽ giải quyết được vấn đề, nhưng đó là chế độ mặc định cho scanf. (Nếu người dùng không biết nhập một số trên dòng đầu tiên, thì dòng thứ hai và thứ ba có thể sẽ không hữu ích.)

Ít nhất nếu bạn fgets(3)biết chương trình của bạn sẽ cần bao nhiêu dòng đầu vào và bạn sẽ không bị tràn bộ đệm ...


1

Giới hạn độ dài của đầu vào chắc chắn dễ dàng hơn. Bạn có thể chấp nhận đầu vào dài tùy ý bằng cách sử dụng một vòng lặp, đọc từng chút một, phân bổ lại không gian cho chuỗi nếu cần ...

Nhưng đó là rất nhiều công việc, vì vậy hầu hết các lập trình viên C chỉ cắt đầu vào ở một số độ dài tùy ý. Tôi cho rằng bạn đã biết điều này, nhưng sử dụng fgets () sẽ không cho phép bạn chấp nhận số lượng văn bản tùy ý - bạn vẫn cần đặt giới hạn.


Vậy có ai biết cách làm điều đó với scanf không?
goe

3
Sử dụng các tiện ích trong một vòng lặp có thể cho phép bạn chấp nhận lượng văn bản tùy ý - chỉ cần tiếp tục nhập realloc()bộ đệm của bạn.
bdonlan

1

Việc tạo ra một hàm phân bổ bộ nhớ cần thiết cho chuỗi của bạn không phải là quá nhiều. Đó là một hàm c nhỏ mà tôi đã viết cách đây một thời gian, tôi luôn sử dụng nó để đọc trong chuỗi.

Nó sẽ trả về chuỗi đã đọc hoặc nếu lỗi bộ nhớ xảy ra NULL. Nhưng lưu ý rằng bạn phải giải phóng () chuỗi của mình và luôn kiểm tra giá trị trả về của nó.

#define BUFFER 32

char *readString()
{
    char *str = malloc(sizeof(char) * BUFFER), *err;
    int pos;
    for(pos = 0; str != NULL && (str[pos] = getchar()) != '\n'; pos++)
    {
        if(pos % BUFFER == BUFFER - 1)
        {
            if((err = realloc(str, sizeof(char) * (BUFFER + pos + 1))) == NULL)
                free(str);
            str = err;
        }
    }
    if(str != NULL)
        str[pos] = '\0';
    return str;
}

sizeof (char)là theo định nghĩa 1. Bạn không cần nó ở đây.
RastaJedi

Thông thường, thực hành tốt là giữ phân bổ / giải phóng con trỏ ở cùng một mức, có nghĩa là hàm của bạn không nên tự cấp phát bộ nhớ, vì sau đó người gọi phải giải phóng nó. Hầu hết các hàm thư viện / posix tiêu chuẩn tuân thủ nguyên tắc này bằng cách trả về một chuỗi tĩnh (giống như strerror(3)) hoặc mong đợi một chuỗi được cấp phát trước được chuyển vào (như ( strerror_r(3)- hoặc scanf(3)) ...
Michael Beer
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.